/* Project: FTP Copyright (C) 2005 Riccardo Mottola Author: Riccardo Mottola Created: 2005-03-30 FTP client class This library is free software; you can redistribute it and/or modify it under the terms of the GNU Lesser General Public License as published by the Free Software Foundation; either version 2.1 of the License, or (at your option) any later version. This library is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more details. You should have received a copy of the GNU Lesser General Public License along with this library; if not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ /* * this class handles acts as a remote client with the FTP server. * the connection modes, default, active (port) and passive * can be set using the three setPort* methods */ #import "ftpclient.h" #import "AppController.h" #import "fileElement.h" #include /* for inet_ntoa and similar */ #include #include #include #include #include #include #define MAX_CONTROL_BUFF 2048 #define MAX_DATA_BUFF 2048 #if defined(__linux__) #define socklentype socklen_t #else #define socklentype int #endif @implementation ftpclient /* initializer */ /* we set possibly unused stuff to NULL */ - (id)init { if (!(self =[super init])) return nil; controller = nil; return self; } - (id)initWithController:(id)cont :(connectionModes)cMode { if (!(self =[super init])) return nil; controller = cont; switch (cMode) { case defaultMode: [self setPortDefault]; break; case portMode: [self setPortPort]; break; case passiveMode: [self setPortPassive]; break; default: [self setPortDefault]; } return self; } /* three methods to set the connection handling */ - (void)setPortDefault { usesPassive = NO; usesPorts = NO; } - (void)setPortPort { usesPassive = NO; usesPorts = YES; } - (void)setPortPassive { usesPassive = YES; usesPorts = NO; } /* changes the current working directory this directory is implicit in many other actions */ - (void)changeWorkingDir:(NSString *)dir { char tempStr[MAX_CONTROL_BUFF]; char tempStr2[MAX_CONTROL_BUFF]; NSMutableArray *reply; [dir getCString:tempStr2]; sprintf(tempStr, "CWD %s\r\n", tempStr2); [self writeLine:tempStr]; if ([self readReply:&reply] == 250) [super changeWorkingDir:dir]; else NSLog(@"cwd failed"); } /* if we have a valid controller, we suppose it respons to appendTextToLog */ /* RM: is there a better way to append a newline? */ - (void)logIt:(NSString *)str { NSMutableString *tempStr; if (controller == NULL) return; tempStr = [NSMutableString stringWithCapacity:([str length] + 1)]; [tempStr appendString:str]; [tempStr appendString:@"\n"]; [controller appendTextToLog:tempStr]; } /* read the reply of a command, be it single or multi-line returned is the first numerical code NOTE: the parser is NOT robust in handling errors */ - (int)readReply :(NSMutableArray **)result { char buff[MAX_CONTROL_BUFF]; int readBytes; int ch; /* the first numerical code, in case of multi-line output it is followed by '-' in the first line and by ' ' in the last line */ char numCodeStr[4]; int numCode; int startNumCode; char separator; enum states { N1, N2, N3, SEPARATOR, CHARS, GOTR, END }; enum states state; BOOL multiline; readBytes = 0; state = N1; separator = 0; multiline = NO; *result = [NSMutableArray arrayWithCapacity:1]; while (!(state == END)) { ch = getc(controlInStream); switch (state) { case N1: buff[readBytes] = ch; numCodeStr[readBytes] = ch; readBytes++; if (ch == ' ') /* skip internal lines of multi-line */ state = CHARS; else state = N2; break; case N2: buff[readBytes] = ch; numCodeStr[readBytes] = ch; readBytes++; state = N3; break; case N3: buff[readBytes] = ch; numCodeStr[readBytes] = ch; readBytes++; state = SEPARATOR; break; case SEPARATOR: buff[readBytes] = ch; numCodeStr[readBytes] = '\0'; readBytes++; numCode = atoi(numCodeStr); separator = ch; state = CHARS; break; case CHARS: if (ch == '\r') state = GOTR; else { buff[readBytes++] = ch; } break; case GOTR: if (ch == '\n') { buff[readBytes] = '\0'; [self logIt:[NSString stringWithCString:buff]]; [*result addObject:[NSString stringWithCString:buff]]; readBytes = 0; if (separator == ' ') { if (multiline) { if (numCode == startNumCode) state = END; } else { startNumCode = numCode; state = END; } } else { startNumCode = numCode; multiline = YES; state = N1; } } break; default: NSLog(@"Duh, a case default in the readReply parser"); } } [*result retain]; return startNumCode; } /* writes a single line to the control connection */ - (int)writeLine:(char *)line { int sentBytes; int bytesToSend; bytesToSend = strlen(line); [self logIt:[NSString stringWithCString:line length:(bytesToSend - 2)]]; if ((sentBytes = send(controlSocket, line, strlen(line), 0)) < bytesToSend) NSLog(@"sent %d out of %d", sentBytes, bytesToSend); return sentBytes; } - (int)setTypeToI { NSMutableArray *reply; [self writeLine:"TYPE I\r\n"]; [self readReply:&reply]; [reply release]; return 0; } - (int)setTypeToA { NSMutableArray *reply; [self writeLine:"TYPE A\r\n"]; [self readReply:&reply]; [reply release]; return 0; } - (void)retrieveFile:(fileElement *)file to:(localclient *)localClient beingAt:(int)depth { NSString *fileName; unsigned long long fileSize; char fNameCStr[MAX_CONTROL_BUFF]; char command[MAX_CONTROL_BUFF]; char buff[MAX_DATA_BUFF]; FILE *localFileStream; int bytesRead; NSMutableArray *reply; struct sockaddr from; int fromLen; int replyCode; unsigned long long totalBytes; NSString *localPath; BOOL gotFile; fromLen = sizeof(from); NSLog(@"filesize should be %u", (unsigned)[file size]); fileName = [file filename]; fileSize = [file size]; localPath = [[localClient workingDir] stringByAppendingPathComponent:fileName]; if ([file isDir]) { NSString *pristineLocalPath; /* original path */ NSString *pristineRemotePath; /* original path */ NSArray *dirList; NSString *remoteDir; NSEnumerator *en; fileElement *fEl; if (depth > 5) { NSLog(@"Max depth reached: %d", depth); return; } pristineLocalPath = [[localClient workingDir] retain]; pristineRemotePath = [[self workingDir] retain]; remoteDir = [[self workingDir] stringByAppendingPathComponent:fileName]; [self changeWorkingDir:remoteDir]; if ([localClient createNewDir:localPath] == YES) { [localClient changeWorkingDir:localPath]; dirList = [self dirContents]; en = [dirList objectEnumerator]; while (fEl = [en nextObject]) { NSLog(@"recurse, download : %@", [fEl filename]); [self retrieveFile:fEl to:localClient beingAt:(depth+1)]; } } /* we get back were we started */ [self changeWorkingDir:pristineRemotePath]; [localClient changeWorkingDir:pristineLocalPath]; [pristineLocalPath release]; [pristineRemotePath release]; return; } /* lets settle to a plain binary standard type */ [self setTypeToI]; if ([self initDataConn] < 0) { NSLog(@"error initiating data connection, retrieveFile"); return; } [fileName getCString:fNameCStr]; sprintf(command, "RETR %s\r\n", fNameCStr); [self writeLine:command]; replyCode = [self readReply:&reply]; NSLog(@"%d reply is %@: ", replyCode, [reply objectAtIndex:0]); if(replyCode != 150) return; /* we have an error or some unexpected condition */ [reply release]; if ([self initDataStream] < 0) return; localFileStream = fopen([localPath cString], "w"); if (localFileStream == NULL) { perror("local fopen failed"); return; } totalBytes = 0; gotFile = NO; [controller setTransferBegin:fileName :fileSize]; while (!gotFile) { bytesRead = read(localSocket, buff, MAX_DATA_BUFF); if (bytesRead == 0) gotFile = YES; else if (bytesRead < 0) { gotFile = YES; NSLog(@"error on socket read, retrieve file"); } else { if (fwrite(buff, sizeof(char), bytesRead, localFileStream) < bytesRead) { NSLog(@"file write error, retrieve file"); } totalBytes += bytesRead; [controller setTransferProgress:totalBytes]; } } [controller setTransferEnd:totalBytes]; NSLog(@"transferred %u", (unsigned long)totalBytes); fclose(localFileStream); [self closeDataStream]; [self readReply:&reply]; [reply release]; } - (void)storeFile:(fileElement *)file from:(localclient *)localClient beingAt:(int)depth { NSString *fileName; unsigned long long fileSize; char fNameCStr[MAX_CONTROL_BUFF]; char command[MAX_CONTROL_BUFF]; char buff[MAX_DATA_BUFF]; FILE *localFileStream; NSMutableArray *reply; int bytesRead; struct sockaddr from; int fromLen; int replyCode; unsigned totalBytes; NSString *localPath; BOOL gotFile; fromLen = sizeof(from); fileName = [file filename]; fileSize = [file size]; localPath = [[localClient workingDir] stringByAppendingPathComponent:fileName]; if ([file isDir]) { NSString *pristineLocalPath; /* original path */ NSString *pristineRemotePath; /* original path */ NSArray *dirList; NSString *remotePath; NSEnumerator *en; fileElement *fEl; if (depth > 3) { NSLog(@"Max depth reached: %d", depth); return; } pristineLocalPath = [[localClient workingDir] retain]; pristineRemotePath = [[self workingDir] retain]; NSLog(@"it is a dir: %@", fileName); remotePath = [pristineRemotePath stringByAppendingPathComponent:fileName]; [localClient changeWorkingDir:localPath]; NSLog(@"local dir changed: %@", [localClient workingDir]); if ([self createNewDir:remotePath] == YES) { NSLog(@"remote dir created succesfully"); [self changeWorkingDir:remotePath]; dirList = [localClient dirContents]; en = [dirList objectEnumerator]; while (fEl = [en nextObject]) { NSLog(@"recurse, upload : %@", [fEl filename]); [self storeFile:fEl from:localClient beingAt:(depth+1)]; } } /* we get back were we started */ [self changeWorkingDir:pristineRemotePath]; [localClient changeWorkingDir:pristineLocalPath]; [pristineLocalPath release]; [pristineRemotePath release]; return; } /* lets settle to a plain binary standard type */ [self setTypeToI]; if ([self initDataConn] < 0) { NSLog(@"error initiating data connection, retrieveFile"); return; } [fileName getCString:fNameCStr]; sprintf(command, "STOR %s\r\n", fNameCStr); [self writeLine:command]; replyCode = [self readReply:&reply]; NSLog(@"%d reply is %@: ", replyCode, [reply objectAtIndex:0]); [reply release]; if ([self initDataStream] < 0) return; localFileStream = fopen([localPath cString], "r"); if (localFileStream == NULL) { perror("local fopen failed"); return; } totalBytes = 0; gotFile = NO; [controller setTransferBegin:fileName :fileSize]; while (!gotFile) { bytesRead = fread(buff, sizeof(char), MAX_DATA_BUFF, localFileStream); if (bytesRead == 0) { gotFile = YES; if (!feof(localFileStream)) NSLog(@"error on file read, store file"); else NSLog(@"feof"); } else { if (write(localSocket, buff, bytesRead) < bytesRead) { NSLog(@"socket write error, store file"); } totalBytes += bytesRead; [controller setTransferProgress:totalBytes]; } } [controller setTransferEnd:totalBytes]; NSLog(@"transferred %u", totalBytes); fclose(localFileStream); [self closeDataStream]; [self readReply:&reply]; [reply release]; } - (void)deleteFile:(fileElement *)file beingAt:(int)depth { NSString *fileName; NSString *localPath; NSFileManager *fm; char command[MAX_CONTROL_BUFF]; NSMutableArray *reply; int replyCode; fm = [NSFileManager defaultManager]; fileName = [file filename]; localPath = [[self workingDir] stringByAppendingPathComponent:fileName]; if ([file isDir]) { NSString *pristineRemotePath; /* original path */ NSArray *dirList; NSString *remotePath; NSEnumerator *en; fileElement *fEl; if (depth > 3) { NSLog(@"Max depth reached: %d", depth); return; } pristineRemotePath = [[self workingDir] retain]; NSLog(@"it is a dir: %@", fileName); remotePath = [pristineRemotePath stringByAppendingPathComponent:fileName]; NSLog(@"remote dir created succesfully"); [self changeWorkingDir:remotePath]; dirList = [self dirContents]; en = [dirList objectEnumerator]; while (fEl = [en nextObject]) { NSLog(@"recurse, delete : %@", [fEl filename]); [self deleteFile:fEl beingAt:(depth+1)]; } /* we get back were we started */ [self changeWorkingDir:pristineRemotePath]; [pristineRemotePath release]; } sprintf(command, "DELE %s\r\n", [fileName cString]); [self writeLine:command]; replyCode = [self readReply:&reply]; NSLog(@"%d reply is %@: ", replyCode, [reply objectAtIndex:0]); [reply release]; } /* initialize a connection */ /* set up and connect the control socket */ - (int)connect:(int)port :(char *)server { struct hostent *hostentPtr; char *tempStr; socklentype addrLen; /* socklen_t on some systems? */ NSMutableArray *reply; NSLog(@"connect to %s : %d", server, port); if((hostentPtr = gethostbyname(server)) == NULL) { NSLog(@"Could not resolve %s", server); return ERR_COULDNT_RESOLVE; } bcopy((char *)hostentPtr->h_addr, (char *)&remoteSockName.sin_addr, hostentPtr->h_length); remoteSockName.sin_family = PF_INET; remoteSockName.sin_port = htons(port); tempStr = inet_ntoa(remoteSockName.sin_addr); if ((controlSocket = socket(PF_INET, SOCK_STREAM, 0)) < 0) { perror("socket failed: "); return ERR_SOCKET_FAIL; } if (connect(controlSocket, (struct sockaddr*) &remoteSockName, sizeof(remoteSockName)) < 0) { perror("connect failed: "); return ERR_CONNECT_FAIL; } /* we retrieve now the local name of the created socked */ /* the local port is for example important as default data port */ addrLen = sizeof(localSockName); if (getsockname(controlSocket, (struct sockaddr *)&localSockName, &addrLen) < 0) { perror("ftpclient: getsockname"); return ERR_GESOCKNAME_FAIL; } controlInStream = fdopen(controlSocket, "r"); [self readReply :&reply]; [reply release]; return 0; } - (void)disconnect { NSMutableArray *reply; [self writeLine:"QUIT\r\n"]; [self readReply:&reply]; } - (int)authenticate:(char *)user :(char *)pass { char tempStr[MAX_CONTROL_BUFF]; NSMutableArray *reply; int replyCode; sprintf(tempStr, "USER %s\r\n", user); [self writeLine:tempStr]; replyCode = [self readReply:&reply]; NSLog(@"user reply is: %@", [reply objectAtIndex:0]); if (replyCode == 530) { NSLog(@"Not logged in: %@", [reply objectAtIndex:0]); [reply release]; [self disconnect]; return -1; } [reply release]; sprintf(tempStr, "PASS %s\r\n", pass); [self writeLine:tempStr]; replyCode = [self readReply:&reply]; NSLog(@"pass reply is: %@", [reply objectAtIndex:0]); if (replyCode == 530) { NSLog(@"Not logged in: %@", [reply objectAtIndex:0]); [reply release]; [self disconnect]; return -1; } [reply release]; /* get home directory as dir we first connected to */ [self writeLine:"PWD\r\n"]; [self readReply:&reply]; if ([reply count] >= 1) { NSString *line; unsigned int length; unsigned int first; unsigned int last; unsigned int i; line = [reply objectAtIndex:0]; NSLog(@"pwd reply is: %@", line); length = [line length]; i = 0; while (i < length && ([line characterAtIndex:i] != '\"')) i++; first = i; if (first < length) { first++; i = length-1; while (i > 0 && ([line characterAtIndex:i] != '\"')) i--; last = i; homeDir = [[line substringWithRange: NSMakeRange(first, last-first)] retain]; NSLog(@"homedir: %@", homeDir); } else homeDir = nil; } return 0; } /* initialize the data connection */ - (int)initDataConn { socklentype addrLen; /* socklen_t on some systems ? */ int socketReuse; socketReuse = YES; /* passive mode */ if (usesPassive) { NSMutableArray *reply; int replyCode; NSScanner *addrScan; int a1, a2, a3, a4; int p1, p2; if ((dataSocket = socket(AF_INET, SOCK_STREAM, 0)) < 0) { perror("socket in initDataConn"); return -1; } [self writeLine:"PASV\r\n"]; replyCode = [self readReply:&reply]; if (replyCode != 227) { NSLog(@"passive mode failed"); return -1; } NSLog(@"pasv reply is: %d %@", replyCode, [reply objectAtIndex:0]); addrScan = [NSScanner scannerWithString:[reply objectAtIndex:0]]; [addrScan setCharactersToBeSkipped:[[NSCharacterSet decimalDigitCharacterSet] invertedSet]]; if ([addrScan scanInt:NULL] == NO) { NSLog(@"error while scanning pasv address"); return -1; } NSLog(@"skipped result code"); if ([addrScan scanInt:&a1] == NO) { NSLog(@"error while scanning pasv address"); return -1; } NSLog(@"got first"); if ([addrScan scanInt:&a2] == NO) { NSLog(@"error while scanning pasv address"); return -1; } NSLog(@"got second"); if ([addrScan scanInt:&a3] == NO) { NSLog(@"error while scanning pasv address"); return -1; } if ([addrScan scanInt:&a4] == NO) { NSLog(@"error while scanning pasv address"); return -1; } if ([addrScan scanInt:&p1] == NO) { NSLog(@"error while scanning pasv port"); return -1; } if ([addrScan scanInt:&p2] == NO) { NSLog(@"error while scanning pasv port"); return -1; } NSLog(@"read: %d %d %d %d : %d %d", a1, a2, a3, a4, p1, p2); dataSockName.sin_family = AF_INET; dataSockName.sin_addr.s_addr = htonl((a1 << 24) | (a2 << 16) | (a3 << 8) | a4); dataSockName.sin_port = htons((p1 << 8) | p2); if (connect(dataSocket, (struct sockaddr *) &dataSockName, sizeof(dataSockName)) < 0) { perror("connect in initDataConn"); return -1; } return 0; } /* active mode, default or PORT arbitrated */ dataSockName = localSockName; /* system picks up a port */ if (usesPorts == YES) dataSockName.sin_port = 0; if ((dataSocket = socket(AF_INET, SOCK_STREAM, 0)) < 0) { perror("socket in initDataConn"); return -1; } /* if we use the default port, we set the option to reuse the port */ /* linux is happier if we set both ends that way */ if (usesPorts == NO) { if (setsockopt(dataSocket, SOL_SOCKET, SO_REUSEADDR, &socketReuse, sizeof (socketReuse)) < 0) { perror("ftpclient: setsockopt (reuse address) on data"); } if (setsockopt(controlSocket, SOL_SOCKET, SO_REUSEADDR, &socketReuse, sizeof (socketReuse)) < 0) { perror("ftpclient: setsockopt (reuse address) on control"); } } if (bind(dataSocket, (struct sockaddr *)&dataSockName, sizeof (dataSockName)) < 0) { perror("ftpclient: bind"); return -1; } if (usesPorts == YES) { addrLen = sizeof (dataSockName); if (getsockname(dataSocket, (struct sockaddr *)&dataSockName, &addrLen) < 0) { perror("ftpclient: getsockname"); return -1; } } if (listen(dataSocket, 1) < 0) { perror("ftpclient: listen"); return -1; } if (usesPorts == YES) { union addrAccess { /* we use this union to extract the 8 bytes of an address */ struct in_addr sinAddr; unsigned char ipv4[4]; } addr; NSMutableArray *reply; char tempStr[256]; unsigned char p1, p2; int returnCode; unsigned int port; addr.sinAddr = dataSockName.sin_addr; port = ntohs(dataSockName.sin_port); p1 = (port & 0xFF00) >> 8; p2 = port & 0x00FF; sprintf(tempStr, "PORT %u,%u,%u,%u,%u,%u\r\n", addr.ipv4[0], addr.ipv4[1], addr.ipv4[2], addr.ipv4[3], p1, p2); [self writeLine:tempStr]; NSLog(@"port str: %s", tempStr); if ((returnCode = [self readReply:&reply]) != 200) { NSLog(@"error occoured in port command: %@", [reply objectAtIndex:0]); return -1; } } return 0; } - (int)initDataStream { struct sockaddr from; socklentype fromLen; fromLen = sizeof(from); if (usesPassive) { dataStream = fdopen(dataSocket, "r"); localSocket = dataSocket; } else { if ((localSocket = accept(dataSocket, &from, &fromLen)) < 0) { perror("accepting socket, initDataStream: "); } dataStream = fdopen(localSocket, "r"); } if (dataStream == NULL) { perror("data stream opening failed"); return -1; } NSLog(@"data stream open"); return 0; } - (int)closeDataConn { close (dataSocket); return 0; } - (void)closeDataStream { fclose (dataStream); // a passive localSocket is just a copy of the dataSocket if (usesPassive == NO) close(localSocket); // apparently it is not true that fclose closes the underlying // descriptor, without closing dataSocket we got a bind error // at the next conneciton attempt [self closeDataConn]; } /* creates a new directory tries to guess if the given dir is relative (no starting /) or absolute Is this portable to non-unix OS's? */ - (BOOL)createNewDir:(NSString *)dir { NSString *remotePath; char command[MAX_CONTROL_BUFF]; char pathCStr[MAX_CONTROL_BUFF]; NSMutableArray *reply; int replyCode; if ([dir hasPrefix:@"/"]) { NSLog(@"%@ is an absolute path", dir); remotePath = dir; } else { NSLog(@"%@ is a relative path", dir); remotePath = [[self workingDir] stringByAppendingPathComponent:dir]; } [remotePath getCString:pathCStr]; sprintf(command, "MKD %s\r\n", pathCStr); [self writeLine:command]; replyCode = [self readReply:&reply]; if (replyCode == 257) return YES; else { NSLog(@"remote mkdir code: %d %@", replyCode, [reply objectAtIndex:0]); return NO; } } /* RM again: a better path limit is needed */ - (NSArray *)dirContents { int ch; char buff[MAX_DATA_BUFF]; int readBytes; enum states_m1 { READ, GOTR }; enum states_m1 state; NSMutableArray *listArr; fileElement *aFile; char path[4096]; NSMutableArray *reply; [workingDir getCString:path]; /* lets settle to a plain ascii standard type */ [self setTypeToA]; /* create an array with a reasonable starting size */ listArr = [NSMutableArray arrayWithCapacity:5]; [self initDataConn]; [self writeLine:"LIST\r\n"]; [self readReply:&reply]; if ([self initDataStream] < 0) return nil; /* read the directory listing, each line being CR-LF terminated */ state = READ; readBytes = 0; while ((ch = getc(dataStream)) != EOF) { if (ch == '\r') state = GOTR; else if (ch == '\n' && state == GOTR) { buff[readBytes] = '\0'; fprintf(stderr, "%s\n", buff); state = READ; /* reset the state for a new line */ readBytes = 0; aFile = [[fileElement alloc] initWithLsLine:buff]; if (aFile) [listArr addObject:aFile]; } else buff[readBytes++] = ch; } if (ferror(dataStream)) { perror("error in reading data stream: "); } else if (feof(dataStream)) { fprintf(stderr, "feof\n"); } [self closeDataStream]; [self readReply:&reply]; return [NSArray arrayWithArray:listArr]; } @end