/* ----------------------------------------------------------------------------- GSFramework Copyright 2001-2013 Emmanuel Julien. All Rights Reserved. ----------------------------------------------------------------------------- */ #include "filesystem/ftp_lib.h" #include "nstring/nstring.h" #include "platform_config.h" #include "alloc/ialloc.h" #include "log/log.h" using namespace GS; #define ConnectionBufferSize 1024 #if __PLATFORM_WINDOWS__ #define SETSOCKOPT_OPTVAL_TYPE (const char *) #define net_read(x,y,z) recv(x,(char*)y,z,0) #define net_write(x,y,z) send(x,(char*)y,z,0) #define net_close closesocket typedef int socklen_t; #elif (__PLATFORM_LINUX__ || __PLATFORM_OSX__) #include #include #include #include #include #define SETSOCKOPT_OPTVAL_TYPE (void *) #define net_read read #define net_write write #define net_close close #define SOCKET int #endif //------------------------------------------------------------------------------ #if (__PLATFORM_WINDOWS__ || __PLATFORM_LINUX__ || __PLATFORM_OSX__) #define FTP_PROPAGATE_ERROR__(__C__) { State s = __C__; if (s != FtpOk) return s; } #include #include #include #include #include #include //---------------------------- FtpConnection::FtpConnection() //---------------------------- { handle = -1; ready = false; buffer = 0; buffer_usage = 0; } //--------------------------------------------- void FtpConnection::FreeBuffer() //--------------------------------------------- { _safe_delete_array(buffer); } //------------------------------------------------- bool FtpConnection::AllocateBuffer() //------------------------------------------------- { FreeBuffer(); return (buffer = new char[ConnectionBufferSize]) != NULL ? true : false; } //------------------------------------------------------------------- FtpLibrary::State FtpLibrary::WaitSocket(FtpConnection *connection) //------------------------------------------------------------------- { fd_set fd, *rfd = NULL, *wfd = NULL; FD_ZERO(&fd); if (connection->direction == FtpConnection::Upload) wfd = &fd; else rfd = &fd; forever { FD_SET(connection->handle, &fd); // 5 seconds timeout. timeval tv; tv.tv_sec = 4; tv.tv_usec = 100000; SOCKET rv = select((int)(connection->handle + 1), rfd, wfd, NULL, &tv); if (rv == -1) return FtpError; else if (!rv) return FtpSocketTimeout; else break; /* if (!IdleCallback()) FtpSocketTimeout; */ } return FtpOk; } //--------------------------------------------------- bool FtpLibrary::CheckResponse(char c) //--------------------------------------------------- { char match[5]; int length; if (ReadASCII(response, 256, &master_connection, length) != FtpOk) return false; if (response[3] == '-') { strncpy(match, response, 3); match[3] = ' '; match[4] = '\0'; do { if (ReadASCII(response, 256, &master_connection, length) != FtpOk) return false; } while (strncmp(response, match, 4)); } return (response[0] == c) ? true : false; } //------------------------------------------------------- bool FtpLibrary::Connect(const char *host) //------------------------------------------------------- { sockaddr_in sin; memset(&sin,0,sizeof(sin)); sin.sin_family = AF_INET; char *lhost = strdup(host), *pnum = strchr(lhost,':'); servent *pse; if (!pnum) { if ((pse = getservbyname("ftp", "tcp")) == NULL) __ERR__(__LOG__ << "[!] (FTP) Failed to get service port.\n", false) sin.sin_port = pse->s_port; } else { *pnum++ = 0; if (isdigit(*pnum)) sin.sin_port = htons((u_short)atoi(pnum)); else { pse = getservbyname(pnum, "tcp"); sin.sin_port = pse->s_port; } } #if __PLATFORM_WINDOWS__ if ((sin.sin_addr.s_addr = inet_addr(lhost)) == -1) #else if (!inet_aton(lhost, &sin.sin_addr)) #endif { hostent *phe; if ((phe = gethostbyname(lhost)) == NULL) __ERR__(__LOG__ << "[!] (FTP) Failed to resolve host.\n", false) memcpy((char *)&sin.sin_addr, phe->h_addr, phe->h_length); } free(lhost); //------------------------------------------------------------------------------------------------------- #define __ConnectError__(_S_)\ { __LOG__ << _S_; net_close(master_connection.handle); master_connection.handle = 0; return false; } //------------------------------------------------------------------------------------------------------- master_connection.handle = (int)socket(PF_INET, SOCK_STREAM, IPPROTO_TCP); if (master_connection.handle == -1) __ConnectError__("[!] (FTP) Failed to create socket.\n") int on = 1; if (setsockopt(master_connection.handle, SOL_SOCKET, SO_REUSEADDR, SETSOCKOPT_OPTVAL_TYPE &on, sizeof(on)) == -1) __ConnectError__("[!] (FTP) Set socket option failed.\n") if (connect(master_connection.handle, (struct sockaddr *)&sin, sizeof(sin)) == -1) __ConnectError__("[!] (FTP) Socket failed to connect.\n") if (!CheckResponse('2')) __ConnectError__("") return true; } //----------------------------------------------------------------- bool FtpLibrary::CheckPASVResponse(unsigned char *v) //----------------------------------------------------------------- { sockaddr sa; socklen_t l = sizeof(sa); if (getpeername(master_connection.handle, &sa, &l) == -1) { net_close(master_connection.handle); return false; } for (int i = 2; i < 6; ++i) v[i] = sa.sa_data[i]; return true; } //--------------------------------------------------------------------------- bool FtpLibrary::FtpSendCmd(const char *command, char expresp) //--------------------------------------------------------------------------- { if (!master_connection.handle) return 0; char ftp_command[256]; _snprintf(ftp_command, 255, "%s\r\n", command); if (net_write(master_connection.handle, ftp_command, (int)strlen(ftp_command)) <= 0) return false; SendCommandCallback(ftp_command); return CheckResponse(expresp); } //--------------------------------------------------------------------------- bool FtpLibrary::Login(const char *user, const char *password) //--------------------------------------------------------------------------- { char ftp_command[64]; // Send user. _snprintf(ftp_command, 63, "USER %s", user); if (!FtpSendCmd(ftp_command, '3')) { if (*GetLastResponse() == '2') return true; return false; } // Send password. _snprintf(ftp_command, 63, "PASS %s", password); return FtpSendCmd(ftp_command, '2'); } //------------------------------------------------------------------------------------------------------------------------------ FtpConnection *FtpLibrary::CreatePORTConnection(TransferMode mode, FtpConnection::Direction dir, char *connection_command) //------------------------------------------------------------------------------------------------------------------------------ { union { sockaddr sa; sockaddr_in in; } sin; // Create the new connection. FtpConnection *connection = new FtpConnection; if (!connection) __ERR__(__LOG__ << "[!] (FTP) Failed to allocate new connection object.\n", NULL) // Get socket from the master connection. socklen_t l = sizeof(sin); if (getsockname(master_connection.handle, &sin.sa, &l) < 0) return connection; // Create socket. connection->handle = (int)socket(PF_INET, SOCK_STREAM, IPPROTO_TCP); if (connection->handle == -1) return connection; // Set socket options. int on = 1; if (setsockopt(connection->handle, SOL_SOCKET, SO_REUSEADDR, SETSOCKOPT_OPTVAL_TYPE &on, sizeof(on)) == -1) return connection; linger lng = { 0, 0 }; if (setsockopt(connection->handle, SOL_SOCKET, SO_LINGER, SETSOCKOPT_OPTVAL_TYPE &lng, sizeof(lng)) == -1) return connection; // Bind socket. sin.in.sin_port = 0; if ( (bind(connection->handle, &sin.sa, sizeof(sin)) == -1) || (listen(connection->handle, 1) < 0) || (getsockname(connection->handle, &sin.sa, &l) < 0) ) return connection; // Open PORT connection. char ftp_command[256]; _snprintf(ftp_command, 255, "PORT %hhu,%hhu,%hhu,%hhu,%hhu,%hhu", (unsigned char)sin.sa.sa_data[2], (unsigned char)sin.sa.sa_data[3], (unsigned char)sin.sa.sa_data[4], (unsigned char)sin.sa.sa_data[5], (unsigned char)sin.sa.sa_data[0], (unsigned char)sin.sa.sa_data[1] ); if (!FtpSendCmd(ftp_command, '2')) return connection; // Handle resuming. if (offset) { _snprintf(ftp_command, 255, "REST %lld", offset); if (!FtpSendCmd(ftp_command, '3')) return connection; // TODO allow for a full restart here? } // Allocate buffer for binary transaction. if ((mode == TransferASCII) && !connection->AllocateBuffer()) return connection; // Finally send the connection command. if (!FtpSendCmd(connection_command, '1')) return connection; // Connection is ready. connection->direction = dir; connection->ready = true; return connection; } //------------------------------------------------------------------------------------------------------------------------------ FtpConnection *FtpLibrary::CreatePASVConnection(TransferMode mode, FtpConnection::Direction dir, char *connection_command) //------------------------------------------------------------------------------------------------------------------------------ { // Set PASV mode. if (!FtpSendCmd("PASV", '2')) return NULL; // Check server answer. char *cp = strchr(response,'('); if (!cp) return NULL; unsigned char v[6]; sscanf(++cp, "%hhu,%hhu,%hhu,%hhu,%hhu,%hhu", &v[2], &v[3], &v[4], &v[5], &v[0], &v[1]); if (correctpasv && !CheckPASVResponse(v)) __ERR__(__LOG__ << "[!] (FTP) Incorrect PASV response.\n", NULL) struct sockaddr sa; sa.sa_family = AF_INET; sa.sa_data[2] = v[2]; sa.sa_data[3] = v[3]; sa.sa_data[4] = v[4]; sa.sa_data[5] = v[5]; sa.sa_data[0] = v[0]; sa.sa_data[1] = v[1]; // Handle resume. char ftp_command[256]; if (offset) { _snprintf(ftp_command, 255, "REST %lld", offset); if (!FtpSendCmd(ftp_command, '3')) return NULL; } // Create the new connection. FtpConnection *connection = new FtpConnection; if (!connection) __ERR__(__LOG__ << "[!] (FTP) Failed to allocate new connection object.\n", NULL) // Create socket. int on = 1; linger lng = { 0, 0 }; connection->handle = (int)socket(PF_INET,SOCK_STREAM,IPPROTO_TCP); if ( (connection->handle == -1) || (setsockopt(connection->handle, SOL_SOCKET, SO_REUSEADDR, SETSOCKOPT_OPTVAL_TYPE &on, sizeof(on)) == -1) || (setsockopt(connection->handle, SOL_SOCKET, SO_LINGER, SETSOCKOPT_OPTVAL_TYPE &lng, sizeof(lng)) == -1) ) return connection; // Setup connection. _snprintf(ftp_command, 255, "%s\r\n", connection_command); if (net_write(master_connection.handle, ftp_command, (int)strlen(ftp_command)) <= 0) return connection; // Connect socket. if ((connect(connection->handle, &sa, sizeof(sa)) == -1) || !CheckResponse('1')) return connection; // Allocate buffer for binary transaction. if ((mode == TransferASCII) && !connection->AllocateBuffer()) return connection; // Connection is ready. connection->direction = dir; connection->ready = true; return connection; } //---------------------------------------------------------------------------- bool FtpLibrary::FtpAcceptConnection(FtpConnection *connection) //---------------------------------------------------------------------------- { // Reset all connections. fd_set mask; FD_ZERO(&mask); FD_SET(master_connection.handle, &mask); FD_SET(connection->handle, &mask); // Setup timeout. timeval tv; tv.tv_usec = 0; tv.tv_sec = 30; // Select handle. int i = (int)master_connection.handle; if (i < connection->handle) i = connection->handle; i = select((int)(i + 1), &mask, NULL, NULL, &tv); switch (i) { case -1: // Error. strncpy(response, strerror(errno), sizeof(response)); break; case 0: // Time out. strcpy(response, "Time out waiting for connection."); break; default: if (FD_ISSET(connection->handle, &mask)) { sockaddr addr; socklen_t l = sizeof(addr); int handle = (int)accept(connection->handle, &addr, &l); i = errno; net_close(connection->handle); if (handle > 0) { connection->handle = handle; return true; } strncpy(response, strerror((int)i), sizeof(response)); connection->handle = 0; return false; } else if (FD_ISSET(connection->handle, &mask)) CheckResponse('2'); break; } net_close(connection->handle); connection->handle = 0; return false; } //------------------------------------------------------------------------------ FtpConnection *FtpLibrary::CreateConnection(const char *path, AccessType type, TransferMode mode) { if (!path && ((type == AccessFileWrite) || (type == AccessFileRead) || (type == AccessFileReadAppend) || (type == AccessFileWriteAppend))) __ERR__(__LOG__ << "[!] (FTP) Missing path argument.\n", NULL) // Setup transfer mode. char ftp_command[256]; _snprintf(ftp_command, 255, "TYPE %c", mode); if (!FtpSendCmd(ftp_command, '2')) __ERR__(__LOG__ << "[!] (FTP) Failed to set tranfer mode.\n", NULL) // Select sub command. const char *sub_command; FtpConnection::Direction direction = FtpConnection::Download; switch (type) { case AccessDir: sub_command = "NLST"; break; case AccessDirVerbose: sub_command = "LIST -aL"; break; case AccessFileReadAppend: case AccessFileRead: sub_command = "RETR"; break; case AccessFileWriteAppend: case AccessFileWrite: sub_command = "STOR"; direction = FtpConnection::Upload; break; default: __ERR__(__LOG__ << "[!] (FTP) Invalid access type.\n", NULL) } // Append path. if (path) _snprintf(ftp_command, 255, "%s %s", sub_command, path); // Open connection. FtpConnection *connection = NULL; switch (connection_mode) { case ConnectionPASV: connection = CreatePASVConnection(mode, direction, ftp_command); break; case ConnectionPORT: connection = CreatePORTConnection(mode, direction, ftp_command); if (!connection || !connection->ready) break; if (!FtpAcceptConnection(connection)) { CloseConnection(connection); return NULL; } break; } return connection; } bool FtpLibrary::CloseConnection(FtpConnection *connection) { // Sanity check. if (connection == &master_connection) return false; // Purge writing cache. int length; if (connection->direction == FtpConnection::Upload) if (connection->buffer) WriteASCII(NULL, 0, connection, length); shutdown(connection->handle, 2); net_close(connection->handle); _safe_delete(connection); return CheckResponse('2'); } //------------------------------------------------------------------------------ //------------------------------------------------------------------------------ FtpLibrary::State FtpLibrary::ReadASCII(char *buf, int max, FtpConnection *connection, int &total_read_count) { /// TODO // Early exit case. if (max == 0) return FtpOk; if ((connection != &master_connection) && (connection->direction != FtpConnection::Download)) return FtpError; // Read. total_read_count = 0; char *p_buffer = buf; forever { // Check available data on cache. if (connection->buffer_usage > 0) { // Determine read size. char *eol = (char *)memchr(connection->buffer, '\n', connection->buffer_usage); int read_size = (int)(eol ? eol - connection->buffer + 1 : connection->buffer_usage); if (read_size > (max - 1)) read_size = max - 1; // Perform reading. memcpy(p_buffer, connection->buffer, read_size); max -= read_size; total_read_count += read_size; p_buffer += read_size; // Update cache. if (read_size < connection->buffer_usage) memcpy(connection->buffer, &connection->buffer[read_size], connection->buffer_usage - read_size); connection->buffer_usage -= read_size; // Catch end of buffer. if ((max == 1) || eol) { p_buffer[0] = 0; break; } } // Wait socket. FTP_PROPAGATE_ERROR__(WaitSocket(connection)) // Fill cache. int buffer_left = (ConnectionBufferSize - connection->buffer_usage) - 1, read_count = net_read(connection->handle, &connection->buffer[connection->buffer_usage], buffer_left); connection->buffer_usage += read_count; if (read_count == -1) __ERR__(__LOG__ << "[!] (FTP) Read error.\n", FtpError) else if (!read_count) break; // EOF } return FtpOk; } FtpLibrary::State FtpLibrary::WriteASCII(char *output, int length, FtpConnection *connection, int &x) { /// TODO if (connection->direction != FtpConnection::Upload) return FtpError; FTP_PROPAGATE_ERROR__(WaitSocket(connection)) if ((x = net_write(connection->handle, output, length)) != length) return FtpError; return FtpOk; } //------------------------------------------------------------------------------ //------------------------------------------------------------------------------ FtpLibrary::State FtpLibrary::ReadBinary(void *buf, int max, FtpConnection *connection, int &length) { if (connection->direction != FtpConnection::Download) return FtpError; FTP_PROPAGATE_ERROR__(WaitSocket(connection)) length = net_read(connection->handle, buf, max); if (length == -1) return FtpError; DataReadCallback(connection); return FtpOk; } FtpLibrary::State FtpLibrary::WriteBinary(void *buf, int len, FtpConnection *connection) { if (connection->direction != FtpConnection::Upload) return FtpError; FTP_PROPAGATE_ERROR__(WaitSocket(connection)) if (net_write(connection->handle, buf, len) != len) return FtpError; DataWriteCallback(connection); return FtpOk; } //------------------------------------------------------------------------------ //----------------------------------------------------------------------------------------------------------------------- FtpLibrary::State FtpLibrary::DataTransfer(const char *localfile, const char *path, AccessType type, TransferMode mode) //----------------------------------------------------------------------------------------------------------------------- { // Open local file or I/O stream. FILE *file; if (localfile) { const char *access; switch (type) { default: case AccessDir: case AccessDirVerbose: case AccessFileRead: access = (mode == TransferBinary) ? "wb" : "w"; break; case AccessFileReadAppend: access = (mode == TransferBinary) ? "ab" : "a"; break; case AccessFileWriteAppend: case AccessFileWrite: access = (mode == TransferBinary) ? "rb" : "r"; break; } file = fopen(localfile, access); if (!file) __ERR__(__LOG_E__ << "Failed to open FTP output file.\n", FtpError) if (type == AccessFileWriteAppend) if (fseek(file, offset, SEEK_SET)) { fclose(file); __ERR__(__LOG_E__ << "Failed to seek in FTP file for transfer.\n", FtpError) } } else file = (type == AccessFileWrite || type == AccessFileWriteAppend) ? stdin : stdout; // Create a new connection. FtpConnection *connection = CreateConnection(path, type, mode); if (!connection) { if (localfile) fclose(file); return FtpError; } // Perform transfer. State retv = FtpOk; GS::Array temp_buffer(ConnectionBufferSize); int length; if ((type == AccessFileWrite) || (type == AccessFileWriteAppend)) { while ((length = (int)fread(temp_buffer, 1, ConnectionBufferSize, file)) > 0) if (WriteBinary(temp_buffer, length, connection) != FtpOk) { retv = FtpError; // FTP write failed. break; } } else { while ((retv = ReadBinary(temp_buffer, ConnectionBufferSize, connection, length)) == FtpOk) if (!length || (fwrite(temp_buffer, 1, length, file) <= 0)) break; // Done. } // Flush file. fflush(file); if (localfile) fclose(file); CloseConnection(connection); return retv; } //------------------------------------------------------------------------------ FtpLibrary::State FtpLibrary::Download(const char *file, const char *path, TransferMode mode, int _offset) { offset = _offset; if (!offset) return DataTransfer(file, path, AccessFileRead, mode); else return DataTransfer(file, path, AccessFileReadAppend, mode); } FtpLibrary::State FtpLibrary::Upload(const char *file, const char *path, TransferMode mode, int _offset) { offset = _offset; if (!offset) return DataTransfer(file, path, AccessFileWrite, mode); else return DataTransfer(file, path, AccessFileWriteAppend, mode); } //------------------------------------------------------------------------------ //------------------------------------ bool FtpLibrary::Quit() //------------------------------------ { if (!master_connection.handle) return true; bool r = FtpSendCmd("QUIT", '2'); net_close(master_connection.handle); master_connection.handle = 0; return r; } //------------------------------------------------------------------------------ bool FtpLibrary::DeleteFile(const char *path) { return FtpSendCmd(String::Format("DELE %s", path).c_str(), '2'); } bool FtpLibrary::ChangeDirectory(const char *path) { return FtpSendCmd(String::Format("CWD %s", path).c_str(), '2'); } bool FtpLibrary::UpDirectory() { return FtpSendCmd("CDUP", '2'); } //------------------------------------------------------------------------------ //------------------------------------------------------------------------------ int FtpLibrary::GetFileSize(const char *path, TransferMode mode) //------------------------------------------------------------------------------ { int rs, sz = -1; if ( FtpSendCmd(String::Format("TYPE %c", mode).c_str(), '2') && FtpSendCmd(String::Format("SIZE %s", path).c_str(), '2') && (sscanf(response, "%d %d", &rs, &sz) == 2) ) return sz; return -1; } //---------------------------------------------------------------------------- bool FtpLibrary::Nlst(const char *outputfile, const char *path) //---------------------------------------------------------------------------- { offset = 0; return DataTransfer(outputfile, path, FtpLibrary::AccessDir, FtpLibrary::TransferASCII) == FtpOk ? true : false; } //------------------------------------------------------------------------------ FtpLibrary::FtpLibrary() { #if __PLATFORM_WINDOWS__ WSADATA wsa; if (WSAStartup(MAKEWORD(1, 1), &wsa)) __LOG__ << "[!] (FTP) WINSOCK startup error.\n"; #endif connection_mode = ConnectionPORT; master_connection.AllocateBuffer(); // offset = 0; correctpasv = false; } FtpLibrary::~FtpLibrary() { Quit(); } //------------------------------------------------------------------------------ #endif /* int FtpLibrary::Site(const char *cmd) { char buf[256]; if ((strlen(cmd) + 7) > sizeof(buf)) return 0; sprintf(buf,"SITE %s",cmd); if (!FtpSendCmd(buf,'2',mp_ftphandle)) return 0; return 1; } int FtpLibrary::Raw(const char *cmd) { char buf[256]; strncpy(buf, cmd, 256); if (!FtpSendCmd(buf,'2',mp_ftphandle)) return 0; return 1; } int FtpLibrary::SysType(char *buf, int max) { int l = max; char *b = buf; char *s; if (!FtpSendCmd("SYST",'2',mp_ftphandle)) return 0; s = &mp_ftphandle->response[4]; while ((--l) && (*s != ' ')) *b++ = *s++; *b++ = '\0'; return 1; } int FtpLibrary::Mkdir(const char *path) { char buf[256]; if ((strlen(path) + 6) > sizeof(buf)) return 0; sprintf(buf,"MKD %s",path); if (!FtpSendCmd(buf,'2', mp_ftphandle)) return 0; return 1; } int FtpLibrary::Rmdir(const char *path) { char buf[256]; if ((strlen(path) + 6) > sizeof(buf)) return 0; sprintf(buf,"RMD %s",path); if (!FtpSendCmd(buf,'2',mp_ftphandle)) return 0; return 1; } int FtpLibrary::Pwd(char *path, int max) { int l = max; char *b = path; char *s; if (!FtpSendCmd("PWD",'2',mp_ftphandle)) return 0; s = strchr(mp_ftphandle->response, '"'); if (s == NULL) return 0; s++; while ((--l) && (*s) && (*s != '"')) *b++ = *s++; *b++ = '\0'; return 1; } int FtpLibrary::Dir(const char *outputfile, const char *path) { mp_ftphandle->offset = 0; return FtpXfer(outputfile, path, mp_ftphandle, FtpLibrary::dirverbose, FtpLibrary::ascii); } int FtpLibrary::ModDate(const char *path, char *dt, int max) { char buf[256]; int rv = 1; if ((strlen(path) + 7) > sizeof(buf)) return 0; sprintf(buf,"MDTM %s",path); if (!FtpSendCmd(buf,'2',mp_ftphandle)) rv = 0; else strncpy(dt, &mp_ftphandle->response[4], max); return rv; } int FtpLibrary::Rename(const char *src, const char *dst) { char cmd[256]; if (((strlen(src) + 7) > sizeof(cmd)) || ((strlen(dst) + 7) > sizeof(cmd))) return 0; sprintf(cmd,"RNFR %s",src); if (!FtpSendCmd(cmd,'3',mp_ftphandle)) return 0; sprintf(cmd,"RNTO %s",dst); if (!FtpSendCmd(cmd,'2',mp_ftphandle)) return 0; return 1; } void FtpLibrary::SetConnmode(connmode mode) { mp_ftphandle->cmode = mode; } ftphandle* FtpLibrary::RawOpen(const char *path, accesstype type, transfermode mode) { int ret; ftphandle* datahandle; ret = CreateConnection(path, type, mode, mp_ftphandle, &datahandle); if (ret) return datahandle; else return NULL; } int FtpLibrary::RawClose(ftphandle* handle) { return FtpClose(handle); } int FtpLibrary::RawWrite(void* buf, int len, ftphandle* handle) { return FtpWrite(buf, len, handle); } int FtpLibrary::RawRead(void* buf, int max, ftphandle* handle) { return FtpRead(buf, max, handle); } */