Skip to content

HTTPS clone URL

Subversion checkout URL

You can clone with HTTPS or Subversion.

Download ZIP
Fetching contributors…

Cannot retrieve contributors at this time

3824 lines (3365 sloc) 136.134 kb
/*
Copyright (c) 2004-2006, Greg Hulands <ghulands@mac.com>
All rights reserved.
Redistribution and use in source and binary forms, with or without modification,
are permitted provided that the following conditions are met:
Redistributions of source code must retain the above copyright notice, this list
of conditions and the following disclaimer.
Redistributions in binary form must reproduce the above copyright notice, this
list of conditions and the following disclaimer in the documentation and/or other
materials provided with the distribution.
Neither the name of Greg Hulands nor the names of its contributors may be used to
endorse or promote products derived from this software without specific prior
written permission.
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY
EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES
OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT
SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT,
INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED
TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR
BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY
WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/
#import "CKFTPConnection.h"
#import "CKConnectionThreadManager.h"
#import "RunLoopForwarder.h"
#import "CKInternalTransferRecord.h"
#import "CKTransferRecord.h"
#import "NSFileManager+Connection.h"
#import "NSObject+Connection.h"
#import "CKCacheableHost.h"
#import "CKConnectionProtocol.h"
#import "CKURLProtectionSpace.h"
#import <sys/dirent.h>
#import <sys/types.h>
#import <sys/socket.h>
#import <netinet/in.h>
NSString *CKFTPErrorDomain = @"FTPErrorDomain";
// 500 ms.
const double kDelegateNotificationTheshold = 0.5;
@interface CKTransferRecord (Internal)
- (void)setSize:(unsigned long long)size;
@end
@interface CKFTPConnection (Private)
- (void)closeDataConnection;
- (void)handleDataReceivedEvent:(NSStreamEvent)eventCode;
- (void)handleDataSendStreamEvent:(NSStreamEvent)eventCode;
- (void)closeDataStreams;
- (void)openDataStreamsToHost:(NSHost *)aHost port:(int)aPort;
- (CKConnectionCommand *)pushDataConnectionOnCommandQueue;
- (CKConnectionCommand *)nextAvailableDataConnectionType;
- (CKTransferRecord *)uploadFile:(NSString *)localPath
orData:(NSData *)data
offset:(unsigned long long)offset
remotePath:(NSString *)remotePath
checkRemoteExistence:(BOOL)flag
delegate:(id)delegate;
- (NSFileHandle *)writeHandle;
- (void)setWriteHandle:(NSFileHandle *)aWriteHandle;
- (NSFileHandle *)readHandle;
- (void)setReadHandle:(NSFileHandle *)aReadHandle;
- (NSData *)readData;
- (void)setReadData:(NSData *)aReadData;
- (NSString *)currentPath;
- (void)setCurrentPath:(NSString *)aCurrentPath;
- (BOOL)isAboveNotificationTimeThreshold:(NSDate *)date;
- (NSString *)setupEPRTConnection; //returns the command after creating a socket
- (NSString *)setupActiveConnection; //return the cmmand after creating a socket
- (void)setDataInputStreamAndOpen:(NSInputStream *)iStream outputStream:(NSOutputStream *)oStream socket:(CFSocketNativeHandle)socket;
- (void)prepareAndOpenDataStreams;
//Command Handling
- (void)_receivedCodeInConnectionNotConnectedState:(int)code command:(NSString *)command buffer:(NSString *)buffer;
- (void)_receivedCodeInConnectionSentUsernameState:(int)code command:(NSString *)command buffer:(NSString *)buffer;
- (void)_receivedCodeInConnectionSentAccountState:(int)code command:(NSString *)command buffer:(NSString *)buffer;
- (void)_receivedCodeInConnectionSentPasswordState:(int)code command:(NSString *)command buffer:(NSString *)buffer;
- (void)_receivedCodeInConnectionAwaitingCurrentDirectoryState:(int)code command:(NSString *)command buffer:(NSString *)buffer;
- (void)_receivedCodeInConnectionAwaitingDirectoryContentsState:(int)code command:(NSString *)command buffer:(NSString *)buffer;
- (void)_receivedCodeInConnectionChangingDirectoryState:(int)code command:(NSString *)command buffer:(NSString *)buffer;
- (void)_receivedCodeInConnectionCreateDirectoryState:(int)code command:(NSString *)command buffer:(NSString *)buffer;
- (void)_receivedCodeInConnectionDeleteDirectoryState:(int)code command:(NSString *)command buffer:(NSString *)buffer;
- (void)_receivedCodeInConnectionRenameFromState:(int)code command:(NSString *)command buffer:(NSString *)buffer;
- (void)_receivedCodeInConnectionAwaitingRenameState:(int)code command:(NSString *)command buffer:(NSString *)buffer;
- (void)_receivedCodeInConnectionDeleteFileState:(int)code command:(NSString *)command buffer:(NSString *)buffer;
- (void)_receivedCodeInConnectionDownloadingFileState:(int)code command:(NSString *)command buffer:(NSString *)buffer;
- (void)_receivedCodeInConnectionUploadingFileState:(int)code command:(NSString *)command buffer:(NSString *)buffer;
- (void)_receivedCodeInConnectionSentOffsetState:(int)code command:(NSString *)command buffer:(NSString *)buffer;
- (void)_receivedCodeInConnectionSentFeatureRequestState:(int)code command:(NSString *)command buffer:(NSString *)buffer;
- (void)_receivedCodeInConnectionSentQuitState:(int)code command:(NSString *)command buffer:(NSString *)buffer;
- (void)_receivedCodeInConnectionSettingPermissionsState:(int)code command:(NSString *)command buffer:(NSString *)buffer;
- (void)_receivedCodeInConnectionSentSizeState:(int)code command:(NSString *)command buffer:(NSString *)buffer;
- (void)_receivedCodeInConnectionSentDisconnectState:(int)code command:(NSString *)command buffer:(NSString *)buffer;
- (void)_receivedCodeInFTPSettingPassiveState:(int)code command:(NSString *)command buffer:(NSString *)buffer;
- (void)_receivedCodeInFTPSettingEPSVState:(int)code command:(NSString *)command buffer:(NSString *)buffer;
- (void)_receivedCodeInFTPSettingActiveState:(int)code command:(NSString *)command buffer:(NSString *)buffer;
- (void)_receivedCodeInFTPSettingEPRTState:(int)code command:(NSString *)command buffer:(NSString *)buffer;
- (void)_receivedCodeInFTPAwaitingRemoteSystemTypeState:(int)code command:(NSString *)command buffer:(NSString *)buffer;
- (void)_receivedCodeInFTPChangeDirectoryListingStyleState:(int)code command:(NSString *)command buffer:(NSString *)buffer;
@end
@interface CKFTPConnection (Authentication) <NSURLAuthenticationChallengeSender>
@property(nonatomic, copy) NSURLCredential *currentAuthenticationCredential;
- (void)authenticateConnection;
- (void)sendPassword;
@end
#pragma mark -
void dealWithConnectionSocket(CFSocketRef s, CFSocketCallBackType type,
CFDataRef address, const void *data, void *info);
#pragma mark -
@implementation CKFTPConnection
+ (void)load // registration of this class
{
NSAutoreleasePool *pool = [[NSAutoreleasePool alloc] init];
[[CKConnectionRegistry sharedConnectionRegistry] registerClass:self forName:[self name] URLScheme:@"ftp"];
[pool release];
}
+ (NSInteger)defaultPort { return 21; }
+ (NSString *)name
{
return @"FTP";
}
+ (NSArray *)URLSchemes
{
return [NSArray arrayWithObject:@"ftp"];
}
- (id)initWithRequest:(NSURLRequest *)request
{
if (self = [super initWithRequest:request])
{
[self setState:CKConnectionNotConnectedState];
// These are never replaced during the lifetime of this object so we don't bother with accessor methods
_dataBuffer = [[NSMutableData data] retain];
_commandBuffer = [[NSMutableString alloc] initWithString:@""];
_ftpFlags.canUseActive = YES;
_ftpFlags.canUseEPRT = YES;
_ftpFlags.canUsePASV = YES;
_ftpFlags.canUseEPSV = YES;
_ftpFlags.hasSize = YES;
_isConnected = NO;
}
return self;
}
- (void)dealloc
{
[self closeDataStreams];
[_buffer release];
[_commandBuffer release];
[_dataBuffer release];
[_writeHandle release];
[self setReadHandle:nil];
[self setWriteHandle:nil];
[self setReadData:nil];
[self setCurrentPath:nil];
[_rootPath release];
[_lastNotified release];
[_noopTimer invalidate];
[_noopTimer release];
[_lastAuthenticationChallenge release];
[_currentAuthenticationCredential release];
[super dealloc];
}
#pragma mark -
#pragma mark Commands
/*! Called from the background thread.
*/
- (void)sendCommand:(id)command
{
/* clang flagged these as not being used
NSString *stringOnlyCommand = command;
if ([command isKindOfClass:[NSInvocation class]])
{
stringOnlyCommand = NSStringFromSelector([command selector]);
}
*/
NSUserDefaults *defaults = [NSUserDefaults standardUserDefaults];
if ([command isKindOfClass:[NSInvocation class]])
{
[command invoke];
return;
}
if ([command isEqualToString:@"DATA_CON"])
{
CKConnectionCommand *cmd = [self nextAvailableDataConnectionType];
[self pushCommandOnHistoryQueue:cmd];
command = [cmd command];
_state = [cmd sentState];
[self closeDataConnection];
}
if ([command isEqualToString:@"EPRT"])
{
_ftpFlags.isActiveDataConn = YES;
command = [self setupEPRTConnection];
}
else if ([command isEqualToString:@"PORT"])
{
_ftpFlags.isActiveDataConn = YES;
command = [self setupActiveConnection];
}
else if ([command isEqualToString:@"EPSV"])
{
_ftpFlags.isActiveDataConn = NO;
}
else if ([command isEqualToString:@"PASV"])
{
_ftpFlags.isActiveDataConn = NO;
}
else if ([command isEqualToString:@"LIST -a"] && _ftpFlags.isMicrosoft)
{
command = @"LIST";
}
NSString *formattedCommand = [NSString stringWithFormat:@"%@\r\n", command];
NSString *commandToEcho = command;
if ([command rangeOfString:@"PASS"].location != NSNotFound)
{
if (![defaults boolForKey:@"AllowPasswordToBeLogged"])
{
commandToEcho = @"PASS ####";
}
}
[[self client] appendString:commandToEcho toTranscript:CKTranscriptSent];
KTLog(CKProtocolDomain, KTLogDebug, @">> %@", commandToEcho);
if ([formattedCommand rangeOfString:@"RETR"].location != NSNotFound)
{
CKInternalTransferRecord *download = [self currentDownload];
[[self client] downloadDidBegin:[download remotePath]];
if ([download delegateRespondsToTransferDidBegin])
{
[[download delegate] transferDidBegin:[download userInfo]];
}
}
if ([formattedCommand rangeOfString:@"STOR"].location != NSNotFound)
{
CKInternalTransferRecord *upload = [self currentUpload];
[[self client] uploadDidBegin:[upload remotePath]];
if ([upload delegateRespondsToTransferDidBegin])
{
[[upload delegate] transferDidBegin:[upload userInfo]];
}
}
[self sendData:[formattedCommand dataUsingEncoding:NSUTF8StringEncoding]];
}
/*! The main communication between the foreground thread and the background thread. Called by EITHER thread.
*/
/*! Parse the response received from the server. Called from the background thread.
*/
- (void)parseCommand:(NSString *)command
{
NSScanner *scanner = [NSScanner scannerWithString:command];
int code;
[scanner scanInt:&code];
// we need to consume everything until 'xxx '
NSMutableString *buffer = [NSMutableString stringWithFormat:@"%@\n", command];
BOOL atEnd = NO;
NSRange r;
NSString *strCode = [NSString stringWithFormat:@"%d ", code];
if (![command hasPrefix:strCode])
{
if ((r = [_commandBuffer rangeOfString:strCode]).location != NSNotFound) {
[buffer appendString:_commandBuffer];
//need to drop out of the commandBuffer up to the new line.
NSRange newLineRange;
NSRange toEnd = NSMakeRange(r.location, [_commandBuffer length] - r.location);
if ((newLineRange = [_commandBuffer rangeOfString:@"\r\n"
options:NSCaseInsensitiveSearch
range:toEnd]).location != NSNotFound
|| (newLineRange = [_commandBuffer rangeOfString:@"\n"
options:NSCaseInsensitiveSearch
range:toEnd]).location != NSNotFound)
[_commandBuffer deleteCharactersInRange:NSMakeRange(0,newLineRange.location+newLineRange.length)];
atEnd = YES;
}
else
{
[buffer appendString:_commandBuffer];
[_commandBuffer deleteCharactersInRange:NSMakeRange(0, [_commandBuffer length])];
}
while (atEnd == NO)
{
[NSThread sleepUntilDate:[NSDate dateWithTimeIntervalSinceNow:0.1]];
NSData *data = [self availableData];
if ([data length] > 0)
{
NSString *line = [[NSString alloc] initWithData:data encoding:NSUTF8StringEncoding];
if (line)
[buffer appendString:line];
if ([line rangeOfString:strCode].location != NSNotFound)
atEnd = YES;
[line release];
}
}
}
[[self client] appendString:buffer toTranscript:CKTranscriptReceived];
KTLog(CKProtocolDomain, KTLogDebug, @"<<# %@", command); /// use <<# to help find commands
int stateToHandle = GET_STATE;
//State independent handling
switch (code)
{
case 200: //Command okay
case 202:
case 215: //NAME system type
case 250:
case 257: //Path Created
case 450: //Requested file action not taken. File unavailable.
case 451: //Requested acion aborted, local error in processing
case 504: //Command not implemented for that parameter (Karelia Case 28078, FileZilla servers do not allow CHMOD, returning 504)
case 551:
{
[self setState:CKConnectionIdleState];
break;
}
case 150: //File status okay, about to open data connection.
{
if ([command rangeOfString:@"directory listing"].location != NSNotFound) //Sometimes we get "150 Here comes the directory listing"
{
//we'll clean the buffer
[_buffer setLength:0];
}
break;
}
case 226:
{
_ftpFlags.received226 = YES;
if ([command rangeOfString:@"abort" options:NSCaseInsensitiveSearch].location != NSNotFound)
{
NSString *remotePath = [NSString string];
if ([self currentUpload] != nil)
{
remotePath = [NSString stringWithString:[[self currentUpload] remotePath]];
//Dequeue and close any handles just as if we finished the upload
[self dequeueUpload];
[self setReadData:nil];
[self setReadHandle:nil];
_transferSize = 0;
}
else if ([self currentDownload] != nil)
{
remotePath = [NSString stringWithString:[[self currentDownload] remotePath]];
//Dequeue and close any handles just as if we finished the download
[self dequeueDownload];
[_writeHandle closeFile];
[self setWriteHandle:nil];
}
[[self client] connectionDidCancelTransfer:remotePath];
}
if (_dataSendStream == nil || _dataReceiveStream == nil)
{
[self setState:CKConnectionIdleState];
}
break;
}
case 332: //need account
{
NSString *account = [[self client] accountForUsername:nil];
if (account)
{
[self sendCommand:[NSString stringWithFormat:@"ACCT %@", account]];
[self setState:CKConnectionSentAccountState];
}
break;
}
case 421: //service timed out.
{
[self closeDataStreams];
[super threadedDisconnect]; //This empties the queues, etc.
_isConnected = NO;
[[self client] connectionDidDisconnectFromHost:[[[self request] URL] host]];
NSString *localizedDescription = LocalizedStringInConnectionKitBundle(@"FTP service not available; Remote server has closed connection", @"FTP service timed out");
NSDictionary *userInfo = [NSDictionary dictionaryWithObjectsAndKeys:
localizedDescription, NSLocalizedDescriptionKey,
command, NSLocalizedFailureReasonErrorKey,
[[self request] URL], ConnectionHostKey, nil];
NSError *error = [NSError errorWithDomain:CKFTPErrorDomain code:code userInfo:userInfo];
[[self client] connectionDidReceiveError:error];
[self setState:CKConnectionNotConnectedState];
break;
}
case 503: //Bad sequence of commands
{
//This is an internal error in the syntax of the commands and arguments sent.
//We should never get to this state as we should construct commands correctly.
if (GET_STATE != CKConnectionSentFeatureRequestState)
{
KTLog(CKProtocolDomain, KTLogError, @"FTP Internal Error: %@", command);
// We should just see if we can process the next command
[self setState:CKConnectionIdleState];
break;
}
else
{
[self authenticateConnection];
break;
}
}
case 522:
{
_ftpFlags.canUseEPRT = NO;
[self sendCommand:@"DATA_CON"];
break;
}
case 530: //User not logged in
{
if (stateToHandle != CKConnectionSentPasswordState) // in that state, want to retry auth
{
NSDictionary *userInfo = [NSDictionary dictionaryWithObjectsAndKeys:
LocalizedStringInConnectionKitBundle(@"Not Logged In", @"FTP Error"), NSLocalizedDescriptionKey,
command, NSLocalizedFailureReasonErrorKey,
[[[self request] URL] host], ConnectionHostKey, nil];
NSError *error = [NSError errorWithDomain:CKFTPErrorDomain code:code userInfo:userInfo];
userInfo = [NSDictionary dictionaryWithObjectsAndKeys:
error, NSUnderlyingErrorKey,
[error localizedDescription], NSLocalizedDescriptionKey,
nil];
error = [NSError errorWithDomain:NSURLErrorDomain code:NSURLErrorUserAuthenticationRequired userInfo:userInfo];
[[self client] connectionDidReceiveError:error];
}
break;
}
default:
break;
}
switch (stateToHandle)
{
case CKConnectionNotConnectedState:
[self _receivedCodeInConnectionNotConnectedState:code command:command buffer:buffer];
break;
case CKConnectionSentUsernameState:
[self _receivedCodeInConnectionSentUsernameState:code command:command buffer:buffer];
break;
case CKConnectionSentAccountState:
[self _receivedCodeInConnectionSentAccountState:code command:command buffer:buffer];
break;
case CKConnectionSentPasswordState:
[self _receivedCodeInConnectionSentPasswordState:code command:command buffer:buffer];
break;
case CKConnectionAwaitingCurrentDirectoryState:
[self _receivedCodeInConnectionAwaitingCurrentDirectoryState:code command:command buffer:buffer];
break;
case CKConnectionAwaitingDirectoryContentsState:
[self _receivedCodeInConnectionAwaitingDirectoryContentsState:code command:command buffer:buffer];
break;
case CKConnectionChangingDirectoryState:
[self _receivedCodeInConnectionChangingDirectoryState:code command:command buffer:buffer];
break;
case CKConnectionCreateDirectoryState:
[self _receivedCodeInConnectionCreateDirectoryState:code command:command buffer:buffer];
break;
case CKConnectionDeleteDirectoryState:
[self _receivedCodeInConnectionDeleteDirectoryState:code command:command buffer:buffer];
break;
case CKConnectionRenameFromState:
[self _receivedCodeInConnectionRenameFromState:code command:command buffer:buffer];
break;
case CKConnectionAwaitingRenameState:
[self _receivedCodeInConnectionAwaitingRenameState:code command:command buffer:buffer];
break;
case CKConnectionDeleteFileState:
[self _receivedCodeInConnectionDeleteFileState:code command:command buffer:buffer];
break;
case CKConnectionDownloadingFileState:
[self _receivedCodeInConnectionDownloadingFileState:code command:command buffer:buffer];
break;
case CKConnectionUploadingFileState:
[self _receivedCodeInConnectionUploadingFileState:code command:command buffer:buffer];
break;
case CKConnectionSentOffsetState:
[self _receivedCodeInConnectionSentOffsetState:code command:command buffer:buffer];
break;
case CKConnectionSentFeatureRequestState:
[self _receivedCodeInConnectionSentFeatureRequestState:code command:command buffer:buffer];
break;
case CKConnectionSentQuitState:
[self _receivedCodeInConnectionSentQuitState:code command:command buffer:buffer];
break;
case CKConnectionSettingPermissionsState:
[self _receivedCodeInConnectionSettingPermissionsState:code command:command buffer:buffer];
break;
case CKConnectionSentSizeState:
[self _receivedCodeInConnectionSentSizeState:code command:command buffer:buffer];
break;
case CKConnectionSentDisconnectState:
[self _receivedCodeInConnectionSentDisconnectState:code command:command buffer:buffer];
break;
case FTPSettingPassiveState:
[self _receivedCodeInFTPSettingPassiveState:code command:command buffer:buffer];
break;
case FTPSettingEPSVState:
[self _receivedCodeInFTPSettingEPSVState:code command:command buffer:buffer];
break;
case FTPSettingActiveState:
[self _receivedCodeInFTPSettingActiveState:code command:command buffer:buffer];
break;
case FTPSettingEPRTState:
[self _receivedCodeInFTPSettingEPRTState:code command:command buffer:buffer];
break;
case FTPAwaitingRemoteSystemTypeState:
[self _receivedCodeInFTPAwaitingRemoteSystemTypeState:code command:command buffer:buffer];
break;
case FTPChangeDirectoryListingStyle:
[self _receivedCodeInFTPChangeDirectoryListingStyleState:code command:command buffer:buffer];
break;
default:
break;
}
}
- (void)_receivedCodeInConnectionNotConnectedState:(int)code command:(NSString *)command buffer:(NSString *)buffer
{
switch (code)
{
case 120: //Service Ready
{
NSDictionary *userInfo = [NSDictionary dictionaryWithObjectsAndKeys:
LocalizedStringInConnectionKitBundle(@"FTP Service Unavailable", @"FTP no service"), NSLocalizedDescriptionKey,
command, NSLocalizedFailureReasonErrorKey,
[[[self request] URL] host], ConnectionHostKey, nil];
NSError *error = [NSError errorWithDomain:CKFTPErrorDomain code:code userInfo:userInfo];
[[self client] connectionDidConnectToHost:[[[self request] URL] host] error:error];
[self setState:CKConnectionNotConnectedState]; //don't really need.
break;
}
case 220: //Service Ready For New User
{
if (_ftpFlags.loggedIn != NO)
break;
if ([command rangeOfString:@"Microsoft FTP Service"].location != NSNotFound ||
[buffer rangeOfString:@"Microsoft FTP Service"].location != NSNotFound)
{
_ftpFlags.isMicrosoft = YES;
}
else
{
_ftpFlags.isMicrosoft = NO;
}
[self authenticateConnection];
break;
}
default:
break;
}
}
- (void)_receivedCodeInConnectionSentUsernameState:(int)code command:(NSString *)command buffer:(NSString *)buffer
{
switch (code)
{
case 230: //User logged in, proceed
{
if (![[_currentAuthenticationCredential user] isEqualToString:@"anonymous"]) break;
_ftpFlags.loggedIn = YES;
// Queue up the commands we want to insert in the queue before notifying client we're connected
// We get the current directory -- and we're notified of a change directory ... so we'll know what directory
// we are starting in.
CKConnectionCommand *getCurrentDirectoryCommand = [CKConnectionCommand command:@"PWD"
awaitState:CKConnectionIdleState
sentState:CKConnectionAwaitingCurrentDirectoryState
dependant:nil
userInfo:nil];
[self pushCommandOnCommandQueue:getCurrentDirectoryCommand];
CKConnectionCommand *getRemoteSystemTypeCommand = [CKConnectionCommand command:@"SYST"
awaitState:CKConnectionIdleState
sentState:FTPAwaitingRemoteSystemTypeState
dependant:nil
userInfo:nil];
[self pushCommandOnCommandQueue:getRemoteSystemTypeCommand];
// What features does the server support?
CKConnectionCommand *featuresCommand = [CKConnectionCommand command:@"FEAT"
awaitState:CKConnectionIdleState
sentState:CKConnectionSentFeatureRequestState
dependant:nil
userInfo:nil];
[self pushCommandOnCommandQueue:featuresCommand];
[self setState:CKConnectionIdleState];
break;
}
case 331: //User name okay, need password.
{
[self sendPassword];
break;
}
default:
break;
}
}
- (void)_receivedCodeInConnectionSentAccountState:(int)code command:(NSString *)command buffer:(NSString *)buffer
{
switch (code)
{
case 230: //User logged in, proceed
{
[self sendPassword];
break;
}
case 530: //User not logged in
{
// TODO: Attempt authentication again
/*NSDictionary *userInfo = [NSDictionary dictionaryWithObjectsAndKeys:
LocalizedStringInConnectionKitBundle(@"Invalid Account name", @"FTP Error"), NSLocalizedDescriptionKey,
command, NSLocalizedFailureReasonErrorKey,
[self host], ConnectionHostKey, nil];
*/
break;
}
default:
break;
}
}
- (void)_receivedCodeInConnectionSentPasswordState:(int)code command:(NSString *)command buffer:(NSString *)buffer
{
switch(code)
{
case 230: //User logged in, proceed
{
_ftpFlags.loggedIn = YES;
// Appropriately store the credentials once the password has been sent
[[NSURLCredentialStorage sharedCredentialStorage] setDefaultCredential:[self currentAuthenticationCredential]
forProtectionSpace:[_lastAuthenticationChallenge protectionSpace]];
[self setCurrentAuthenticationCredential:nil];
// We get the current directory -- and we're notified of a change directory ... so we'll know what directory
// we are starting in.
CKConnectionCommand *getCurrentDirectoryCommand = [CKConnectionCommand command:@"PWD"
awaitState:CKConnectionIdleState
sentState:CKConnectionAwaitingCurrentDirectoryState
dependant:nil
userInfo:nil];
[self pushCommandOnCommandQueue:getCurrentDirectoryCommand];
CKConnectionCommand *getRemoteSystemTypeCommand = [CKConnectionCommand command:@"SYST"
awaitState:CKConnectionIdleState
sentState:FTPAwaitingRemoteSystemTypeState
dependant:nil
userInfo:nil];
[self pushCommandOnCommandQueue:getRemoteSystemTypeCommand];
// What features does the server support?
CKConnectionCommand *featuresCommand = [CKConnectionCommand command:@"FEAT"
awaitState:CKConnectionIdleState
sentState:CKConnectionSentFeatureRequestState
dependant:nil
userInfo:nil];
[self pushCommandOnCommandQueue:featuresCommand];
[self setState:CKConnectionIdleState];
break;
}
case 530: // Authentication failed. We shall request fresh authentication
{
[self authenticateConnection];
break;
}
default:
break;
}
}
- (void)_receivedCodeInConnectionAwaitingCurrentDirectoryState:(int)code command:(NSString *)command buffer:(NSString *)buffer
{
NSError *error = nil;
NSString *path = [self scanBetweenQuotes:command];
if (!path || [path length] == 0)
path = [[[[self lastCommand] command] substringFromIndex:4] stringByTrimmingCharactersInSet:[NSCharacterSet whitespaceAndNewlineCharacterSet]];
switch (code)
{
case 257: //Path Created
{
[self setCurrentPath:path];
if (_rootPath == nil)
_rootPath = [path copy];
break;
}
case 421:
{
NSString *localizedDescription = LocalizedStringInConnectionKitBundle(@"FTP service not available; Remote server has closed connection", @"FTP service timed out");
NSDictionary *userInfo = [NSDictionary dictionaryWithObjectsAndKeys:
localizedDescription, NSLocalizedDescriptionKey,
command, NSLocalizedFailureReasonErrorKey,
[[[self request] URL] host], ConnectionHostKey, nil];
error = [NSError errorWithDomain:CKFTPErrorDomain code:code userInfo:userInfo];
break;
}
case 550: //Requested action not taken, file not found. //Permission Denied
{
NSDictionary *userInfo = [NSDictionary dictionaryWithObjectsAndKeys:LocalizedStringInConnectionKitBundle(@"Permission Denied", @"Permission Denied"), NSLocalizedDescriptionKey, path, NSFilePathErrorKey, nil];
error = [NSError errorWithDomain:CKFTPErrorDomain code:code userInfo:userInfo];
}
default:
break;
}
[[self client] connectionDidChangeToDirectory:path error:error];
}
- (void)_receivedCodeInConnectionAwaitingDirectoryContentsState:(int)code command:(NSString *)command buffer:(NSString *)buffer
{
switch (code)
{
case 425: //Couldn't open data connection
{
CKConnectionCommand *last = [self lastCommand];
CKConnectionState lastState = [[[self commandHistory] objectAtIndex:1] sentState];
if (lastState == FTPSettingEPSVState)
{
_ftpFlags.canUseEPSV = NO;
[self closeDataStreams];
[self sendCommand:@"DATA_CON"];
[self pushCommandOnCommandQueue:last];
}
else if (lastState == FTPSettingEPRTState)
{
_ftpFlags.canUseEPRT = NO;
[self closeDataStreams];
[self sendCommand:@"DATA_CON"];
[self pushCommandOnCommandQueue:last];
}
else if (lastState == FTPSettingActiveState)
{
_ftpFlags.canUseActive = NO;
[self closeDataStreams];
[self sendCommand:@"DATA_CON"];
[self pushCommandOnCommandQueue:last];
}
else if (lastState == FTPSettingPassiveState)
{
_ftpFlags.canUsePASV = NO;
[self closeDataStreams];
[self sendCommand:@"DATA_CON"];
[self pushCommandOnCommandQueue:last];
}
break;
}
default:
break;
}
}
- (void)_receivedCodeInConnectionChangingDirectoryState:(int)code command:(NSString *)command buffer:(NSString *)buffer
{
NSError *error = nil;
NSString *path = [self scanBetweenQuotes:command];
if (!path || [path length] == 0)
path = [[[[self lastCommand] command] substringFromIndex:4] stringByTrimmingCharactersInSet:[NSCharacterSet whitespaceAndNewlineCharacterSet]];
switch (code)
{
case 421:
{
NSString *localizedDescription = LocalizedStringInConnectionKitBundle(@"FTP service not available; Remote server has closed connection", @"FTP service timed out");
NSDictionary *userInfo = [NSDictionary dictionaryWithObjectsAndKeys:
localizedDescription, NSLocalizedDescriptionKey,
command, NSLocalizedFailureReasonErrorKey,
[[[self request] URL] host], ConnectionHostKey, nil];
error = [NSError errorWithDomain:CKFTPErrorDomain code:code userInfo:userInfo];
break;
}
case 500: //Syntax error, command unrecognized.
case 501: //Syntax error in parameters or arguments.
case 502: //Command not implemented
{
NSDictionary *userInfo = [NSDictionary dictionaryWithObjectsAndKeys:
LocalizedStringInConnectionKitBundle(@"Failed to change to directory", @"Bad ftp command"), NSLocalizedDescriptionKey,
command, NSLocalizedFailureReasonErrorKey, path, NSFilePathErrorKey, nil];
error = [NSError errorWithDomain:CKFTPErrorDomain code:ConnectionErrorChangingDirectory userInfo:userInfo];
break;
}
case 550: //Requested action not taken, file not found. //Permission Denied
{
NSDictionary *userInfo = [NSDictionary dictionaryWithObjectsAndKeys:LocalizedStringInConnectionKitBundle(@"Permission Denied", @"Permission Denied"), NSLocalizedDescriptionKey, path, NSFilePathErrorKey, nil];
error = [NSError errorWithDomain:CKFTPErrorDomain code:code userInfo:userInfo];
break;
}
default:
break;
}
[[self client] connectionDidChangeToDirectory:path error:error];
[self setState:CKConnectionIdleState];
}
- (void)_receivedCodeInConnectionCreateDirectoryState:(int)code command:(NSString *)command buffer:(NSString *)buffer
{
NSError *error = nil;
switch (code)
{
case 421:
{
NSString *localizedDescription = LocalizedStringInConnectionKitBundle(@"FTP service not available; Remote server has closed connection", @"FTP service timed out");
NSDictionary *userInfo = [NSDictionary dictionaryWithObjectsAndKeys:
localizedDescription, NSLocalizedDescriptionKey,
command, NSLocalizedFailureReasonErrorKey,
[[[self request] URL] host], ConnectionHostKey, nil];
error = [NSError errorWithDomain:CKFTPErrorDomain code:code userInfo:userInfo];
break;
}
case 521: //Supported Address Families
{
if (!_isRecursiveUploading)
{
NSString *localizedDescription = LocalizedStringInConnectionKitBundle(@"Create directory operation failed", @"FTP Create directory error");
NSString *path = nil;
NSMutableDictionary *userInfo = [NSMutableDictionary dictionary];
if ([command rangeOfString:@"exists"].location != NSNotFound)
{
[userInfo setObject:[NSNumber numberWithBool:YES] forKey:ConnectionDirectoryExistsKey];
if ([command rangeOfString:@":"].location != NSNotFound)
{
path = [command substringWithRange:NSMakeRange(4, [command rangeOfString:@":"].location - 4)];
[userInfo setObject:path forKey:ConnectionDirectoryExistsFilenameKey];
[userInfo setObject:path forKey:NSFilePathErrorKey];
}
}
[userInfo setObject:localizedDescription forKey:NSLocalizedDescriptionKey];
[userInfo setObject:command forKey:NSLocalizedFailureReasonErrorKey];
error = [NSError errorWithDomain:CKFTPErrorDomain code:code userInfo:userInfo];
}
break;
}
case 550: //Requested action not taken, file not found.
{
NSMutableDictionary *userInfo = [NSMutableDictionary dictionary];
NSString *localizedDescription = LocalizedStringInConnectionKitBundle(@"Create directory operation failed", @"FTP Create directory error");
//Some servers won't say that the directory exists. Once I get peer connections going, I will be able to ask the
//peer if the dir exists for confirmation until then we will make the assumption that it exists.
//if ([command rangeOfString:@"exists"].location != NSNotFound) {
[userInfo setObject:[NSNumber numberWithBool:YES] forKey:ConnectionDirectoryExistsKey];
NSString *path = [[[[self lastCommand] command] substringFromIndex:4] stringByTrimmingCharactersInSet:[NSCharacterSet whitespaceAndNewlineCharacterSet]];
[userInfo setObject:path forKey:ConnectionDirectoryExistsFilenameKey];
[userInfo setObject:path forKey:NSFilePathErrorKey];
[userInfo setObject:localizedDescription forKey:NSLocalizedDescriptionKey];
[userInfo setObject:command forKey:NSLocalizedFailureReasonErrorKey];
error = [NSError errorWithDomain:CKFTPErrorDomain code:code userInfo:userInfo];
break;
}
default:
break;
}
NSString *path = [self scanBetweenQuotes:command];
if (!path || [path length] == 0)
{
path = [[[[self lastCommand] command] substringFromIndex:4] stringByTrimmingCharactersInSet:[NSCharacterSet whitespaceAndNewlineCharacterSet]];
}
[[self client] connectionDidCreateDirectory:path error:error];
[self setState:CKConnectionIdleState];
}
- (void)_receivedCodeInConnectionDeleteDirectoryState:(int)code command:(NSString *)command buffer:(NSString *)buffer
{
NSError *error = nil;
switch (code)
{
case 421:
{
NSString *localizedDescription = LocalizedStringInConnectionKitBundle(@"FTP service not available; Remote server has closed connection", @"FTP service timed out");
NSDictionary *userInfo = [NSDictionary dictionaryWithObjectsAndKeys:
localizedDescription, NSLocalizedDescriptionKey,
command, NSLocalizedFailureReasonErrorKey,
[[[self request] URL] host], ConnectionHostKey, nil];
error = [NSError errorWithDomain:CKFTPErrorDomain code:code userInfo:userInfo];
break;
}
case 550: //Requested action not taken, file not found.
{
NSMutableDictionary *userInfo = [NSMutableDictionary dictionary];
NSString *localizedDescription = [NSString stringWithFormat:@"%@: %@", LocalizedStringInConnectionKitBundle(@"Failed to delete directory", @"couldn't delete the file"), [[self currentDirectory] stringByAppendingPathComponent:[self currentDeletion]]];
[userInfo setObject:[self currentDeletion] forKey:NSFilePathErrorKey];
[userInfo setObject:localizedDescription forKey:NSLocalizedDescriptionKey];
[userInfo setObject:command forKey:NSLocalizedFailureReasonErrorKey];
error = [NSError errorWithDomain:CKFTPErrorDomain code:code userInfo:userInfo];
break;
}
default:
break;
}
// Uses same _fileDeletes queue, hope that's safe to do. (Any chance one could get ahead of another?)
[[self client] connectionDidDeleteDirectory:[_fileDeletes objectAtIndex:0] error:error];
[self dequeueDeletion];
[self setState:CKConnectionIdleState];
}
- (void)_receivedCodeInConnectionRenameFromState:(int)code command:(NSString *)command buffer:(NSString *)buffer
{
NSError *error = nil;
switch (code)
{
case 350: //Requested action pending further information
{
[self setState:CKConnectionRenameToState];
break;
}
case 421:
{
NSString *localizedDescription = LocalizedStringInConnectionKitBundle(@"FTP service not available; Remote server has closed connection", @"FTP service timed out");
NSDictionary *userInfo = [NSDictionary dictionaryWithObjectsAndKeys:
localizedDescription, NSLocalizedDescriptionKey,
command, NSLocalizedFailureReasonErrorKey,
[[[self request] URL] host], ConnectionHostKey, nil];
error = [NSError errorWithDomain:CKFTPErrorDomain code:code userInfo:userInfo];
break;
}
default:
{
NSDictionary *userInfo = [NSDictionary dictionaryWithObjectsAndKeys:LocalizedStringInConnectionKitBundle(@"No such file", @"No such file"), NSLocalizedDescriptionKey, nil];
error = [NSError errorWithDomain:CKFTPErrorDomain code:code userInfo:userInfo];
break;
}
}
//Unlike other methods, we check that error isn't nil here, because we're sending the finished delegate message on error, whereas the "successful" codes send downloadProgressed messages.
if (error)
{
[[self client] connectionDidRename:[_fileRenames objectAtIndex:0] to:[_fileRenames objectAtIndex:1] error:error];
[_fileRenames removeObjectAtIndex:0];
[_fileRenames removeObjectAtIndex:0];
[self setState:CKConnectionIdleState];
}
}
- (void)_receivedCodeInConnectionAwaitingRenameState:(int)code command:(NSString *)command buffer:(NSString *)buffer
{
NSString *fromPath = [_fileRenames objectAtIndex:0];
NSString *toPath = [_fileRenames objectAtIndex:1];
NSError *error = nil;
switch (code)
{
case 421:
{
NSString *localizedDescription = LocalizedStringInConnectionKitBundle(@"FTP service not available; Remote server has closed connection", @"FTP service timed out");
NSDictionary *userInfo = [NSDictionary dictionaryWithObjectsAndKeys:
localizedDescription, NSLocalizedDescriptionKey,
command, NSLocalizedFailureReasonErrorKey,
[[[self request] URL] host], ConnectionHostKey, nil];
error = [NSError errorWithDomain:CKFTPErrorDomain code:code userInfo:userInfo];
break;
}
case 450: //Requested file action not taken. File unavailable. //File in Use
{
NSString *remotePath = [[self currentUpload] remotePath];
NSDictionary *userInfo = [NSDictionary dictionaryWithObjectsAndKeys:
LocalizedStringInConnectionKitBundle(@"File in Use", @"FTP file in use"), NSLocalizedDescriptionKey,
command, NSLocalizedFailureReasonErrorKey,
remotePath, NSFilePathErrorKey, nil];
error = [NSError errorWithDomain:CKFTPErrorDomain code:code userInfo:userInfo];
break;
}
case 550: //Requested action not taken, file not found. //Permission Denied
{
NSDictionary *userInfo = [NSDictionary dictionaryWithObjectsAndKeys:
LocalizedStringInConnectionKitBundle(@"Permission Denied", @"Permission Denied"), NSLocalizedDescriptionKey,
command, NSLocalizedFailureReasonErrorKey,
fromPath, @"fromPath",
toPath, @"toPath", nil];
error = [NSError errorWithDomain:CKFTPErrorDomain code:code userInfo:userInfo];
break;
}
default:
break;
}
[[self client] connectionDidRename:fromPath to:toPath error:error];
[_fileRenames removeObjectAtIndex:0];
[_fileRenames removeObjectAtIndex:0];
[self setState:CKConnectionIdleState];
}
- (void)_receivedCodeInConnectionDeleteFileState:(int)code command:(NSString *)command buffer:(NSString *)buffer
{
NSError *error = nil;
switch (code)
{
case 421:
{
NSString *localizedDescription = LocalizedStringInConnectionKitBundle(@"FTP service not available; Remote server has closed connection", @"FTP service timed out");
NSDictionary *userInfo = [NSDictionary dictionaryWithObjectsAndKeys:
localizedDescription, NSLocalizedDescriptionKey,
command, NSLocalizedFailureReasonErrorKey,
[[[self request] URL] host], ConnectionHostKey, nil];
error = [NSError errorWithDomain:CKFTPErrorDomain code:code userInfo:userInfo];
break;
}
case 450: //Requested file action not taken. File unavailable.
{
NSString *remotePath = [[self currentUpload] remotePath];
NSDictionary *userInfo = [NSDictionary dictionaryWithObjectsAndKeys:
LocalizedStringInConnectionKitBundle(@"File in Use", @"FTP file in use"), NSLocalizedDescriptionKey,
command, NSLocalizedFailureReasonErrorKey,
remotePath, NSFilePathErrorKey, nil];
error = [NSError errorWithDomain:CKFTPErrorDomain code:code userInfo:userInfo];
break;
}
case 550: //Requested action not taken, file not found.
{
NSMutableDictionary *userInfo = [NSMutableDictionary dictionary];
NSString *localizedDescription = [NSString stringWithFormat:@"%@: %@", LocalizedStringInConnectionKitBundle(@"Failed to delete file", @"couldn't delete the file"), [[self currentDirectory] stringByAppendingPathComponent:[self currentDeletion]]];
[userInfo setObject:[self currentDeletion] forKey:NSFilePathErrorKey];
[userInfo setObject:localizedDescription forKey:NSLocalizedDescriptionKey];
[userInfo setObject:command forKey:NSLocalizedFailureReasonErrorKey];
error = [NSError errorWithDomain:CKFTPErrorDomain code:code userInfo:userInfo];
break;
}
default:
break;
}
[[self client] connectionDidDeleteFile:[self currentDeletion] error:error];
[self dequeueDeletion];
[self setState:CKConnectionIdleState];
}
- (void)_receivedCodeInConnectionDownloadingFileState:(int)code command:(NSString *)command buffer:(NSString *)buffer
{
NSError *error = nil;
switch (code)
{
case 150: //File status okay, about to open data connection.
{
CKInternalTransferRecord *download = [self currentDownload];
if (_writeHandle == nil) // we can get setup in the handleDataReceievedEvent: method
{
NSFileManager *fm = [NSFileManager defaultManager];
// check the file offset of the current download to see if we resume the transfer
unsigned long long fileOffset = [download offset];
bool isResume = ( fileOffset > 0 && [fm fileExistsAtPath:[download localPath]] );
if ( !isResume ) {
[fm removeItemAtPath:[download localPath] error:NULL];
[fm createFileAtPath:[download localPath]
contents:nil
attributes:nil];
_transferSent = 0;
}
[self setWriteHandle:[NSFileHandle fileHandleForWritingAtPath:[download localPath]]];
if ( isResume ) {
[_writeHandle seekToEndOfFile];
_transferSent = fileOffset;
}
}
//start to read in the data to kick start it
uint8_t *buf = (uint8_t *)malloc(sizeof(uint8_t) * kStreamChunkSize);
int len = [_dataReceiveStream read:buf maxLength:kStreamChunkSize];
if (len >= 0) {
[_writeHandle writeData:[NSData dataWithBytesNoCopy:buf length:len freeWhenDone:NO]];
_transferSent += len;
[[self client] download:[download remotePath] didReceiveDataOfLength:len];
if ([download delegateRespondsToTransferTransferredData])
{
[[download delegate] transfer:[download userInfo] transferredDataOfLength:len];
}
int percent = 100.0 * (float)_transferSent / ((float)_transferSize * 1.0);
[[self client] download:[download remotePath] didProgressToPercent:[NSNumber numberWithInt:percent]];
if ([download delegateRespondsToTransferProgressedTo])
{
[[download delegate] transfer:[download userInfo] progressedTo:[NSNumber numberWithInt:percent]];
}
_transferLastPercent = percent;
}
free(buf);
break;
}
case 421:
{
NSString *localizedDescription = LocalizedStringInConnectionKitBundle(@"FTP service not available; Remote server has closed connection", @"FTP service timed out");
NSDictionary *userInfo = [NSDictionary dictionaryWithObjectsAndKeys:
localizedDescription, NSLocalizedDescriptionKey,
command, NSLocalizedFailureReasonErrorKey,
[[[self request] URL] host], ConnectionHostKey, nil];
error = [NSError errorWithDomain:CKFTPErrorDomain code:code userInfo:userInfo];
break;
}
case 425: //Couldn't open data connection
{
CKConnectionCommand *last = [self lastCommand];
CKConnectionState lastState = [[[self commandHistory] objectAtIndex:1] sentState];
if (lastState == FTPSettingEPSVState)
{
_ftpFlags.canUseEPSV = NO;
[self closeDataStreams];
[self sendCommand:@"DATA_CON"];
[self pushCommandOnCommandQueue:last];
}
else if (lastState == FTPSettingEPRTState)
{
_ftpFlags.canUseEPRT = NO;
[self closeDataStreams];
[self sendCommand:@"DATA_CON"];
[self pushCommandOnCommandQueue:last];
}
else if (lastState == FTPSettingActiveState)
{
_ftpFlags.canUseActive = NO;
[self closeDataStreams];
[self sendCommand:@"DATA_CON"];
[self pushCommandOnCommandQueue:last];
}
else if (lastState == FTPSettingPassiveState)
{
_ftpFlags.canUsePASV = NO;
[self closeDataStreams];
[self sendCommand:@"DATA_CON"];
[self pushCommandOnCommandQueue:last];
}
break;
}
case 450: //Requested file action not taken. File unavailable.
{
NSString *remotePath = [[self currentUpload] remotePath];
NSDictionary *userInfo = [NSDictionary dictionaryWithObjectsAndKeys:
LocalizedStringInConnectionKitBundle(@"File in Use", @"FTP file in use"), NSLocalizedDescriptionKey,
command, NSLocalizedFailureReasonErrorKey,
remotePath, NSFilePathErrorKey, nil];
error = [NSError errorWithDomain:CKFTPErrorDomain code:code userInfo:userInfo];
break;
}
case 451: //Requested acion aborted, local error in processing
{
NSString *remotePath = [[self currentDownload] remotePath];
NSDictionary *userInfo = [NSDictionary dictionaryWithObjectsAndKeys:
LocalizedStringInConnectionKitBundle(@"Action Aborted. Local Error", @"FTP Abort"), NSLocalizedDescriptionKey,
command, NSLocalizedFailureReasonErrorKey,
remotePath, NSFilePathErrorKey, nil];
error = [NSError errorWithDomain:CKFTPErrorDomain code:code userInfo:userInfo];
break;
}
case 550: //Requested action not taken, file not found.
{
NSMutableDictionary *userInfo = [NSMutableDictionary dictionary];
CKInternalTransferRecord *download = [self currentDownload];
NSString *localizedDescription = [NSString stringWithFormat:LocalizedStringInConnectionKitBundle(@"File %@ does not exist on server", @"FTP file download error"), [download remotePath]];
[userInfo setObject:[download remotePath] forKey:NSFilePathErrorKey];
[userInfo setObject:localizedDescription forKey:NSLocalizedDescriptionKey];
[userInfo setObject:command forKey:NSLocalizedFailureReasonErrorKey];
error = [NSError errorWithDomain:CKFTPErrorDomain code:code userInfo:userInfo];
break;
}
default:
break;
}
if (error)
{
//Unlike other methods, we check that error isn't nil here, because we're sending the finished delegate message on error, whereas the "successful" codes send downloadProgressed messages.
CKInternalTransferRecord *download = [[self currentDownload] retain];
[self dequeueDownload];
[[self client] downloadDidFinish:[download remotePath] error:error];
if ([download delegateRespondsToTransferDidFinish])
[[download delegate] transferDidFinish:[download userInfo] error:error];
[download release];
//At this point the top of the command queue is something associated with this download. Remove it and all of its dependents.
[_queueLock lock];
CKConnectionCommand *nextCommand = ([_commandQueue count] > 0) ? [_commandQueue objectAtIndex:0] : nil;
if (nextCommand)
{
NSEnumerator *enumerator = [[nextCommand dependantCommands] objectEnumerator];
CKConnectionCommand *dependent;
while (dependent = [enumerator nextObject])
{
[_commandQueue removeObject:dependent];
}
[_commandQueue removeObject:nextCommand];
}
[_queueLock unlock];
[self setState:CKConnectionIdleState];
}
}
- (void)_receivedCodeInConnectionUploadingFileState:(int)code command:(NSString *)command buffer:(NSString *)buffer
{
NSError *error = nil;
switch (code)
{
case 110: //Restart marker reply
{
CKInternalTransferRecord *d = [self currentUpload];
NSString *file = [d localPath]; // actual path to file, or destination name if from data
NSString *remoteFile = [d remotePath];
unsigned long long offset = [d offset];
NSData *data = [d data];
unsigned chunkLength = 0;
const uint8_t bytes [kStreamChunkSize];
_transferSent = 0;
_transferCursor = offset;
_transferLastPercent = 0;
if (nil != data) // use data. (Note that data only can be as big as an insigned in, not a long long)
{
[self setReadData:data];
[self setReadHandle:nil]; // make sure we're not also trying to read from file
// Calculate size to transfer is total data size minus offset
_transferSize = [data length] - offset;
chunkLength = MAX([data length] - offset, kStreamChunkSize);
[data getBytes:&bytes range:NSMakeRange(offset, chunkLength)];
}
else // use file
{
[self setReadData:nil]; // make sure we're not also trying to read from data
[self setReadHandle:[NSFileHandle fileHandleForReadingAtPath:file]];
NSAssert((nil != _readHandle), @"_readHandle is nil!");
// Calculate size to transfer is total file size minus offset
_transferSize = [[[[NSFileManager defaultManager] attributesOfItemAtPath:file error:NULL] objectForKey:NSFileSize] longLongValue] - offset;
[_readHandle seekToFileOffset:offset];
NSData *chunk = [_readHandle readDataOfLength:kStreamChunkSize];
[chunk getBytes:&bytes];
chunkLength = [chunk length]; // actual length of bytes read
}
//kick start the transfer
[_dataSendStream write:bytes maxLength:chunkLength];
_transferSent += chunkLength;
_transferCursor += chunkLength;
[[self client] upload:remoteFile didSendDataOfLength:chunkLength];
if ([d delegateRespondsToTransferTransferredData])
{
[[d delegate] transfer:[d userInfo] transferredDataOfLength:chunkLength];
}
int percent = (float)_transferSent / ((float)_transferSize * 1.0);
if (percent > _transferLastPercent)
{
[[self client] upload:remoteFile didProgressToPercent:[NSNumber numberWithInt:percent]]; // send message if we have increased %
if ([d delegateRespondsToTransferProgressedTo])
{
[[d delegate] transfer:[d userInfo] progressedTo:[NSNumber numberWithInt:percent]];
}
}
_transferLastPercent = percent;
break;
}
case 125: // Windows NT servers seem to use 125 unlike the rest of the world...
case 150: // File status okay, about to open data connection.
{
CKInternalTransferRecord *d = [self currentUpload];
NSString *file = [d localPath]; // actual path to file, or destination name if from data
NSString *remoteFile = [d remotePath];
NSData *data = [d data];
if (nil == file && nil == data)
{
NSString *str = [NSString stringWithFormat:@"FTPConnection parseCommand: no file or data. currrentUpload = %p remotePath = %@",
d, remoteFile ];
NSLog(@"%@", str);
//NSAssert(NO, str); // hacky way to throw an exception.
}
else
{
unsigned chunkLength = 0;
const uint8_t *bytes;
_transferLastPercent = 0;
_transferSent = 0;
_transferCursor = 0;
if (nil != data) // use data. (Note that data only can be as big as an insigned in, not a long long)
{
[self setReadData:data];
[self setReadHandle:nil]; // make sure we're not also trying to read from file
_transferSize = [data length];
chunkLength = MIN(_transferSize, kStreamChunkSize);
bytes = (uint8_t *)[data bytes];
}
else // use file
{
[self setReadData:nil]; // make sure we're not also trying to read from data
// The file handle will be nil if the file doesn't exist. Cancel the transfer and report the error
NSFileHandle *fileHandle = [NSFileHandle fileHandleForReadingAtPath:file];
if (fileHandle)
{
[self setReadHandle:fileHandle];
NSData *chunk = [fileHandle readDataOfLength:kStreamChunkSize];
bytes = (uint8_t *)[chunk bytes];
chunkLength = [chunk length]; // actual length of bytes read
NSNumber *size = [[[NSFileManager defaultManager] attributesOfItemAtPath:file error:NULL] objectForKey:NSFileSize];
_transferSize = [size unsignedLongLongValue];
}
else
{
NSString *localizedDescription = [NSString stringWithFormat:LocalizedStringInConnectionKitBundle(@"Local file %@ doesn't exist", @"File doesn't exist"), [file lastPathComponent]];
NSDictionary *userInfo = [NSDictionary dictionaryWithObjectsAndKeys:
localizedDescription, NSLocalizedDescriptionKey,
file, NSFilePathErrorKey,
[[[self request] URL] host], ConnectionHostKey, nil];
error = [NSError errorWithDomain:CKConnectionErrorDomain code:ConnectionErrorUploading userInfo:userInfo];
[self threadedCancelTransfer];
break;
}
}
//kick start the transfer
[_dataSendStream write:bytes maxLength:chunkLength];
_transferSent += chunkLength;
_transferCursor += chunkLength;
if ([d delegateRespondsToTransferTransferredData])
{
[[d delegate] transfer:[d userInfo] transferredDataOfLength:chunkLength];
}
[[self client] upload:remoteFile didSendDataOfLength:chunkLength];
int percent = (float)_transferSent / ((float)_transferSize * 1.0);
if (percent > _transferLastPercent)
{
[[self client] upload:remoteFile didProgressToPercent:[NSNumber numberWithInt:percent]]; // send message if we have increased %
if ([d delegateRespondsToTransferProgressedTo])
{
[[d delegate] transfer:[d userInfo] progressedTo:[NSNumber numberWithInt:percent]];
}
}
_transferLastPercent = percent;
}
break;
}
case 421:
{
NSString *localizedDescription = LocalizedStringInConnectionKitBundle(@"FTP service not available; Remote server has closed connection", @"FTP service timed out");
NSDictionary *userInfo = [NSDictionary dictionaryWithObjectsAndKeys:
localizedDescription, NSLocalizedDescriptionKey,
command, NSLocalizedFailureReasonErrorKey,
[[[self request] URL] host], ConnectionHostKey, nil];
error = [NSError errorWithDomain:CKFTPErrorDomain code:code userInfo:userInfo];
break;
}
case 425: //Couldn't open data connection
{
CKConnectionCommand *last = [self lastCommand];
CKConnectionState lastState = [[[self commandHistory] objectAtIndex:1] sentState];
if (lastState == FTPSettingEPSVState)
{
_ftpFlags.canUseEPSV = NO;
[self closeDataStreams];
[self sendCommand:@"DATA_CON"];
[self pushCommandOnCommandQueue:last];
}
else if (lastState == FTPSettingEPRTState)
{
_ftpFlags.canUseEPRT = NO;
[self closeDataStreams];
[self sendCommand:@"DATA_CON"];
[self pushCommandOnCommandQueue:last];
}
else if (lastState == FTPSettingActiveState)
{
_ftpFlags.canUseActive = NO;
[self closeDataStreams];
[self sendCommand:@"DATA_CON"];
[self pushCommandOnCommandQueue:last];
}
else if (lastState == FTPSettingPassiveState)
{
_ftpFlags.canUsePASV = NO;
[self closeDataStreams];
[self sendCommand:@"DATA_CON"];
[self pushCommandOnCommandQueue:last];
}
break;
}
case 450: //Requested file action not taken. File unavailable.
{
NSString *remotePath = [[self currentUpload] remotePath];
NSDictionary *userInfo = [NSDictionary dictionaryWithObjectsAndKeys:
LocalizedStringInConnectionKitBundle(@"File in Use", @"FTP file in use"), NSLocalizedDescriptionKey,
command, NSLocalizedFailureReasonErrorKey,
remotePath, NSFilePathErrorKey, nil];
error = [NSError errorWithDomain:CKFTPErrorDomain code:code userInfo:userInfo];
break;
}
case 452: //Requested action not taken. Insufficient storage space in system.
{
NSDictionary *userInfo = [NSDictionary dictionaryWithObjectsAndKeys:
LocalizedStringInConnectionKitBundle(@"No Storage Space Available", @"FTP Error"), NSLocalizedDescriptionKey,
command, NSLocalizedFailureReasonErrorKey,
[[self currentUpload] remotePath], NSFilePathErrorKey, nil];
error = [NSError errorWithDomain:CKFTPErrorDomain code:code userInfo:userInfo];
[self sendCommand:@"ABOR"];
break;
}
case 532: //Need account for storing files.
{
NSDictionary *userInfo = [NSDictionary dictionaryWithObjectsAndKeys:
LocalizedStringInConnectionKitBundle(@"You need an Account to Upload Files", @"FTP Error"), NSLocalizedDescriptionKey,
command, NSLocalizedFailureReasonErrorKey,
[[[self request] URL] host], ConnectionHostKey,
[[self currentUpload] remotePath], NSFilePathErrorKey, nil];
error = [NSError errorWithDomain:CKFTPErrorDomain code:code userInfo:userInfo];
break;
}
case 550: //Requested action not taken, file not found.
{
NSMutableDictionary *userInfo = [NSMutableDictionary dictionary];
CKInternalTransferRecord *upload = [self currentUpload];
NSString *localizedDescription = [NSString stringWithFormat:LocalizedStringInConnectionKitBundle(@"You do not have access to write file %@", @"FTP file upload error"), [upload remotePath]];
[userInfo setValue:[upload remotePath] forKey:NSFilePathErrorKey];
[self dequeueUpload];
[userInfo setObject:localizedDescription forKey:NSLocalizedDescriptionKey];
[userInfo setObject:command forKey:NSLocalizedFailureReasonErrorKey];
error = [NSError errorWithDomain:CKFTPErrorDomain code:code userInfo:userInfo];
break;
}
case 552: //Requested file action aborted, storage allocation exceeded.
{
NSDictionary *userInfo = [NSDictionary dictionaryWithObjectsAndKeys:
LocalizedStringInConnectionKitBundle(@"Cannot Upload File. Storage quota on server exceeded", @"FTP upload error"), NSLocalizedDescriptionKey,
command, NSLocalizedFailureReasonErrorKey,
[[self currentUpload] remotePath], NSFilePathErrorKey, nil];
error = [NSError errorWithDomain:CKFTPErrorDomain code:code userInfo:userInfo];
break;
}
default:
break;
}
// If there was an error, report it to the delegate
if (error)
{
CKInternalTransferRecord *upload = [[self currentUpload] retain];
[self dequeueUpload];
[[self client] uploadDidFinish:[upload remotePath] error:error];
if ([upload delegateRespondsToTransferDidFinish])
[[upload delegate] transferDidFinish:[upload userInfo] error:error];
[upload release];
//At this point the top of the command queue is something associated with this upload. Remove it and all of its dependents.
[_queueLock lock];
CKConnectionCommand *nextCommand = ([_commandQueue count] > 0) ? [_commandQueue objectAtIndex:0] : nil;
if (nextCommand)
{
NSEnumerator *e = [[nextCommand dependantCommands] objectEnumerator];
CKConnectionCommand *dependent;
while (dependent = [e nextObject])
{
[_commandQueue removeObject:dependent];
}
[_commandQueue removeObject:nextCommand];
}
[_queueLock unlock];
[self setState:CKConnectionIdleState];
}
}
- (void)_receivedCodeInConnectionSentOffsetState:(int)code command:(NSString *)command buffer:(NSString *)buffer
{
switch (code)
{
case 350: //Requested action pending further information
{
[self setState:CKConnectionIdleState];
break;
}
default:
break;
}
}
- (void)_receivedCodeInConnectionSentFeatureRequestState:(int)code command:(NSString *)command buffer:(NSString *)buffer
{
switch (code)
{
case 211: //System status, or system help ready
{
//parse features
if ([buffer rangeOfString:@"SIZE"].location != NSNotFound)
_ftpFlags.hasSize = YES;
else
_ftpFlags.hasSize = NO;
if ([buffer rangeOfString:@"ADAT"].location != NSNotFound)
_ftpFlags.hasADAT = YES;
else
_ftpFlags.hasADAT = NO;
if ([buffer rangeOfString:@"AUTH"].location != NSNotFound)
_ftpFlags.hasAUTH = YES;
else
_ftpFlags.hasAUTH = NO;
if ([buffer rangeOfString:@"CCC"].location != NSNotFound)
_ftpFlags.hasCCC = YES;
else
_ftpFlags.hasCCC = NO;
if ([buffer rangeOfString:@"CONF"].location != NSNotFound)
_ftpFlags.hasCONF = YES;
else
_ftpFlags.hasCONF = NO;
if ([buffer rangeOfString:@"ENC"].location != NSNotFound)
_ftpFlags.hasENC = YES;
else
_ftpFlags.hasENC = NO;
if ([buffer rangeOfString:@"MIC"].location != NSNotFound)
_ftpFlags.hasMIC = YES;
else
_ftpFlags.hasMIC = NO;
if ([buffer rangeOfString:@"PBSZ"].location != NSNotFound)
_ftpFlags.hasPBSZ = YES;
else
_ftpFlags.hasPBSZ = NO;
if ([buffer rangeOfString:@"PROT"].location != NSNotFound)
_ftpFlags.hasPROT = YES;
else
_ftpFlags.hasPROT = NO;
if ([buffer rangeOfString:@"MDTM"].location != NSNotFound)
_ftpFlags.hasMDTM = YES;
else
_ftpFlags.hasMDTM = NO;
if ([buffer rangeOfString:@"SITE"].location != NSNotFound)
_ftpFlags.hasSITE = YES;
else
_ftpFlags.hasSITE = NO;
if (_ftpFlags.loggedIn == NO) {
[self authenticateConnection];
} else {
[self setState:CKConnectionIdleState];
}
break;
}
case 500: //Syntax error, command unrecognized.
case 501: //Syntax error in parameters or arguments.
case 502: //Command not implemented
case 550: //Operation not permitted
{
if (!_ftpFlags.loggedIn)
{
[self authenticateConnection];
}
else
{
[self setState:CKConnectionIdleState];
}
break;
}
case 530: //User not logged in
{
// the server doesn't support FEAT before login
[self authenticateConnection];
break;
}
default:
break;
}
}
- (void)_receivedCodeInConnectionSentQuitState:(int)code command:(NSString *)command buffer:(NSString *)buffer
{
switch (code)
{
case 221: //Service closing control connection.
{
[super threadedDisconnect];
break;
}
default:
break;
}
}
- (void)_receivedCodeInConnectionSettingPermissionsState:(int)code command:(NSString *)command buffer:(NSString *)buffer
{
NSError *error = nil;
switch (code)
{
case 421:
{
NSString *localizedDescription = LocalizedStringInConnectionKitBundle(@"FTP service not available; Remote server has closed connection", @"FTP service timed out");
NSDictionary *userInfo = [NSDictionary dictionaryWithObjectsAndKeys:
localizedDescription, NSLocalizedDescriptionKey,
command, NSLocalizedFailureReasonErrorKey,
[[[self request] URL] host], ConnectionHostKey, nil];
error = [NSError errorWithDomain:CKFTPErrorDomain code:code userInfo:userInfo];
break;
}
case 450: //Requested file action not taken. File unavailable.
{
NSString *remotePath = [[self currentUpload] remotePath];
NSDictionary *userInfo = [NSDictionary dictionaryWithObjectsAndKeys:
LocalizedStringInConnectionKitBundle(@"File in Use", @"FTP file in use"), NSLocalizedDescriptionKey,
command, NSLocalizedFailureReasonErrorKey,
remotePath, NSFilePathErrorKey, nil];
error = [NSError errorWithDomain:CKFTPErrorDomain code:code userInfo:userInfo];
break;
}
case 550: //Requested action not taken, file not found.
{
NSMutableDictionary *userInfo = [NSMutableDictionary dictionary];
NSString *localizedDescription = [NSString stringWithFormat:LocalizedStringInConnectionKitBundle(@"Failed to set permissions for path %@", @"FTP Upload error"), [self currentPermissionChange]];
[userInfo setObject:[self currentPermissionChange] forKey:NSFilePathErrorKey];
[userInfo setObject:localizedDescription forKey:NSLocalizedDescriptionKey];
[userInfo setObject:command forKey:NSLocalizedFailureReasonErrorKey];
error = [NSError errorWithDomain:CKFTPErrorDomain code:code userInfo:userInfo];
break;
}
case 553: //Requested action not taken. Illegal file name.
{
NSString *localizedDescription = [NSString stringWithFormat:LocalizedStringInConnectionKitBundle(@"Failed to set permissions for path %@", @"FTP Upload error"), [self currentPermissionChange]];
NSDictionary *userInfo = [NSDictionary dictionaryWithObjectsAndKeys:
localizedDescription, NSLocalizedDescriptionKey,
command, NSLocalizedFailureReasonErrorKey,
[self currentPermissionChange], NSFilePathErrorKey, nil];
error = [NSError errorWithDomain:CKFTPErrorDomain code:code userInfo:userInfo];
break;
}
default:
break;
}
[[self client] connectionDidSetPermissionsForFile:[_filePermissions objectAtIndex:0] error:error];
[self dequeuePermissionChange];
[self setState:CKConnectionIdleState];
}
- (void)_receivedCodeInConnectionSentSizeState:(int)code command:(NSString *)command buffer:(NSString *)buffer
{
NSError *error = nil;
switch (code)
{
case 213: //File status
{
CKInternalTransferRecord *download = [self currentDownload];
if ([command rangeOfString:@"("].location != NSNotFound)
{
NSScanner *sizeScanner = [NSScanner scannerWithString:command];
NSCharacterSet *bracketSet = [NSCharacterSet characterSetWithCharactersInString:@"()"];
[sizeScanner scanUpToCharactersFromSet:bracketSet intoString:nil];
if ( [sizeScanner scanLocation] < [command length] )
{
[sizeScanner setScanLocation:[sizeScanner scanLocation] + 1];
sscanf([[command substringFromIndex:[sizeScanner scanLocation]] cStringUsingEncoding:NSUTF8StringEncoding],
"%llu", &_transferSize);
}
}
else
{
// some servers return 213 4937728
NSScanner *sizeScanner = [NSScanner scannerWithString:command];
NSCharacterSet *sp = [NSCharacterSet whitespaceCharacterSet];
[sizeScanner scanUpToCharactersFromSet:sp intoString:nil];
if ( [sizeScanner scanLocation] < [command length] )
{
[sizeScanner setScanLocation:[sizeScanner scanLocation] + 1];
sscanf([[command substringFromIndex:[sizeScanner scanLocation]] cStringUsingEncoding:NSUTF8StringEncoding],
"%llu", &_transferSize);
}
else
{
_transferSize = LONG_MAX;
}
}
if ([[download delegate] isKindOfClass:[CKTransferRecord class]])
{
[(CKTransferRecord *)[download delegate] setSize:_transferSize];
}
_transferSent = 0;
[self setState:CKConnectionIdleState];
break;
}
case 421:
{
NSString *localizedDescription = LocalizedStringInConnectionKitBundle(@"FTP service not available; Remote server has closed connection", @"FTP service timed out");
NSDictionary *userInfo = [NSDictionary dictionaryWithObjectsAndKeys:
localizedDescription, NSLocalizedDescriptionKey,
command, NSLocalizedFailureReasonErrorKey,
[[[self request] URL] host], ConnectionHostKey, nil];
error = [NSError errorWithDomain:CKFTPErrorDomain code:code userInfo:userInfo];
break;
}
case 550: //Requested action not taken, file not found.
{
NSMutableDictionary *userInfo = [NSMutableDictionary dictionary];
CKInternalTransferRecord *download = [self currentDownload];
NSString *localizedDescription = [NSString stringWithFormat:LocalizedStringInConnectionKitBundle(@"File %@ does not exist on server", @"FTP file download error"), [download remotePath]];
[userInfo setObject:[download remotePath] forKey:NSFilePathErrorKey];
[userInfo setObject:localizedDescription forKey:NSLocalizedDescriptionKey];
[userInfo setObject:command forKey:NSLocalizedFailureReasonErrorKey];
error = [NSError errorWithDomain:CKFTPErrorDomain code:code userInfo:userInfo];
break;
}
default:
break;
}
if (error)
{
//Unlike other methods, we check that error isn't nil here, because we're sending the finished delegate message on error, whereas the "successful" codes send downloadProgressed messages.
CKInternalTransferRecord *download = [[self currentDownload] retain];
[self dequeueDownload];
[[self client] downloadDidFinish:[download remotePath] error:error];
if ([download delegateRespondsToTransferDidFinish])
[[download delegate] transferDidFinish:[download userInfo] error:error];
[download release];
//At this point the top of the command queue is something associated with this download. Remove it and all of its dependents.
[_queueLock lock];
CKConnectionCommand *nextCommand = ([_commandQueue count] > 0) ? [_commandQueue objectAtIndex:0] : nil;
if (nextCommand)
{
NSEnumerator *e = [[nextCommand dependantCommands] objectEnumerator];
CKConnectionCommand *dependent;
while (dependent = [e nextObject])
{
[_commandQueue removeObject:dependent];
}
[_commandQueue removeObject:nextCommand];
}
[_queueLock unlock];
[self setState:CKConnectionIdleState];
}
}
- (void)_receivedCodeInConnectionSentDisconnectState:(int)code command:(NSString *)command buffer:(NSString *)buffer
{
switch (code)
{
case 221: //Service closing control connection.
{
[super threadedDisconnect];
break;
}
default:
break;
}
}
- (void)_receivedCodeInFTPSettingPassiveState:(int)code command:(NSString *)command buffer:(NSString *)buffer
{
switch (code)
{
case 227: //Entering Passive Mode
{
int i[6];
int j;
unsigned char n[6];
char *buf = (char *)[command UTF8String];
char *start = strchr(buf,'(');
if ( !start )
start = strchr(buf,'=');
if ( !start ||
( sscanf(start, "(%d,%d,%d,%d,%d,%d)",&i[0], &i[1], &i[2], &i[3], &i[4], &i[5]) != 6 &&
sscanf(start, "=%d,%d,%d,%d,%d,%d", &i[0], &i[1], &i[2], &i[3], &i[4], &i[5]) != 6 ) )
{
_ftpFlags.canUsePASV = NO;
NSString *localizedDescription = LocalizedStringInConnectionKitBundle(@"All data connection modes have been exhausted. Check with the server administrator.", @"FTP no data stream types available");
NSDictionary *userInfo = [NSDictionary dictionaryWithObjectsAndKeys:
localizedDescription, NSLocalizedDescriptionKey,
command, NSLocalizedFailureReasonErrorKey,
[[[self request] URL] host], ConnectionHostKey, nil];
NSError *err = [NSError errorWithDomain:CKFTPErrorDomain code:FTPErrorNoDataModes userInfo:userInfo];
[[self client] connectionDidReceiveError:err];
_state = CKConnectionSentQuitState;
[self sendCommand:@"QUIT"];
}
for (j=0; j<6; j++)
{
n[j] = (unsigned char) (i[j] & 0xff);
}
int port = i[4] << 8 | i[5];
//port = ntohs(i[5] << 8 | i[4]);
NSString *hostString = [NSString stringWithFormat:@"%d.%d.%d.%d", i[0], i[1], i[2], i[3]];
NSHost *host = [CKCacheableHost hostWithAddress:hostString];
[host setValue:[NSArray arrayWithObject:[[[self request] URL] host]] forKey:@"names"]; // KVC hack
[self closeDataStreams];
[self setState:FTPAwaitingDataConnectionToOpen];
[self openDataStreamsToHost:host port:port];
break;
}
case 500: //Syntax error, command unrecognized.
case 501: //Syntax error in parameters or arguments.
case 502: //Command not implemented
{
_ftpFlags.canUsePASV = NO;
[self closeDataStreams];
[self sendCommand:@"DATA_CON"];
break;
}
default:
break;
}
}
- (void)_receivedCodeInFTPSettingEPSVState:(int)code command:(NSString *)command buffer:(NSString *)buffer
{
switch (code)
{
case 229: //Extended Passive Mode Entered
{
//get the port number
int port = 0;
char *cmd = (char *)[command UTF8String];
char *start = strchr(cmd,'|');
if ( !start || sscanf(start, "|||%d|", &port) != 1)
{
_ftpFlags.canUseEPSV = NO;
CKConnectionCommand *cmd = [self nextAvailableDataConnectionType];
_state = [cmd sentState];
[self sendCommand:[cmd command]];
}
NSHost *host = [CKCacheableHost hostWithName:[[[self request] URL] host]];
[host setValue:[NSArray arrayWithObject:[[[self request] URL] host]] forKey:@"names"]; // KVC hack
[self closeDataStreams];
[self setState:FTPAwaitingDataConnectionToOpen];
[self openDataStreamsToHost:host port:port];
break;
}
case 500: //Syntax error, command unrecognized.
case 501: //Syntax error in parameters or arguments.
case 502: //Command not implemented
{
_ftpFlags.canUseEPSV = NO;
[self closeDataStreams];
[self sendCommand:@"DATA_CON"];
break;
}
default:
break;
}
}
- (void)_receivedCodeInFTPSettingActiveState:(int)code command:(NSString *)command buffer:(NSString *)buffer
{
switch (code)
{
case 500: //Syntax error, command unrecognized.
case 501: //Syntax error in parameters or arguments.
case 502: //Command not implemented
{
_ftpFlags.canUseActive = NO;
[self closeDataStreams];
[self sendCommand:@"DATA_CON"];
break;
}
default:
break;
}
}
- (void)_receivedCodeInFTPSettingEPRTState:(int)code command:(NSString *)command buffer:(NSString *)buffer
{
switch (code)
{
case 500: //Syntax error, command unrecognized.
case 501: //Syntax error in parameters or arguments.
case 502: //Command not implemented
{
_ftpFlags.canUseEPRT = NO;
[self sendCommand:@"DATA_CON"];
break;
}
default:
break;
}
}
- (void)_receivedCodeInFTPAwaitingRemoteSystemTypeState:(int)code command:(NSString *)command buffer:(NSString *)buffer
{
switch (code)
{
case 215: //NAME system type
{
if ([[command lowercaseString] rangeOfString:@"windows"].location != NSNotFound)
{
_ftpFlags.isMicrosoft = YES;
[self setState:FTPChangeDirectoryListingStyle];
[self sendCommand:@"SITE DIRSTYLE"];
break;
}
else
{
_ftpFlags.isMicrosoft = NO;
}
break;
}
case 500: //Syntax error, command unrecognized.
case 501: //Syntax error in parameters or arguments.
case 502: //Command not implemented
{
[self setState:CKConnectionIdleState];
break;
}
default:
break;
}
}
- (void)_receivedCodeInFTPChangeDirectoryListingStyleState:(int)code command:(NSString *)command buffer:(NSString *)buffer
{
switch (code)
{
case 500: //Syntax error, command unrecognized.
case 501: //Syntax error in parameters or arguments.
case 502: //Command not implemented
{
[self setState:CKConnectionIdleState];
break;
}
case 550: //Requested action not taken, file not found.
{
[self setState:CKConnectionIdleState];
break;
}
default:
break;
}
}
#pragma mark -
#pragma mark Stream Handling
- (void)threadedDisconnect
{
_state = CKConnectionSentDisconnectState;
[self sendCommand:@"QUIT"];
}
- (void)threadedCancelTransfer
{
[self sendCommand:@"ABOR"];
_isForceDisconnecting = YES;
[self closeDataConnection];
[super threadedCancelTransfer];
}
/*! Stream delegate method. "The delegate receives this message only if the stream object is scheduled on a runloop. The message is sent on the stream object’s thread."
*/
- (void)stream:(NSStream *)aStream handleEvent:(NSStreamEvent)eventCode
{
if (aStream == _dataReceiveStream)
{
[self handleDataReceivedEvent:eventCode];
}
else if (aStream == _dataSendStream)
{
[self handleDataSendStreamEvent:eventCode];
}
else
{
[super stream:aStream handleEvent:eventCode];
}
}
/*! Stream delegate support method.
*/
- (void)processReceivedData:(NSData *)data
{
NSRange newLinePosition;
NSString *str = [[NSString alloc] initWithData:data encoding:NSASCIIStringEncoding];
if (str)
{
[_commandBuffer appendString:str];
}
[str release];
while ((newLinePosition=[_commandBuffer rangeOfString:@"\r\n"]).location != NSNotFound ||
(newLinePosition=[_commandBuffer rangeOfString:@"\n"]).location != NSNotFound)
{
NSString *cmd = [_commandBuffer substringToIndex:newLinePosition.location];
[_commandBuffer deleteCharactersInRange:NSMakeRange(0,newLinePosition.location+newLinePosition.length)]; // delete the parsed part
[self parseCommand:cmd]; // parse first line of the buffer
}
}
- (void)handleDataReceivedEvent:(NSStreamEvent)eventCode
{
switch (eventCode)
{
case NSStreamEventHasBytesAvailable:
{
uint8_t *buf = (uint8_t *)malloc(sizeof (uint8_t) * kStreamChunkSize);
int len = [_dataReceiveStream read:buf maxLength:kStreamChunkSize];
if (len >= 0)
{
NSData *data = [NSData dataWithBytesNoCopy:buf length:len freeWhenDone:NO];
/*
If this is uncommented, it'll cause massive CPU load when we're doing downloads.
From Greg: "if you enable this, you computer will heat your house this winter"
KTLog(CKStreamDomain, KTLogDebug, @"FTPD << %@", [data shortDescription]);
*/
if (GET_STATE == CKConnectionDownloadingFileState)
{
CKInternalTransferRecord *download = [self currentDownload];
NSString *file = [download remotePath];
if (_writeHandle == nil) // data receieved before receiving a 150
{
NSFileManager *fm = [NSFileManager defaultManager];
// check the file offset of the current download to see if we resume the transfer
unsigned long long fileOffset = [download offset];
bool isResume = ( fileOffset > 0 && [fm fileExistsAtPath:[download localPath]] );
if ( !isResume ) {
[fm removeItemAtPath:[download localPath] error:NULL];
[fm createFileAtPath:[download localPath]
contents:nil
attributes:nil];
_transferSent = 0;
}
[self setWriteHandle:[NSFileHandle fileHandleForWritingAtPath:[download localPath]]];
if ( isResume ) {
[_writeHandle seekToEndOfFile];
_transferSent = fileOffset;
}
}
[_writeHandle writeData:data];
_transferSent += len;
//if ([self isAboveNotificationTimeThreshold:[NSDate date]])
{
if (_transferSize > 0)
{
int percent = 100.0 * (float)_transferSent / ((float)_transferSize * 1.0);
if (percent > _transferLastPercent)
{
if ([download delegateRespondsToTransferProgressedTo])
{
[[download delegate] transfer:[download userInfo] progressedTo:[NSNumber numberWithInt:percent]];
}
[[self client] download:file didProgressToPercent:[NSNumber numberWithInt:percent]]; // send message if we have increased %
_transferLastPercent = percent;
}
}
[[self client] download:file didReceiveDataOfLength:len];
if ([download delegateRespondsToTransferTransferredData])
{
[[download delegate] transfer:[download userInfo] transferredDataOfLength:len];
}
}
}
else {
[_dataBuffer appendBytes:buf length:len];
}
}
free(buf);
break;
}
case NSStreamEventOpenCompleted:
{
KTLog(CKTransportDomain, KTLogDebug, @"FTP Data receive stream opened");
[_openStreamsTimeout invalidate];
[_openStreamsTimeout release];
_openStreamsTimeout = nil;
break;
}
case NSStreamEventErrorOccurred:
{
[[self client] appendFormat:@"Receive Stream Error: %@" toTranscript:CKTranscriptSent, [_receiveStream streamError]];
KTLog(CKStreamDomain, KTLogError, @"receive error %@", [_receiveStream streamError]);
KTLog(CKProtocolDomain, KTLogDebug, @"error state received = %@", [self stateName:GET_STATE]);
// we don't want the error to go to the delegate unless we fail on setting the active con
/* Some servers when trying to test PASV can crap out and throw an error */
if (GET_STATE == FTPAwaitingDataConnectionToOpen)
{
CKConnectionCommand *lastCommand = [[self commandHistory] objectAtIndex:0];
CKConnectionState lastState = [lastCommand sentState];
switch (lastState)
{
case FTPSettingEPSVState:
_ftpFlags.canUseEPSV = NO;
break;
case FTPSettingEPRTState:
_ftpFlags.canUseEPRT = NO;
break;
case FTPSettingActiveState:
_ftpFlags.canUseActive = NO;
break;
case FTPSettingPassiveState:
_ftpFlags.canUsePASV = NO;
break;
}
[self closeDataStreams];
[self sendCommand:@"DATA_CON"];
break;
}
if (GET_STATE == FTPSettingEPSVState)
{
_ftpFlags.canUseEPSV = NO;
[self closeDataStreams];
[self sendCommand:@"DATA_CON"];
break;
}
if (GET_STATE == FTPSettingEPRTState)
{
_ftpFlags.canUseEPRT = NO;
[self closeDataStreams];
[self sendCommand:@"DATA_CON"];
break;
}
if (GET_STATE == FTPSettingActiveState)
{
_ftpFlags.canUseActive = NO;
[self closeDataStreams];
[self sendCommand:@"DATA_CON"];
break;
}
if (GET_STATE == FTPSettingPassiveState)
{
_ftpFlags.canUsePASV = NO;
[self closeDataStreams];
[self sendCommand:@"DATA_CON"];
break;
}
// if uploading, skip the transfer
if (GET_STATE == CKConnectionUploadingFileState ||
GET_STATE == CKConnectionDownloadingFileState ||
GET_STATE == FTPAwaitingDataConnectionToOpen ||
GET_STATE == CKConnectionAwaitingDirectoryContentsState)
{
if (GET_STATE == CKConnectionUploadingFileState)
{
CKInternalTransferRecord *upload = [[self currentUpload] retain];
[self dequeueUpload];
[[self client] uploadDidFinish:[upload remotePath] error:[_receiveStream streamError]];
if ([upload delegateRespondsToTransferDidFinish])
[[upload delegate] transferDidFinish:[upload userInfo] error:[_receiveStream streamError]];
[upload release];
//At this point the top of the command queue is something associated with this upload. Remove it and all of its dependents.
[_queueLock lock];
CKConnectionCommand *nextCommand = ([_commandQueue count] > 0) ? [_commandQueue objectAtIndex:0] : nil;
if (nextCommand)
{
NSEnumerator *e = [[nextCommand dependantCommands] objectEnumerator];
CKConnectionCommand *dependent;
while (dependent = [e nextObject])
{
[_commandQueue removeObject:dependent];
}
[_commandQueue removeObject:nextCommand];
}
[_queueLock unlock];
[self setState:CKConnectionIdleState];
}
if (GET_STATE == CKConnectionDownloadingFileState)
{
CKInternalTransferRecord *download = [[self currentDownload] retain];
[self dequeueDownload];
[[self client] downloadDidFinish:[download remotePath] error:[_receiveStream streamError]];
if ([download delegateRespondsToTransferDidFinish])
[[download delegate] transferDidFinish:[download userInfo] error:[_receiveStream streamError]];
[download release];
//At this point the top of the command queue is something associated with this download. Remove it and all of its dependents.
[_queueLock lock];
CKConnectionCommand *nextCommand = ([_commandQueue count] > 0) ? [_commandQueue objectAtIndex:0] : nil;
if (nextCommand)
{
NSEnumerator *e = [[nextCommand dependantCommands] objectEnumerator];
CKConnectionCommand *dependent;
while (dependent = [e nextObject])
{
[_commandQueue removeObject:dependent];
}
[_commandQueue removeObject:nextCommand];
}
[_queueLock unlock];
[self setState:CKConnectionIdleState];
}
//This will most likely occur when there is a misconfig of the server and we cannot open a data connection so we have unroll the command stack
[self closeDataStreams];
NSArray *history = [self commandHistory];
//NSDictionary *conCommand = [history objectAtIndex:1];
// NSLog(@"command history:\n%@", [[self commandHistory] description]);
CKConnectionCommand *lastCommand = [history objectAtIndex:0];
CKConnectionState lastState = [lastCommand sentState];
if (lastState == FTPSettingEPSVState)
{
_ftpFlags.canUseEPSV = NO;
[self sendCommand:@"DATA_CON"];
break;
}
else if (lastState == FTPSettingEPRTState)
{
_ftpFlags.canUseEPRT = NO;
[self sendCommand:@"DATA_CON"];
break;
}
else if (lastState == FTPSettingActiveState)
{
_ftpFlags.canUseActive = NO;
[self sendCommand:@"DATA_CON"];
break;
}
else if (lastState == FTPSettingPassiveState)
{
_ftpFlags.canUseActive = NO;
[self sendCommand:@"DATA_CON"];
}
}
else
{
KTLog(CKStreamDomain, KTLogDebug, @"NSStreamEventErrorOccurred: %@", [_dataReceiveStream streamError]);
}
break;
}
case NSStreamEventEndEncountered:
{
KTLog(CKStreamDomain, KTLogDebug, @"FTP Data receive stream ended");
[self closeDataConnection];
break;
}
case NSStreamEventNone:
{
break;
}
case NSStreamEventHasSpaceAvailable:
{
break;
}
}
}
- (void)handleDataSendStreamEvent:(NSStreamEvent)eventCode
{
switch (eventCode)
{
case NSStreamEventHasBytesAvailable: //should never be called
{
break;
}
case NSStreamEventOpenCompleted:
{
[_openStreamsTimeout invalidate];
[_openStreamsTimeout release];
_openStreamsTimeout = nil;
KTLog(CKTransportDomain, KTLogDebug, @"FTP Data send stream opened");
if (!_ftpFlags.isActiveDataConn)
[self setState:CKConnectionIdleState];
break;
}
case NSStreamEventErrorOccurred:
{
[[self client] appendFormat:@"Send Stream Error: %@" toTranscript:CKTranscriptSent, [_receiveStream streamError]];
KTLog(CKStreamDomain, KTLogDebug, @"send error %@", [_sendStream streamError]);
// we don't want the error to go to the delegate unless we fail on setting the active con
/* Some servers when trying to test PASV can crap out and throw an error */
if (GET_STATE == FTPSettingEPSVState)
{
_ftpFlags.canUseEPSV = NO;
[self closeDataStreams];
[self sendCommand:@"DATA_CON"];
break;
}
if (GET_STATE == FTPSettingEPRTState)
{
_ftpFlags.canUseEPRT = NO;
[self closeDataStreams];
[self sendCommand:@"DATA_CON"];
break;
}
if (GET_STATE == FTPSettingActiveState)
{
_ftpFlags.canUseActive = NO;
[self closeDataStreams];
[self sendCommand:@"DATA_CON"];
break;
}
if (GET_STATE == FTPSettingPassiveState)
{
_ftpFlags.canUsePASV = NO;
[self closeDataStreams];
[self sendCommand:@"DATA_CON"];
break;
}
// if uploading, skip the transfer
if (GET_STATE == CKConnectionUploadingFileState ||
GET_STATE == CKConnectionDownloadingFileState ||
GET_STATE == FTPAwaitingDataConnectionToOpen ||
GET_STATE == CKConnectionAwaitingDirectoryContentsState) {
if (GET_STATE == CKConnectionUploadingFileState)
{
CKInternalTransferRecord *rec = [self currentUpload];
if ([rec delegateRespondsToError])
{
[[rec delegate] transfer:[rec userInfo] receivedError:[_sendStream streamError]];
}
}
if (GET_STATE == CKConnectionDownloadingFileState)
{
CKInternalTransferRecord *rec = [self currentDownload];
if ([rec delegateRespondsToError])
{
[[rec delegate] transfer:[rec userInfo] receivedError:[_sendStream streamError]];
}
}
//This will most likely occur when there is a misconfig of the server and we cannot open a data connection so we have unroll the command stack
[self closeDataStreams];
NSArray *history = [self commandHistory];
//NSDictionary *conCommand = [history objectAtIndex:1];
// NSLog(@"command history:\n%@", [[self commandHistory] description]);
CKConnectionCommand *lastCommand = [history objectAtIndex:0];
CKConnectionState lastState = [lastCommand sentState];
if (lastState == FTPSettingEPSVState)
{
_ftpFlags.canUseEPSV = NO;
[self sendCommand:@"DATA_CON"];
break;
}
else if (lastState == FTPSettingEPRTState)
{
_ftpFlags.canUseEPRT = NO;
[self sendCommand:@"DATA_CON"];
break;
}
else if (lastState == FTPSettingActiveState)
{
_ftpFlags.canUseActive = NO;
[self sendCommand:@"DATA_CON"];
break;
}
else if (lastState == FTPSettingPassiveState)
{
_ftpFlags.canUseActive = NO;
[self sendCommand:@"DATA_CON"];
}
}
else
{
KTLog(CKStreamDomain, KTLogDebug, @"NSStreamEventErrorOccurred: %@", [_dataReceiveStream streamError]);
[[self client] connectionDidReceiveError:[_dataReceiveStream streamError]];
}
break;
}
case NSStreamEventEndEncountered:
{
KTLog(CKStreamDomain, KTLogDebug, @"FTP Data send stream ended");
[self closeDataConnection];
break;
}
case NSStreamEventNone:
{
break;
}
case NSStreamEventHasSpaceAvailable:
{
if (GET_STATE == CKConnectionUploadingFileState)
{
unsigned chunkLength = 0;
const uint8_t *bytes = NULL;
CKInternalTransferRecord *upload = [self currentUpload];
NSString *remoteFile = [upload remotePath];
if (nil != _readHandle) // reading from file handle
{
NSData *chunk = [_readHandle readDataOfLength:kStreamChunkSize];
bytes = (uint8_t *)[chunk bytes];
chunkLength = [chunk length];
}
else if (nil != _readData)
{
chunkLength = MIN([_readData length] - _transferCursor, kStreamChunkSize);
bytes = (uint8_t *)[[_readData subdataWithRange:NSMakeRange(_transferCursor, chunkLength)] bytes];
}
if (0 != _transferSize && (chunkLength > 0 || _transferSent == _transferSize)) //should only send 0 bytes to initiate a connection shutdown. Do nothing if nothing to transfer yet.
{
[_dataSendStream write:bytes maxLength:chunkLength];
_transferSent += chunkLength;
_transferCursor += chunkLength;
//if ([self isAboveNotificationTimeThreshold:[NSDate date]]) {
if ([upload delegateRespondsToTransferTransferredData])
{
[[upload delegate] transfer:[upload userInfo] transferredDataOfLength:chunkLength];
}
[[self client] upload:remoteFile didSendDataOfLength:chunkLength];
//}
int percent = 100.0 * (float)_transferSent / ((float)_transferSize * 1.0);
if (percent > _transferLastPercent)
{
[[self client] upload:remoteFile didProgressToPercent:[NSNumber numberWithInt:percent]]; // send message if we have increased %
if ([upload delegateRespondsToTransferProgressedTo])
{
[[upload delegate] transfer:[upload userInfo] progressedTo:[NSNumber numberWithInt:percent]];
}
}
_transferLastPercent = percent;
}
}
break;
}
default:
{
KTLog(CKStreamDomain, KTLogDebug, @"Composite Event Code! Need to deal with this!");
break;
}
}
}
- (void)closeDataConnection
{
KTLog(CKStreamDomain, KTLogDebug, @"closeDataConnection");
[self closeDataStreams];
// no delegate notifications if we force disconnected the connection
if (_isForceDisconnecting)
{
_isForceDisconnecting = NO;
return;
}
if (GET_STATE == CKConnectionDownloadingFileState)
{
CKInternalTransferRecord *download = [[self currentDownload] retain];
[self dequeueDownload];
[[self client] downloadDidFinish:[download remotePath] error:nil];
if ([download delegateRespondsToTransferDidFinish])
{
[[download delegate] transferDidFinish:[download userInfo] error:nil];
}
[_writeHandle closeFile];
[self setWriteHandle:nil];
if (_ftpFlags.received226)
{
[self setState:CKConnectionIdleState];
}
[download release];
}
else if (GET_STATE == CKConnectionUploadingFileState)
{
CKInternalTransferRecord *upload = [[self currentUpload] retain];
[self dequeueUpload];
[[self client] uploadDidFinish:[upload remotePath] error:nil];
if ([upload delegateRespondsToTransferDidFinish])
{
[[upload delegate] transferDidFinish:[upload userInfo] error:nil];
}
[self setReadData:nil];
[self setReadHandle:nil];
_transferSize = 0;
if (_ftpFlags.received226)
{
[self setState:CKConnectionIdleState];
}
[upload release];
}
else if (GET_STATE == CKConnectionAwaitingDirectoryContentsState)
{
// Parse the directory listing. Code shamelessly cadged from CKCurlFTPConnection
NSMutableArray *contents = [[NSMutableArray alloc] init];
while (YES)
{
CFDictionaryRef parsedDict = NULL;
CFIndex bytesConsumed = CFFTPCreateParsedResourceListing(NULL,
[_dataBuffer bytes], [_dataBuffer length],
&parsedDict);
if (bytesConsumed > 0)
{
// Make sure CFFTPCreateParsedResourceListing was able to properly
// parse the incoming data
if (parsedDict)
{
// Convert from CFFTP's format to ours
NSString *type = NSFileTypeUnknown;
switch ([(NSNumber *)CFDictionaryGetValue(parsedDict, kCFFTPResourceType) integerValue])
{
case DT_CHR:
type = NSFileTypeCharacterSpecial;
break;
case DT_DIR:
type = NSFileTypeDirectory;
break;
case DT_BLK:
type = NSFileTypeBlockSpecial;
break;
case DT_REG:
type = NSFileTypeRegular;
break;
case DT_LNK:
type = NSFileTypeSymbolicLink;
break;
case DT_SOCK:
type = NSFileTypeSocket;
break;
}
NSDictionary *attributes = [[NSDictionary alloc] initWithObjectsAndKeys:
CFDictionaryGetValue(parsedDict, kCFFTPResourceName), cxFilenameKey,
type, NSFileType,
nil];
[contents addObject:attributes];
[attributes release];
CFRelease(parsedDict);
}
[_dataBuffer replaceBytesInRange:NSMakeRange(0, bytesConsumed) withBytes:NULL length:0];
}
else
{
// error or finished! This is ConnectionKit, so just bail out
break;
}
}
KTLog(CKParsingDomain, KTLogDebug, @"Contents of Directory %@:\n%@", _currentPath, [contents shortDescription]);
[self cacheDirectory:_currentPath withContents:contents];
[[self client] connectionDidReceiveContents:contents ofDirectory:_currentPath error:nil];
[contents release];
[_dataBuffer setLength:0];
if (_ftpFlags.received226)
{
[self setState:CKConnectionIdleState];
}
}
}
- (void)closeDataStreams
{
[_dataReceiveStream setDelegate:nil];
[_dataSendStream setDelegate:nil];
[_dataReceiveStream close];
[_dataSendStream close];
[_dataReceiveStream removeFromRunLoop:[NSRunLoop currentRunLoop] forMode:NSDefaultRunLoopMode];
[_dataSendStream removeFromRunLoop:[NSRunLoop currentRunLoop] forMode:NSDefaultRunLoopMode];
[_dataReceiveStream release];
[_dataSendStream release];
_dataReceiveStream = nil;
_dataSendStream = nil;
if (_connectedActive > 0) {
close(_connectedActive);
_connectedActive = -1;
}
//cancel the noop timer
[_noopTimer invalidate];
[_noopTimer release]; _noopTimer = nil;
//_ftpFlags.isActiveDataConn = NO;
}
- (void)openDataStreamsToHost:(NSHost *)aHost port:(int)aPort
{
[NSStream getStreamsToHost:aHost
port:aPort
inputStream:&_dataReceiveStream
outputStream:&_dataSendStream];
[self prepareAndOpenDataStreams];
// send no op commands
NSUserDefaults *ud = [NSUserDefaults standardUserDefaults];
if ([ud objectForKey:@"FTPSendsNoOps"] && [[ud objectForKey:@"FTPSendsNoOps"] boolValue])
{
_noopTimer = [[NSTimer scheduledTimerWithTimeInterval:60
target:self
selector:@selector(sendNoOp:)
userInfo:nil
repeats:YES] retain];
}
}
- (void)prepareAndOpenDataStreams
{
[_dataReceiveStream retain];
[_dataSendStream retain];
[_dataReceiveStream setDelegate:self];
[_dataSendStream setDelegate:self];
[_dataReceiveStream scheduleInRunLoop:[NSRunLoop currentRunLoop] forMode:NSDefaultRunLoopMode];
[_dataSendStream scheduleInRunLoop:[NSRunLoop currentRunLoop] forMode:NSDefaultRunLoopMode];
[_dataReceiveStream open];
[_dataSendStream open];
unsigned dataConnectionTimeout = 10;
NSNumber *defaultsValue = [[NSUserDefaults standardUserDefaults] objectForKey:@"CKFTPDataConnectionTimeoutValue"];
if (defaultsValue) {
dataConnectionTimeout = [defaultsValue unsignedIntValue];
}
KTLog(CKTransportDomain, KTLogDebug, @"Setting data connection timeout to %u seconds", dataConnectionTimeout);
[_openStreamsTimeout invalidate];
_openStreamsTimeout = [[NSTimer scheduledTimerWithTimeInterval:dataConnectionTimeout
target:self
selector:@selector(dataConnectionOpenTimedOut:)
userInfo:nil
repeats:NO] retain];
}
- (void)dataConnectionOpenTimedOut:(NSTimer *)timer
{
//do something
KTLog(CKTransportDomain, KTLogError, @"Timed out opening data connection");
[[self client] appendString:LocalizedStringInConnectionKitBundle(@"Data Stream Timed Out", @"Failed to open a data stream connection")
toTranscript:CKTranscriptData];
[timer invalidate];
[_openStreamsTimeout release];
_openStreamsTimeout = nil;
[self closeDataStreams];
CKConnectionCommand *last = [self lastCommand];
CKConnectionState lastState = [last sentState];
if (lastState == FTPSettingEPSVState)
{
_ftpFlags.canUseEPSV = NO;
[self closeDataStreams];
[self sendCommand:@"DATA_CON"];
}
else if (lastState == FTPSettingEPRTState)
{
_ftpFlags.canUseEPRT = NO;
[self closeDataStreams];
[self sendCommand:@"DATA_CON"];
}
else if (lastState == FTPSettingActiveState)
{
_ftpFlags.canUseActive = NO;
[self closeDataStreams];
[self sendCommand:@"DATA_CON"];
}
else if (lastState == FTPSettingPassiveState)
{
_ftpFlags.canUsePASV = NO;
[self closeDataStreams];
[self sendCommand:@"DATA_CON"];
}
}
- (void)closeStreams
{
_ftpFlags.setBinaryTransferMode = NO;
[super closeStreams];
}
#pragma mark -
#pragma mark Operations
- (void)changeToDirectory:(NSString *)dirPath
{
NSAssert(dirPath && ![dirPath isEqualToString:@""], @"dirPath is nil!");
CKConnectionCommand *pwd = [CKConnectionCommand command:@"PWD"
awaitState:CKConnectionIdleState
sentState:CKConnectionAwaitingCurrentDirectoryState
dependant:nil
userInfo:nil];
CKConnectionCommand *cwd = [CKConnectionCommand command:[NSString stringWithFormat:@"CWD %@", dirPath]
awaitState:CKConnectionIdleState
sentState:CKConnectionChangingDirectoryState
dependant:pwd
userInfo:nil];
[self queueCommand:cwd];
[self queueCommand:pwd];
}
- (NSString *)currentDirectory
{
return _currentPath;
}
- (NSString *)rootDirectory
{
return _rootPath;
}
- (void)createDirectory:(NSString *)dirPath
{
NSAssert(dirPath && ![dirPath isEqualToString:@""], @"no directory specified");
CKConnectionCommand *cmd = [CKConnectionCommand command:[NSString stringWithFormat:@"MKD %@", dirPath]
awaitState:CKConnectionIdleState
sentState:CKConnectionCreateDirectoryState
dependant:nil
userInfo:nil];
[self queueCommand:cmd];
}
- (void)threadedSetPermissions:(NSNumber *)perms forFile:(NSString *)path
{
unsigned long permissions = [perms unsignedLongValue];
NSString *cmd = [NSString stringWithFormat:@"SITE CHMOD %lo %@", permissions, path];
CKConnectionCommand *com = [CKConnectionCommand command:cmd
awaitState:CKConnectionIdleState
sentState:CKConnectionSettingPermissionsState
dependant:nil
userInfo:nil];
[self pushCommandOnHistoryQueue:com];
[self sendCommand:cmd];
// Not all servers return SITE in the FEAT request.
/*if (_ftpFlags.hasSITE)
{
unsigned long permissions = [perms unsignedLongValue];
NSString *cmd = [NSString stringWithFormat:@"SITE CHMOD %lo %@", permissions, path];
ConnectionCommand *com = [ConnectionCommand command:cmd
awaitState:CKConnectionIdleState
sentState:ConnectionSettingPermissionsState
dependant:nil
userInfo:nil];
[self pushCommandOnHistoryQueue:com];
[self sendCommand:cmd];
}
else
{
// do we send an error or silenty fail????
[self dequeuePermissionChange];
[self setState:CKConnectionIdleState];
}*/
}
- (void)createDirectoryAtPath:(NSString *)path posixPermissions:(NSNumber *)permissions;
{
NSAssert(path && ![path isEqualToString:@""], @"no directory specified");
if (!permissions) return [self createDirectory:path];
NSInvocation *inv = [NSInvocation invocationWithSelector:@selector(threadedSetPermissions:forFile:)
target:self
arguments:[NSArray arrayWithObjects: permissions, path, nil]];
CKConnectionCommand *chmod = [CKConnectionCommand command:inv
awaitState:CKConnectionIdleState
sentState:CKConnectionSettingPermissionsState
dependant:nil
userInfo:nil];
CKConnectionCommand *mkdir = [CKConnectionCommand command:[NSString stringWithFormat:@"MKD %@", path]
awaitState:CKConnectionIdleState
sentState:CKConnectionCreateDirectoryState
dependant:chmod
userInfo:nil];
[self queuePermissionChange:path];
[self queueCommand:mkdir];
[self queueCommand:chmod];
}
- (void)setPermissions:(unsigned long)permissions forFile:(NSString *)path
{
NSAssert(path && ![path isEqualToString:@""], @"no file/path specified");
NSInvocation *inv = [NSInvocation invocationWithSelector:@selector(threadedSetPermissions:forFile:)
target:self
arguments:[NSArray arrayWithObjects: [NSNumber numberWithUnsignedLong:permissions], path, nil]];
[self queuePermissionChange:path];
CKConnectionCommand *chmod = [CKConnectionCommand command:inv
awaitState:CKConnectionIdleState
sentState:CKConnectionSettingPermissionsState
dependant:nil
userInfo:path];
[self queueCommand:chmod];
}
- (void)deleteDirectory:(NSString *)dirPath
{
NSAssert(dirPath && ![dirPath isEqualToString:@""], @"dirPath is nil!");
[self queueDeletion:dirPath];
CKConnectionCommand *rm = [CKConnectionCommand command:[NSString stringWithFormat:@"RMD %@", dirPath]
awaitState:CKConnectionIdleState
sentState:CKConnectionDeleteDirectoryState
dependant:nil
userInfo:dirPath];
[self queueCommand:rm];
}
- (void)rename:(NSString *)fromPath to:(NSString *)toPath
{
NSAssert(fromPath && ![fromPath isEqualToString:@""], @"fromPath is nil!");
NSAssert(toPath && ![toPath isEqualToString:@""], @"toPath is nil!");
[self queueRename:fromPath];
[self queueRename:toPath];
CKConnectionCommand *to = [CKConnectionCommand command:[NSString stringWithFormat:@"RNTO %@", toPath]
awaitState:CKConnectionRenameToState
sentState:CKConnectionAwaitingRenameState
dependant:nil
userInfo:toPath];
CKConnectionCommand *from = [CKConnectionCommand command:[NSString stringWithFormat:@"RNFR %@", fromPath]
awaitState:CKConnectionIdleState
sentState:CKConnectionRenameFromState
dependant:to
userInfo:fromPath];
[self queueCommand:from];
[self queueCommand:to];
}
- (void)deleteFile:(NSString *)path
{
NSAssert(path && ![path isEqualToString:@""], @"path is nil!");
[self queueDeletion:path];
CKConnectionCommand *del = [CKConnectionCommand command:[NSString stringWithFormat:@"DELE %@", path]
awaitState:CKConnectionIdleState
sentState:CKConnectionDeleteFileState
dependant:nil
userInfo:nil];
[self queueCommand:del];
}
/*! Upload file to the given directory
*/
- (CKTransferRecord *)uploadFileAtURL:(NSURL *)url toPath:(NSString *)path posixPermissions:(NSNumber *)permissions;
{
if (![url isFileURL]) return [self uploadData:[NSData dataWithContentsOfURL:url] toPath:path posixPermissions:permissions];
NSAssert(url && ![[url path] isEqualToString:@""], @"localPath is nil!");
NSAssert(path && ![path isEqualToString:@""], @"remotePath is nil!");
CKTransferRecord *result = [self uploadFile:[url path]
orData:nil
offset:0
remotePath:path
checkRemoteExistence:NO
delegate:nil];
if (permissions) [self setPermissions:[permissions unsignedLongValue] forFile:path];
return result;
}
- (CKTransferRecord *)uploadData:(NSData *)data toPath:(NSString *)path posixPermissions:(NSNumber *)permissions;
{
NSAssert(data, @"no data"); // data should not be nil, but it shoud be OK to have zero length!
NSAssert(path && ![path isEqualToString:@""], @"remotePath is nil!");
CKTransferRecord *result = [self uploadFile:nil
orData:data
offset:0
remotePath:path
checkRemoteExistence:NO
delegate:nil];
if (permissions) [self setPermissions:[permissions unsignedLongValue] forFile:path];
return result;
}
- (CKTransferRecord *)downloadFile:(NSString *)remotePath
toDirectory:(NSString *)dirPath
overwrite:(BOOL)flag
delegate:(id)delegate
{
NSAssert(remotePath && ![remotePath isEqualToString:@""], @"no remotePath");
NSAssert(dirPath && ![dirPath isEqualToString:@""], @"no dirPath");
NSString *remoteFileName = [remotePath lastPathComponent];
NSString *localPath = [dirPath stringByAppendingPathComponent:remoteFileName];
if (!flag)
{
if ([[NSFileManager defaultManager] fileExistsAtPath:localPath])
{
NSDictionary *userInfo = [NSDictionary dictionaryWithObjectsAndKeys:
LocalizedStringInConnectionKitBundle(@"Local File already exists", @"FTP download error"), NSLocalizedDescriptionKey,
remotePath, NSFilePathErrorKey, nil];
NSError *error = [NSError errorWithDomain:CKFTPErrorDomain code:FTPDownloadFileExists userInfo:userInfo];
[[self client] connectionDidReceiveError:error];
return nil;
}
}
/*
TYPE I
SIZE file
PASV/EPSV/ERPT/PORT
RETR file
TYPE A
*/
[self startBulkCommands];
CKTransferRecord *record = [CKTransferRecord recordWithName:remotePath size:0];
CKInternalTransferRecord *download = [CKInternalTransferRecord recordWithLocal:localPath
data:nil
offset:0
remote:remotePath
delegate:delegate ? delegate : record
userInfo:record];
[record setProperty:remotePath forKey:CKQueueDownloadRemoteFileKey];
[record setProperty:localPath forKey:CKQueueDownloadDestinationFileKey];
[record setProperty:[NSNumber numberWithInt:0] forKey:CKQueueDownloadTransferPercentReceived];
[self queueDownload:download];
CKConnectionCommand *retr = [CKConnectionCommand command:[NSString stringWithFormat:@"RETR %@", remotePath]
awaitState:CKConnectionIdleState
sentState:CKConnectionDownloadingFileState
dependant:nil
userInfo:download];
CKConnectionCommand *dataCmd = [self pushDataConnectionOnCommandQueue];
[dataCmd addDependantCommand: retr];
CKConnectionCommand *size = [CKConnectionCommand command:[NSString stringWithFormat:@"SIZE %@", remotePath]
awaitState:CKConnectionIdleState
sentState:CKConnectionSentSizeState
dependant:dataCmd
userInfo:nil];
if (!_ftpFlags.setBinaryTransferMode) {
CKConnectionCommand *bin = [CKConnectionCommand command:@"TYPE I"
awaitState:CKConnectionIdleState
sentState:FTPModeChangeState
dependant:nil
userInfo:nil];
[self queueCommand:bin];
_ftpFlags.setBinaryTransferMode = YES;
}
if (_ftpFlags.hasSize)
[self queueCommand:size];
[self queueCommand:dataCmd];
[self queueCommand:retr];
[self endBulkCommands];
return record;
}
- (CKTransferRecord *)resumeDownloadFile:(NSString *)remotePath
toDirectory:(NSString *)dirPath
fileOffset:(unsigned long long)offset
delegate:(id)delegate
{
NSAssert(remotePath && ![remotePath isEqualToString:@""], @"no remotePath");
NSAssert(dirPath && ![dirPath isEqualToString:@""], @"no dirPath");
NSNumber *off = [NSNumber numberWithLongLong:offset];
NSString *remoteFileName = [remotePath lastPathComponent];
NSString *localPath = [dirPath stringByAppendingPathComponent:remoteFileName];
/*
TYPE I
SIZE file
PASV/EPSV/ERPT/PORT
RETR file
TYPE A
*/
[self startBulkCommands];
CKTransferRecord *record = [CKTransferRecord recordWithName:remotePath size:0];
CKInternalTransferRecord *download = [CKInternalTransferRecord recordWithLocal:localPath
data:nil
offset:offset
remote:remotePath
delegate:delegate ? delegate : record
userInfo:record];
[record setProperty:remotePath forKey:CKQueueDownloadRemoteFileKey];
[record setProperty:localPath forKey:CKQueueDownloadDestinationFileKey];
[record setProperty:[NSNumber numberWithInt:0] forKey:CKQueueDownloadTransferPercentReceived];
[self queueDownload:download];
CKConnectionCommand *retr = [CKConnectionCommand command:[NSString stringWithFormat:@"RETR %@", remotePath]
awaitState:CKConnectionIdleState
sentState:CKConnectionDownloadingFileState
dependant:nil
userInfo:download];
CKConnectionCommand *rest = [CKConnectionCommand command:[NSString stringWithFormat:@"REST %@", off]
awaitState:CKConnectionIdleState
sentState:CKConnectionSentOffsetState
dependant:retr
userInfo:nil];
CKConnectionCommand *dataCmd = [self pushDataConnectionOnCommandQueue];
[dataCmd addDependantCommand:rest];
CKConnectionCommand *size = [CKConnectionCommand command:[NSString stringWithFormat:@"SIZE %@", remotePath]
awaitState:CKConnectionIdleState
sentState:CKConnectionSentSizeState
dependant:dataCmd
userInfo:nil];
if (!_ftpFlags.setBinaryTransferMode) {
CKConnectionCommand *bin = [CKConnectionCommand command:@"TYPE I"
awaitState:CKConnectionIdleState
sentState:FTPModeChangeState
dependant:nil
userInfo:nil];
[self queueCommand:bin];
_ftpFlags.setBinaryTransferMode = YES;
}
if (_ftpFlags.hasSize)
[self queueCommand:size];
[self queueCommand:dataCmd];
[self queueCommand:rest];
[self queueCommand:retr];
[self endBulkCommands];
return record;
}
- (void)directoryContents
{
CKConnectionCommand *ls = [CKConnectionCommand command:@"LIST -a"
awaitState:CKConnectionIdleState
sentState:CKConnectionAwaitingDirectoryContentsState
dependant:nil
userInfo:nil];
CKConnectionCommand *dataCmd = [self pushDataConnectionOnCommandQueue];
[dataCmd addDependantCommand:ls];
[self queueCommand:dataCmd];
[self queueCommand:ls];
}
- (void)threadedContentsOfDirectory:(NSString *)dirPath
{
NSString *currentDir = [[[self currentDirectory] copy] autorelease];
CKConnectionCommand *pwd2 = [CKConnectionCommand command:@"PWD"
awaitState:CKConnectionIdleState
sentState:CKConnectionAwaitingCurrentDirectoryState
dependant:nil
userInfo:nil];
CKConnectionCommand *cwd2 = [CKConnectionCommand command:[NSString stringWithFormat:@"CWD %@", currentDir]
awaitState:CKConnectionIdleState
sentState:CKConnectionChangingDirectoryState
dependant:pwd2
userInfo:nil];
CKConnectionCommand *dataCmd = [self pushDataConnectionOnCommandQueue];
CKConnectionCommand *ls = [CKConnectionCommand command:@"LIST -a"
awaitState:CKConnectionIdleState
sentState:CKConnectionAwaitingDirectoryContentsState
dependant:dataCmd
userInfo:nil];
CKConnectionCommand *pwd = [CKConnectionCommand command:@"PWD"
awaitState:CKConnectionIdleState
sentState:CKConnectionAwaitingCurrentDirectoryState
dependant:nil
userInfo:nil];
CKConnectionCommand *cwd = [CKConnectionCommand command:[NSString stringWithFormat:@"CWD %@", dirPath]
awaitState:CKConnectionIdleState
sentState:CKConnectionChangingDirectoryState
dependant:pwd
userInfo:nil];
[self startBulkCommands];
[_commandQueue insertObject:pwd2 atIndex:0];
[_commandQueue insertObject:cwd2 atIndex:0];
[_commandQueue insertObject:ls atIndex:0];
[_commandQueue insertObject:dataCmd atIndex:0];
[_commandQueue insertObject:pwd atIndex:0];
[_commandQueue insertObject:cwd atIndex:0];
[self endBulkCommands];
[self setState:CKConnectionIdleState];
}
- (void)contentsOfDirectory:(NSString *)dirPath
{
NSAssert(dirPath && ![dirPath isEqualToString:@""], @"no dirPath");
NSArray *cachedContents = [self cachedContentsWithDirectory:dirPath];
if (cachedContents)
{
[[self client] connectionDidReceiveContents:cachedContents ofDirectory:dirPath error:nil];
if ([[NSUserDefaults standardUserDefaults] boolForKey:@"CKDoesNotRefreshCachedListings"])
{
return;
}
}
NSInvocation *inv = [NSInvocation invocationWithSelector:@selector(threadedContentsOfDirectory:)
target:self
arguments:[NSArray arrayWithObject:dirPath]];
CKConnectionCommand *ls = [CKConnectionCommand command:inv
awaitState:CKConnectionIdleState
sentState:CKConnectionAwaitingDirectoryContentsState
dependant:nil
userInfo:nil];
[self queueCommand:ls];
}
#pragma mark -
#pragma mark Queue Support
- (NSString *)stateName:(int)state
{
switch (state) {
case FTPSettingActiveState: return @"FTPSettingActiveState";
case FTPSettingPassiveState: return @"FTPSettingPassiveState";
case FTPSettingEPSVState: return @"FTPSettingEPSVState";
case FTPModeChangeState: return @"FTPModeChangeState";
case FTPSettingEPRTState: return @"FTPSettingEPRTState";
case FTPAwaitingDataConnectionToOpen: return @"FTPAwaitingDataConnectionToOpen";
case FTPAwaitingRemoteSystemTypeState: return @"FTPAwaitingRemoteSystemTypeState";
case FTPChangeDirectoryListingStyle: return @"FTPChangeDirectoryListingStyle";
case FTPNoOpState: return @"FTPNoOpState";
default: return [super stateName:state];
}
}
- (CKConnectionCommand *)nextAvailableDataConnectionType
{
/*
This property is a string specifying the preferred data connection type. If it's specified, we will keep trying with *that* connection type. This is desired behavior because as is often the case, the second or third (or 10th, if we experience a connection drop) try at setting a given data type will work –– whereas falling back to other (typically unsupported) types will not. Thus, by setting this property, you are *explicitly* asking this connection to connect *only* with that connection type.
If it's not specified, we'll continually fall back to different connection types until we find that works (or fail.)
Supported Types (as strings) are:
(1) "Passive"
(2) "Extended Passive"
(3) "Extended Active"
(4) "Active"
*/
NSString *preferredDataConnectionType = [[self request] FTPDataConnectionType];
NSString *connectionTypeString = nil;
CKConnectionState sendState;
BOOL explicitlySetConnectionType = (preferredDataConnectionType && [preferredDataConnectionType length] > 0);
BOOL prefersPASV = (explicitlySetConnectionType && [preferredDataConnectionType isEqualToString:@"Passive"]);
BOOL prefersEPSV = (explicitlySetConnectionType && [preferredDataConnectionType isEqualToString:@"Extended Passive"]);
BOOL prefersEPRT = (explicitlySetConnectionType && [preferredDataConnectionType isEqualToString:@"Extended Active"]);
BOOL prefersPORT = (explicitlySetConnectionType && [preferredDataConnectionType isEqualToString:@"Active"]);
if (prefersPASV || (!explicitlySetConnectionType && _ftpFlags.canUsePASV))
{
connectionTypeString = @"PASV";
sendState = FTPSettingPassiveState;
}
else if (prefersEPSV || (!explicitlySetConnectionType && _ftpFlags.canUseEPSV))
{
connectionTypeString = @"EPSV";
sendState = FTPSettingEPSVState;
}
else if (prefersEPRT || (!explicitlySetConnectionType && _ftpFlags.canUseEPRT))
{
connectionTypeString = @"EPRT";
sendState = FTPSettingEPRTState;
}
else if (prefersPORT || (!explicitlySetConnectionType && _ftpFlags.canUseActive))
{
connectionTypeString = @"PORT";
sendState = FTPSettingActiveState;
}
else
{
NSString *localizedDescription = LocalizedStringInConnectionKitBundle(@"Exhausted all connection types to server. Please contact server administrator", @"FTP no data streams available");
NSDictionary *userInfo = [NSDictionary dictionaryWithObjectsAndKeys:
localizedDescription, NSLocalizedDescriptionKey,
[[[self request] URL] host], ConnectionHostKey, nil];
NSError *err = [NSError errorWithDomain:CKFTPErrorDomain code:FTPErrorNoDataModes userInfo:userInfo];
[[self client] connectionDidReceiveError:err];
}
CKConnectionCommand *command;
if (connectionTypeString)
{
command = [CKConnectionCommand command:connectionTypeString
awaitState:CKConnectionIdleState
sentState:sendState
dependant:nil
userInfo:nil];
}
else
{
command = [CKConnectionCommand command:@"QUIT"
awaitState:CKConnectionIdleState
sentState:CKConnectionSentQuitState
dependant:nil
userInfo:nil];
}
_ftpFlags.received226 = NO;
return command;
}
- (CKConnectionCommand *)pushDataConnectionOnCommandQueue
{
return [CKConnectionCommand command:@"DATA_CON"
awaitState:CKConnectionIdleState
sentState:FTPDeterminingDataConnectionType
dependant:nil
userInfo:nil];
}
#pragma mark -
#pragma mark General Support
- (void)setDataInputStreamAndOpen:(NSInputStream *)iStream outputStream:(NSOutputStream *)oStream socket:(CFSocketNativeHandle)sock
{
_connectedActive = sock;
_dataSendStream = oStream;
_dataReceiveStream = iStream;
[self prepareAndOpenDataStreams];
}
void dealWithConnectionSocket(CFSocketRef s, CFSocketCallBackType type,
CFDataRef address, const void *data, void *info)
{
CKFTPConnection *con = (CKFTPConnection *)info;
CFSocketNativeHandle connectedFrom = *(CFSocketNativeHandle *)data;
CFReadStreamRef read;
CFWriteStreamRef write;
CFStreamCreatePairWithSocket(kCFAllocatorDefault,connectedFrom,&read,&write);
//cast as NSStreams
NSInputStream *iStream = [(NSInputStream *)read autorelease];
NSOutputStream *oStream = [(NSOutputStream *)write autorelease];
//send the data streams to the ftp connection object
[con setDataInputStreamAndOpen:iStream outputStream:oStream socket:connectedFrom];
//close down the original listening socket
CFSocketInvalidate(s);
CFRelease(s);
}
- (BOOL)setupActiveConnectionWithPort:(unsigned)port
{
CFOptionFlags cbTypes = kCFSocketAcceptCallBack; //once accepted we will tear down the _activeSocket
CFSocketContext ctx = {0, self, NULL, NULL, NULL};
_activeSocket = CFSocketCreate(kCFAllocatorDefault,
PF_INET,
SOCK_STREAM,
IPPROTO_TCP,
cbTypes,
(CFSocketCallBack)&dealWithConnectionSocket,
&ctx);
CFSocketSetSocketFlags(_activeSocket,kCFSocketCloseOnInvalidate);
int on = 1;
setsockopt(CFSocketGetNative(_activeSocket), SOL_SOCKET, SO_REUSEPORT, &on, sizeof(on));
setsockopt(CFSocketGetNative(_activeSocket), SOL_SOCKET, SO_REUSEADDR, &on, sizeof(on));
//add to the runloop
CFRunLoopSourceRef src = CFSocketCreateRunLoopSource(kCFAllocatorDefault,_activeSocket,0);
if (src)
{
CFRunLoopAddSource(CFRunLoopGetCurrent(), src, kCFRunLoopCommonModes);
CFRelease(src);
}
CFSocketError err;
struct sockaddr_in my_addr;
memset(&my_addr, 0, sizeof(my_addr));
my_addr.sin_family = PF_INET;
my_addr.sin_port = htons(port);
my_addr.sin_addr.s_addr = inet_addr([[[NSHost currentHost] ipv4Address] UTF8String]);
bzero(&(my_addr.sin_zero), 8);
CFDataRef addrData = CFDataCreate(kCFAllocatorDefault,(const UInt8 *)&my_addr,sizeof(my_addr));
if (addrData)
{
err = CFSocketSetAddress(_activeSocket,addrData);
CFRelease(addrData);
}
if (err != kCFSocketSuccess) {
KTLog(CKTransportDomain, KTLogError, @"Failed CFSocketSetAddress() to %@:%u", [[NSHost currentHost] ipv4Address], port);
if (_activeSocket)
{
CFSocketInvalidate(_activeSocket);
CFRelease(_activeSocket);
_activeSocket = nil;
}
return NO;
}
return YES;
}
- (NSString *)setupEPRTConnection
{
if (![self setupActiveConnectionWithPort:0])
{
KTLog(CKTransportDomain, KTLogError, @"Failed to setup EPRT socket, trying PORT");
//try doing a port command
_ftpFlags.canUseEPRT = NO;
_state = FTPSettingActiveState;
return [self setupActiveConnection];
}
CFDataRef addrData = CFSocketCopyAddress(_activeSocket);
struct sockaddr_in active_addr;
CFDataGetBytes(addrData,CFRangeMake(0,CFDataGetLength(addrData)),(UInt8 *)&active_addr);
if (addrData) CFRelease(addrData);
unsigned port = ntohs(active_addr.sin_port); //Do I need to convert from network byte order? YES
return [NSString stringWithFormat:@"EPRT |1|%@|%u|", [[NSHost currentHost] ipv4Address], port];
}
- (NSString *)setupActiveConnection
{
if (_lastActivePort == 0)
{
_lastActivePort = [self localPort] + 1;
}
else
{
_lastActivePort += 1;
}
if (![self setupActiveConnectionWithPort:_lastActivePort])
{
KTLog(CKTransportDomain, KTLogError, @"Failed to setup PORT socket, trying PASV");
_state = FTPSettingPassiveState;
return @"PASV";
}
div_t portDiv = div(_lastActivePort, 256);
NSString *ip = [[[[NSHost currentHost] ipv4Address] componentsSeparatedByString:@"."] componentsJoinedByString:@","];
return [NSString stringWithFormat:@"PORT %@,%d,%d", ip, portDiv.quot, portDiv.rem];
}
/*! Deal with quoted string. Quotes are doubled.... 257 "/he said ""yo"" to me" created
*/
- (NSString *)scanBetweenQuotes:(NSString *)aString
{
NSRange r1 = [aString rangeOfString:@"\""];
NSRange r2 = [aString rangeOfString:@"\"" options:NSBackwardsSearch];
if (NSNotFound == r1.location || NSNotFound == r2.location || r1.location == r2.location)
{
return nil; // can't find quotes
}
NSString *betweenQuotes = [aString substringWithRange:NSMakeRange(r1.location + 1, r2.location - (r1.location +1))];
NSMutableString *result = [NSMutableString stringWithString:betweenQuotes];
[result replaceOccurrencesOfString:@"\"\"" withString:@"\"" options:NSLiteralSearch range:NSMakeRange(0, [result length])];
return result;
}
/*! Support upload method, handles all the gory details
*/
- (CKTransferRecord *)uploadFile:(NSString *)localPath
orData:(NSData *)data
offset:(unsigned long long)offset
remotePath:(NSString *)remotePath
checkRemoteExistence:(BOOL)flag
delegate:(id)delegate
{
if (nil == localPath)
{
localPath = [remotePath lastPathComponent];
}
if (nil == remotePath)
{
remotePath = [[self currentDirectory] stringByAppendingPathComponent:[localPath lastPathComponent]];
}
KTLog(CKQueueDomain, KTLogDebug, @"Queueing Upload: localPath = %@ data = %d bytes offset = %lld remotePath = %@", localPath, [data length], offset, remotePath);
CKConnectionCommand *store = [CKConnectionCommand command:[NSString stringWithFormat:@"STOR %@", remotePath]
awaitState:CKConnectionIdleState
sentState:CKConnectionUploadingFileState
dependant:nil
userInfo:nil];
CKConnectionCommand *rest = nil;
if (offset != 0) {
rest = [CKConnectionCommand command:[NSString stringWithFormat:@"REST %qu", offset]
awaitState:CKConnectionIdleState
sentState:CKConnectionSentOffsetState
dependant:store
userInfo:nil];
}
unsigned long long uploadSize = 0;
if (data)
{
uploadSize = [data length];
}
else
{
NSDictionary *attribs = [[NSFileManager defaultManager] attributesOfItemAtPath:localPath error:NULL];
uploadSize = [[attribs objectForKey:NSFileSize] unsignedLongLongValue];
}
CKTransferRecord *record = [CKTransferRecord recordWithName:remotePath
size:uploadSize];
[record setUpload:YES];
[record setObject:localPath forKey:CKQueueUploadLocalFileKey];
[record setObject:remotePath forKey:CKQueueUploadRemoteFileKey];
CKInternalTransferRecord *dict = [CKInternalTransferRecord recordWithLocal:localPath
data:data
offset:offset
remote:remotePath
delegate:delegate ? delegate : record
userInfo:record];
[self queueUpload:dict];
[store setUserInfo:dict];
CKConnectionCommand *dataCmd = [self pushDataConnectionOnCommandQueue];
[dataCmd addDependantCommand:offset != 0 ? rest : store];
[dataCmd setUserInfo:dict];
[self startBulkCommands];
if (!_ftpFlags.setBinaryTransferMode) {
CKConnectionCommand *bin = [CKConnectionCommand command:@"TYPE I"
awaitState:CKConnectionIdleState
sentState:FTPModeChangeState
dependant:nil
userInfo:nil];
[self queueCommand:bin];
_ftpFlags.setBinaryTransferMode = YES;
}
[self queueCommand:dataCmd];
if (0 != offset)
{
[self queueCommand:rest];
}
[self queueCommand:store];
[self endBulkCommands];
return record;
}
/*! when transferring large files over a fast connection, like the same machine, the delegate notification
* system slows the transfer down too much because the runloopforwarder has to pause the worker thread until
* the ui has updated. This method limits the amount of notifcations sent to the delegate
*/
- (BOOL)isAboveNotificationTimeThreshold:(NSDate *)date
{
BOOL ret = YES;
if (_lastNotified == nil) _lastNotified = [date copy];
double diff = [date timeIntervalSinceReferenceDate] - [_lastNotified timeIntervalSinceReferenceDate];
if (diff > kDelegateNotificationTheshold) {
ret = YES;
[_lastNotified autorelease];
_lastNotified = [date retain];
} else {
ret = NO;
}
return ret;
}
- (void)sendNoOp:(NSTimer *)timer
{
[self sendCommand:@"NOOP"];
}
#pragma mark -
#pragma mark Accessors
- (NSFileHandle *)writeHandle
{
return _writeHandle;
}
- (void)setWriteHandle:(NSFileHandle *)aWriteHandle
{
[_writeHandle closeFile];
[aWriteHandle retain];
[_writeHandle release];
_writeHandle = aWriteHandle;
}
- (NSFileHandle *)readHandle
{
return _readHandle;
}
- (void)setReadHandle:(NSFileHandle *)aReadHandle
{
[_readHandle closeFile];
[aReadHandle retain];
[_readHandle release];
_readHandle = aReadHandle;
}
- (NSData *)readData
{
return _readData;
}
- (void)setReadData:(NSData *)aReadData
{
[aReadData retain];
[_readData release];
_readData = aReadData;
}
- (NSString *)currentPath // same as external function currentDirectory
{
return _currentPath;
}
- (void)setCurrentPath:(NSString *)aCurrentPath
{
if (_currentPath != aCurrentPath) {
[_currentPath release];
_currentPath = [aCurrentPath copy];
}
}
@end
#pragma mark -
@implementation CKFTPConnection (Authentication)
- (NSURLCredential *)currentAuthenticationCredential { return _currentAuthenticationCredential; }
- (void)setCurrentAuthenticationCredential:(NSURLCredential *)credential
{
credential = [credential copy];
[_currentAuthenticationCredential release]; // Should generally be nil already
_currentAuthenticationCredential = credential;
}
/* Uses the delegate to authenticate the connection. If the delegate (heaven forbid) doesn't
* implement authentication, we will fall back to annonymous login if possible.
*/
- (void)authenticateConnection
{
// Cancel old credentials
[self setCurrentAuthenticationCredential:nil];
// Create authentication challenge object and store it as the last authentication attempt
NSInteger previousFailureCount = 0;
if (_lastAuthenticationChallenge)
{
previousFailureCount = [_lastAuthenticationChallenge previousFailureCount] + 1;
}
[_lastAuthenticationChallenge release];
NSURLProtectionSpace *protectionSpace = [[CKURLProtectionSpace alloc] initWithHost:[[[self request] URL] host]
port:[self port]
protocol:[[[self request] URL] scheme]
realm:nil
authenticationMethod:NSURLAuthenticationMethodDefault];
_lastAuthenticationChallenge = [[NSURLAuthenticationChallenge alloc]
initWithProtectionSpace:protectionSpace
proposedCredential:nil
previousFailureCount:previousFailureCount
failureResponse:nil
error:nil
sender:self];
[protectionSpace release];
// As the delegate to handle the challenge
[[self client] connectionDidReceiveAuthenticationChallenge:_lastAuthenticationChallenge];
}
/* FTP's horrible design requires us to send the username or account name and then wait for a response
* before sending the password. This method performs the second half of the operation and sends the
* password that our delegate previously supplied.
*/
- (void)sendPassword
{
NSString *password = [[[self currentAuthenticationCredential] password] copy];
NSAssert(password, @"Somehow a password-less credential has entered the FTP system");
[self sendCommand:[NSString stringWithFormat:@"PASS %@", password]];
[self setState:CKConnectionSentPasswordState];
[password release];
}
- (void)cancelAuthenticationChallenge:(NSURLAuthenticationChallenge *)challenge
{
NSParameterAssert(challenge == _lastAuthenticationChallenge);
NSDictionary *userInfo = [NSDictionary dictionaryWithObject:LocalizedStringInConnectionKitBundle(@"Not Logged In", @"FTP Error")
forKey:NSLocalizedDescriptionKey];
[[self client] connectionDidReceiveError:[NSError errorWithDomain:NSURLErrorDomain
code:NSURLErrorUserCancelledAuthentication
userInfo:userInfo]];
[self forceDisconnect];
}
- (void)continueWithoutCredentialForAuthenticationChallenge:(NSURLAuthenticationChallenge *)challenge
{
// TODO: Attempt to continue downloading, login anonymously, or disconnect with an error?
}
/* Start login
*/
- (void)useCredential:(NSURLCredential *)credential forAuthenticationChallenge:(NSURLAuthenticationChallenge *)challenge
{
NSParameterAssert(credential);
NSParameterAssert([[credential user] length] > 0);
if (challenge == _lastAuthenticationChallenge)
{
// Store the credentials ready for the password request
[self setCurrentAuthenticationCredential:credential];
if ([credential password])
{
// Send the username
[self setState:CKConnectionSentUsernameState];
[self sendCommand:[NSString stringWithFormat:@"USER %@", [credential user]]];
}
else
{
// TODO: Fail with error
}
}
}
@end
#pragma mark -
@implementation NSURLRequest (CKFTPConnection)
- (NSString *)FTPDataConnectionType { return [NSURLProtocol propertyForKey:@"CKFTPDataConnectionType" inRequest:self]; }
@end
@implementation NSMutableURLRequest (CKFTPConnection)
- (void)setFTPDataConnectionType:(NSString *)type
{
if (type)
{
[NSURLProtocol setProperty:type forKey:@"CKFTPDataConnectionType" inRequest:self];
}
else
{
[NSURLProtocol removePropertyForKey:@"CKFTPDataConnectionType" inRequest:self];
}
}
@end
Jump to Line