/* $Id: ftp-server.cpp $ */ /** @file * Generic FTP server (RFC 959) implementation. * * Partly also implements RFC 3659 (Extensions to FTP, for "SIZE", ++). * * Known limitations so far: * - UTF-8 support only. * - Only supports ASCII + binary (image type) file streams for now. * - No directory / file caching yet. * - No support for writing / modifying ("DELE", "MKD", "RMD", "STOR", ++). * - No FTPS / SFTP support. * - No passive mode ("PASV") support. * - No IPv6 support. * - No proxy support. * - No FXP support. */ /* * Copyright (C) 2020-2023 Oracle and/or its affiliates. * * This file is part of VirtualBox base platform packages, as * available from https://www.virtualbox.org. * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU General Public License * as published by the Free Software Foundation, in version 3 of the * License. * * This program is distributed in the hope that it will be useful, but * WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU * General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program; if not, see . * * The contents of this file may alternatively be used under the terms * of the Common Development and Distribution License Version 1.0 * (CDDL), a copy of it is provided in the "COPYING.CDDL" file included * in the VirtualBox distribution, in which case the provisions of the * CDDL are applicable instead of those of the GPL. * * You may elect to license modified versions of this file under the * terms and conditions of either the GPL or the CDDL or both. * * SPDX-License-Identifier: GPL-3.0-only OR CDDL-1.0 */ /********************************************************************************************************************************* * Header Files * *********************************************************************************************************************************/ #define LOG_GROUP RTLOGGROUP_FTP #include #include "internal/iprt.h" #include "internal/magics.h" #include #include #include #include #include /* For file mode flags. */ #include #include #include #include #include #include #include #include #include #include /********************************************************************************************************************************* * Structures and Typedefs * *********************************************************************************************************************************/ /** * Internal FTP server instance. */ typedef struct RTFTPSERVERINTERNAL { /** Magic value. */ uint32_t u32Magic; /** Callback table. */ RTFTPSERVERCALLBACKS Callbacks; /** Pointer to TCP server instance. */ PRTTCPSERVER pTCPServer; /** Number of currently connected clients. */ uint32_t cClients; /** Pointer to user-specific data. Optional. */ void *pvUser; /** Size of user-specific data. Optional. */ size_t cbUser; } RTFTPSERVERINTERNAL; /** Pointer to an internal FTP server instance. */ typedef RTFTPSERVERINTERNAL *PRTFTPSERVERINTERNAL; /** * FTP directory entry. */ typedef struct RTFTPDIRENTRY { /** The information about the entry. */ RTFSOBJINFO Info; /** Symbolic link target (allocated after the name). */ const char *pszTarget; /** Owner if applicable (allocated after the name). */ const char *pszOwner; /** Group if applicable (allocated after the name). */ const char *pszGroup; /** The length of szName. */ size_t cchName; /** The entry name. */ RT_FLEXIBLE_ARRAY_EXTENSION char szName[RT_FLEXIBLE_ARRAY]; } RTFTPDIRENTRY; /** Pointer to a FTP directory entry. */ typedef RTFTPDIRENTRY *PRTFTPDIRENTRY; /** Pointer to a FTP directory entry pointer. */ typedef PRTFTPDIRENTRY *PPRTFTPDIRENTRY; /** * Collection of directory entries. * Used for also caching stuff. */ typedef struct RTFTPDIRCOLLECTION { /** Current size of papEntries. */ size_t cEntries; /** Memory allocated for papEntries. */ size_t cEntriesAllocated; /** Current entries pending sorting and display. */ PPRTFTPDIRENTRY papEntries; /** Total number of bytes allocated for the above entries. */ uint64_t cbTotalAllocated; /** Total number of file content bytes. */ uint64_t cbTotalFiles; } RTFTPDIRCOLLECTION; /** Pointer to a directory collection. */ typedef RTFTPDIRCOLLECTION *PRTFTPDIRCOLLECTION; /** Pointer to a directory entry collection pointer. */ typedef PRTFTPDIRCOLLECTION *PPRTFTPDIRCOLLECTION; /********************************************************************************************************************************* * Defined Constants And Macros * *********************************************************************************************************************************/ /** Validates a handle and returns VERR_INVALID_HANDLE if not valid. */ #define RTFTPSERVER_VALID_RETURN_RC(hFTPServer, a_rc) \ do { \ AssertPtrReturn((hFTPServer), (a_rc)); \ AssertReturn((hFTPServer)->u32Magic == RTFTPSERVER_MAGIC, (a_rc)); \ } while (0) /** Validates a handle and returns VERR_INVALID_HANDLE if not valid. */ #define RTFTPSERVER_VALID_RETURN(hFTPServer) RTFTPSERVER_VALID_RETURN_RC((hFTPServer), VERR_INVALID_HANDLE) /** Validates a handle and returns (void) if not valid. */ #define RTFTPSERVER_VALID_RETURN_VOID(hFTPServer) \ do { \ AssertPtrReturnVoid(hFTPServer); \ AssertReturnVoid((hFTPServer)->u32Magic == RTFTPSERVER_MAGIC); \ } while (0) /** Handles a FTP server callback with no arguments and returns. */ #define RTFTPSERVER_HANDLE_CALLBACK_RET(a_Name) \ do \ { \ PRTFTPSERVERCALLBACKS pCallbacks = &pClient->pServer->Callbacks; \ if (pCallbacks->a_Name) \ { \ RTFTPCALLBACKDATA Data = { &pClient->State }; \ return pCallbacks->a_Name(&Data); \ } \ return VERR_NOT_IMPLEMENTED; \ } while (0) /** Handles a FTP server callback with no arguments and sets rc accordingly. */ #define RTFTPSERVER_HANDLE_CALLBACK(a_Name) \ do \ { \ PRTFTPSERVERCALLBACKS pCallbacks = &pClient->pServer->Callbacks; \ if (pCallbacks->a_Name) \ { \ RTFTPCALLBACKDATA Data = { &pClient->State, pClient->pServer->pvUser, pClient->pServer->cbUser }; \ rc = pCallbacks->a_Name(&Data); \ } \ else \ rc = VERR_NOT_IMPLEMENTED; \ } while (0) /** Handles a FTP server callback with arguments and sets rc accordingly. */ #define RTFTPSERVER_HANDLE_CALLBACK_VA(a_Name, ...) \ do \ { \ PRTFTPSERVERCALLBACKS pCallbacks = &pClient->pServer->Callbacks; \ if (pCallbacks->a_Name) \ { \ RTFTPCALLBACKDATA Data = { &pClient->State, pClient->pServer->pvUser, pClient->pServer->cbUser }; \ rc = pCallbacks->a_Name(&Data, __VA_ARGS__); \ } \ else \ rc = VERR_NOT_IMPLEMENTED; \ } while (0) /** Handles a FTP server callback with arguments and returns. */ #define RTFTPSERVER_HANDLE_CALLBACK_VA_RET(a_Name, ...) \ do \ { \ PRTFTPSERVERCALLBACKS pCallbacks = &pClient->pServer->Callbacks; \ if (pCallbacks->a_Name) \ { \ RTFTPCALLBACKDATA Data = { &pClient->State, pClient->pServer->pvUser, pClient->pServer->cbUser }; \ return pCallbacks->a_Name(&Data, __VA_ARGS__); \ } \ return VERR_NOT_IMPLEMENTED; \ } while (0) /********************************************************************************************************************************* * Structures and Typedefs * *********************************************************************************************************************************/ /** Supported FTP server command IDs. * Alphabetically, named after their official command names. */ typedef enum RTFTPSERVERCMD { /** Invalid command, do not use. Always must come first. */ RTFTPSERVERCMD_INVALID = 0, /** Aborts the current command on the server. */ RTFTPSERVERCMD_ABOR, /** Changes the current working directory. */ RTFTPSERVERCMD_CDUP, /** Changes the current working directory. */ RTFTPSERVERCMD_CWD, /** Reports features supported by the server. */ RTFTPSERVERCMD_FEAT, /** Lists a directory. */ RTFTPSERVERCMD_LIST, /** Sets the transfer mode. */ RTFTPSERVERCMD_MODE, /** Sends a nop ("no operation") to the server. */ RTFTPSERVERCMD_NOOP, /** Sets the password for authentication. */ RTFTPSERVERCMD_PASS, /** Sets the port to use for the data connection. */ RTFTPSERVERCMD_PORT, /** Gets the current working directory. */ RTFTPSERVERCMD_PWD, /** Get options. Needed in conjunction with the FEAT command. */ RTFTPSERVERCMD_OPTS, /** Terminates the session (connection). */ RTFTPSERVERCMD_QUIT, /** Retrieves a specific file. */ RTFTPSERVERCMD_RETR, /** Retrieves the size of a file. */ RTFTPSERVERCMD_SIZE, /** Retrieves the current status of a transfer. */ RTFTPSERVERCMD_STAT, /** Sets the structure type to use. */ RTFTPSERVERCMD_STRU, /** Gets the server's OS info. */ RTFTPSERVERCMD_SYST, /** Sets the (data) representation type. */ RTFTPSERVERCMD_TYPE, /** Sets the user name for authentication. */ RTFTPSERVERCMD_USER, /** End marker. */ RTFTPSERVERCMD_END, /** The usual 32-bit hack. */ RTFTPSERVERCMD_32BIT_HACK = 0x7fffffff } RTFTPSERVERCMD; struct RTFTPSERVERCLIENT; /** * Structure for maintaining a single data connection. */ typedef struct RTFTPSERVERDATACONN { /** Pointer to associated client of this data connection. */ RTFTPSERVERCLIENT *pClient; /** Data connection IP. */ RTNETADDRIPV4 Addr; /** Data connection port number. */ uint16_t uPort; /** The current data socket to use. * Can be NIL_RTSOCKET if no data port has been specified (yet) or has been closed. */ RTSOCKET hSocket; /** Thread serving the data connection. */ RTTHREAD hThread; /** Thread started indicator. */ volatile bool fStarted; /** Thread stop indicator. */ volatile bool fStop; /** Thread stopped indicator. */ volatile bool fStopped; /** Overall result when closing the data connection. */ int rc; /** Number of command arguments. */ uint8_t cArgs; /** Command arguments array. Optional and can be NULL. * Will be free'd by the data connection thread. */ char** papszArgs; /** Circular buffer for caching data before writing. */ PRTCIRCBUF pCircBuf; } RTFTPSERVERDATACONN; /** Pointer to a data connection struct. */ typedef RTFTPSERVERDATACONN *PRTFTPSERVERDATACONN; /** * Structure for maintaining an internal FTP server client. */ typedef struct RTFTPSERVERCLIENT { /** Pointer to internal server state. */ PRTFTPSERVERINTERNAL pServer; /** Socket handle the client is bound to. */ RTSOCKET hSocket; /** Actual client state. */ RTFTPSERVERCLIENTSTATE State; /** The last set data connection IP. */ RTNETADDRIPV4 DataConnAddr; /** The last set data connection port number. */ uint16_t uDataConnPort; /** Data connection information. * At the moment we only allow one data connection per client at a time. */ PRTFTPSERVERDATACONN pDataConn; } RTFTPSERVERCLIENT; /** Pointer to an internal FTP server client state. */ typedef RTFTPSERVERCLIENT *PRTFTPSERVERCLIENT; /** Function pointer declaration for a specific FTP server command handler. */ typedef DECLCALLBACKTYPE(int, FNRTFTPSERVERCMD,(PRTFTPSERVERCLIENT pClient, uint8_t cArgs, const char * const *apszArgs)); /** Pointer to a FNRTFTPSERVERCMD(). */ typedef FNRTFTPSERVERCMD *PFNRTFTPSERVERCMD; /********************************************************************************************************************************* * Internal Functions * *********************************************************************************************************************************/ static int rtFtpServerDataConnOpen(PRTFTPSERVERDATACONN pDataConn, PRTNETADDRIPV4 pAddr, uint16_t uPort); static int rtFtpServerDataConnClose(PRTFTPSERVERDATACONN pDataConn); static void rtFtpServerDataConnReset(PRTFTPSERVERDATACONN pDataConn); static int rtFtpServerDataConnStart(PRTFTPSERVERDATACONN pDataConn, PFNRTTHREAD pfnThread, uint8_t cArgs, const char * const *apszArgs); static int rtFtpServerDataConnStop(PRTFTPSERVERDATACONN pDataConn); static void rtFtpServerDataConnDestroy(PRTFTPSERVERDATACONN pDataConn); static int rtFtpServerDataConnFlush(PRTFTPSERVERDATACONN pDataConn); static void rtFtpServerClientStateReset(PRTFTPSERVERCLIENTSTATE pState); /** @name Command handlers. * @{ */ static FNRTFTPSERVERCMD rtFtpServerHandleABOR; static FNRTFTPSERVERCMD rtFtpServerHandleCDUP; static FNRTFTPSERVERCMD rtFtpServerHandleCWD; static FNRTFTPSERVERCMD rtFtpServerHandleFEAT; static FNRTFTPSERVERCMD rtFtpServerHandleLIST; static FNRTFTPSERVERCMD rtFtpServerHandleMODE; static FNRTFTPSERVERCMD rtFtpServerHandleNOOP; static FNRTFTPSERVERCMD rtFtpServerHandlePASS; static FNRTFTPSERVERCMD rtFtpServerHandlePORT; static FNRTFTPSERVERCMD rtFtpServerHandlePWD; static FNRTFTPSERVERCMD rtFtpServerHandleOPTS; static FNRTFTPSERVERCMD rtFtpServerHandleQUIT; static FNRTFTPSERVERCMD rtFtpServerHandleRETR; static FNRTFTPSERVERCMD rtFtpServerHandleSIZE; static FNRTFTPSERVERCMD rtFtpServerHandleSTAT; static FNRTFTPSERVERCMD rtFtpServerHandleSTRU; static FNRTFTPSERVERCMD rtFtpServerHandleSYST; static FNRTFTPSERVERCMD rtFtpServerHandleTYPE; static FNRTFTPSERVERCMD rtFtpServerHandleUSER; /** @} */ /** * Structure for maintaining a single command entry for the command table. */ typedef struct RTFTPSERVERCMD_ENTRY { /** Command ID. */ RTFTPSERVERCMD enmCmd; /** Command represented as ASCII string. */ char szCmd[RTFTPSERVER_MAX_CMD_LEN]; /** Whether the commands needs a logged in (valid) user. */ bool fNeedsUser; /** Function pointer invoked to handle the command. */ PFNRTFTPSERVERCMD pfnCmd; } RTFTPSERVERCMD_ENTRY; /** Pointer to a command entry. */ typedef RTFTPSERVERCMD_ENTRY *PRTFTPSERVERCMD_ENTRY; /********************************************************************************************************************************* * Global Variables * *********************************************************************************************************************************/ /** * Table of handled commands. */ static const RTFTPSERVERCMD_ENTRY g_aCmdMap[] = { { RTFTPSERVERCMD_ABOR, "ABOR", true, rtFtpServerHandleABOR }, { RTFTPSERVERCMD_CDUP, "CDUP", true, rtFtpServerHandleCDUP }, { RTFTPSERVERCMD_CWD, "CWD", true, rtFtpServerHandleCWD }, { RTFTPSERVERCMD_FEAT, "FEAT", false, rtFtpServerHandleFEAT }, { RTFTPSERVERCMD_LIST, "LIST", true, rtFtpServerHandleLIST }, { RTFTPSERVERCMD_MODE, "MODE", true, rtFtpServerHandleMODE }, { RTFTPSERVERCMD_NOOP, "NOOP", true, rtFtpServerHandleNOOP }, { RTFTPSERVERCMD_PASS, "PASS", false, rtFtpServerHandlePASS }, { RTFTPSERVERCMD_PORT, "PORT", true, rtFtpServerHandlePORT }, { RTFTPSERVERCMD_PWD, "PWD", true, rtFtpServerHandlePWD }, { RTFTPSERVERCMD_OPTS, "OPTS", false, rtFtpServerHandleOPTS }, { RTFTPSERVERCMD_QUIT, "QUIT", false, rtFtpServerHandleQUIT }, { RTFTPSERVERCMD_RETR, "RETR", true, rtFtpServerHandleRETR }, { RTFTPSERVERCMD_SIZE, "SIZE", true, rtFtpServerHandleSIZE }, { RTFTPSERVERCMD_STAT, "STAT", true, rtFtpServerHandleSTAT }, { RTFTPSERVERCMD_STRU, "STRU", true, rtFtpServerHandleSTRU }, { RTFTPSERVERCMD_SYST, "SYST", false, rtFtpServerHandleSYST }, { RTFTPSERVERCMD_TYPE, "TYPE", true, rtFtpServerHandleTYPE }, { RTFTPSERVERCMD_USER, "USER", false, rtFtpServerHandleUSER }, { RTFTPSERVERCMD_END, "", false, NULL } }; /** RFC-1123 month of the year names. */ static const char * const g_apszMonths[1+12] = { "000", "Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec" }; /** Feature string which represents all commands we support in addition to RFC 959 (see RFC 2398). * Must match the command table above. * * Don't forget the beginning space (" ") at each feature. */ #define RTFTPSERVER_FEATURES_STRING \ " SIZE\r\n" \ " UTF8" /** Maximum length in characters a FTP server path can have (excluding termination). */ #define RTFTPSERVER_MAX_PATH RTPATH_MAX /********************************************************************************************************************************* * Protocol Functions * *********************************************************************************************************************************/ /** * Replies a (three digit) reply code back to the client. * * @returns VBox status code. * @param pClient Client to reply to. * @param enmReply Reply code to send. */ static int rtFtpServerSendReplyRc(PRTFTPSERVERCLIENT pClient, RTFTPSERVER_REPLY enmReply) { /* Note: If we don't supply any additional text, make sure to include an empty stub, as * some clients expect this as part of their parsing code. */ char szReply[32]; RTStrPrintf2(szReply, sizeof(szReply), "%RU32 -\r\n", enmReply); LogFlowFunc(("Sending reply code %RU32\n", enmReply)); return RTTcpWrite(pClient->hSocket, szReply, strlen(szReply)); } /** * Replies a (three digit) reply code with a custom message back to the client. * * @returns VBox status code. * @param pClient Client to reply to. * @param enmReply Reply code to send. * @param pszFormat Format string of message to send with the reply code. */ static int rtFtpServerSendReplyRcEx(PRTFTPSERVERCLIENT pClient, RTFTPSERVER_REPLY enmReply, const char *pszFormat, ...) { char *pszMsg = NULL; va_list args; va_start(args, pszFormat); char *pszFmt = NULL; const int cch = RTStrAPrintfV(&pszFmt, pszFormat, args); va_end(args); AssertReturn(cch > 0, VERR_NO_MEMORY); int rc = RTStrAPrintf(&pszMsg, "%RU32 -", enmReply); AssertRCReturn(rc, rc); /** @todo Support multi-line replies (see 4.2ff). */ if (pszFmt) { rc = RTStrAAppend(&pszMsg, " "); AssertRCReturn(rc, rc); rc = RTStrAAppend(&pszMsg, pszFmt); AssertRCReturn(rc, rc); } rc = RTStrAAppend(&pszMsg, "\r\n"); AssertRCReturn(rc, rc); RTStrFree(pszFmt); rc = RTTcpWrite(pClient->hSocket, pszMsg, strlen(pszMsg)); RTStrFree(pszMsg); return rc; } /** * Replies a string back to the client. * * @returns VBox status code. * @param pClient Client to reply to. * @param pszFormat Format to reply. * @param ... Format arguments. */ static int rtFtpServerSendReplyStr(PRTFTPSERVERCLIENT pClient, const char *pszFormat, ...) { va_list args; va_start(args, pszFormat); char *psz = NULL; const int cch = RTStrAPrintfV(&psz, pszFormat, args); va_end(args); AssertReturn(cch > 0, VERR_NO_MEMORY); int rc = RTStrAAppend(&psz, "\r\n"); AssertRCReturn(rc, rc); LogFlowFunc(("Sending reply '%s'\n", psz)); rc = RTTcpWrite(pClient->hSocket, psz, strlen(psz)); RTStrFree(psz); return rc; } /** * Validates if a given absolute path is valid or not. * * @returns \c true if path is valid, or \c false if not. * @param pszPath Path to check. * @param fIsAbsolute Whether the path to check is an absolute path or not. */ static bool rtFtpServerPathIsValid(const char *pszPath, bool fIsAbsolute) { if (!pszPath) return false; bool fIsValid = strlen(pszPath) && RTStrIsValidEncoding(pszPath) && RTStrStr(pszPath, "..") == NULL; /** @todo Very crude for now -- improve this. */ if ( fIsValid && fIsAbsolute) { RTFSOBJINFO objInfo; int rc2 = RTPathQueryInfo(pszPath, &objInfo, RTFSOBJATTRADD_NOTHING); if (RT_SUCCESS(rc2)) { fIsValid = RTFS_IS_DIRECTORY(objInfo.Attr.fMode) || RTFS_IS_FILE(objInfo.Attr.fMode); /* No symlinks and other stuff not allowed. */ } else fIsValid = false; } LogFlowFunc(("pszPath=%s -> %RTbool\n", pszPath, fIsValid)); return fIsValid; } /** * Sets the current working directory for a client. * * @returns VBox status code. * @param pState Client state to set current working directory for. * @param pszPath Working directory to set. */ static int rtFtpSetCWD(PRTFTPSERVERCLIENTSTATE pState, const char *pszPath) { RTStrFree(pState->pszCWD); if (!rtFtpServerPathIsValid(pszPath, false /* fIsAbsolute */)) return VERR_INVALID_PARAMETER; pState->pszCWD = RTStrDup(pszPath); LogFlowFunc(("Current CWD is now '%s'\n", pState->pszCWD)); int rc = pState->pszCWD ? VINF_SUCCESS : VERR_NO_MEMORY; AssertRC(rc); return rc; } /** * Looks up an user account. * * @returns VBox status code, or VERR_NOT_FOUND if user has not been found. * @param pClient Client to look up user for. * @param pszUser User name to look up. */ static int rtFtpServerLookupUser(PRTFTPSERVERCLIENT pClient, const char *pszUser) { RTFTPSERVER_HANDLE_CALLBACK_VA_RET(pfnOnUserConnect, pszUser); } /** * Handles the actual client authentication. * * @returns VBox status code, or VERR_ACCESS_DENIED if authentication failed. * @param pClient Client to authenticate. * @param pszUser User name to authenticate with. * @param pszPassword Password to authenticate with. */ static int rtFtpServerAuthenticate(PRTFTPSERVERCLIENT pClient, const char *pszUser, const char *pszPassword) { RTFTPSERVER_HANDLE_CALLBACK_VA_RET(pfnOnUserAuthenticate, pszUser, pszPassword); } /** * Converts a RTFSOBJINFO struct to a string. * * @returns VBox status code. * @param pObjInfo RTFSOBJINFO object to convert. * @param pszFsObjInfo Where to store the output string. * @param cbFsObjInfo Size of the output string in bytes. */ static int rtFtpServerFsObjInfoToStr(PRTFSOBJINFO pObjInfo, char *pszFsObjInfo, size_t cbFsObjInfo) { RTFMODE fMode = pObjInfo->Attr.fMode; char chFileType; switch (fMode & RTFS_TYPE_MASK) { case RTFS_TYPE_FIFO: chFileType = 'f'; break; case RTFS_TYPE_DEV_CHAR: chFileType = 'c'; break; case RTFS_TYPE_DIRECTORY: chFileType = 'd'; break; case RTFS_TYPE_DEV_BLOCK: chFileType = 'b'; break; case RTFS_TYPE_FILE: chFileType = '-'; break; case RTFS_TYPE_SYMLINK: chFileType = 'l'; break; case RTFS_TYPE_SOCKET: chFileType = 's'; break; case RTFS_TYPE_WHITEOUT: chFileType = 'w'; break; default: chFileType = '?'; break; } char szTimeBirth[RTTIME_STR_LEN]; char szTimeChange[RTTIME_STR_LEN]; char szTimeModification[RTTIME_STR_LEN]; char szTimeAccess[RTTIME_STR_LEN]; #define INFO_TO_STR(a_Format, ...) \ do \ { \ const ssize_t cchSize = RTStrPrintf2(szTemp, sizeof(szTemp), a_Format, __VA_ARGS__); \ AssertReturn(cchSize > 0, VERR_BUFFER_OVERFLOW); \ const int rc2 = RTStrCat(pszFsObjInfo, cbFsObjInfo, szTemp); \ AssertRCReturn(rc2, rc2); \ } while (0); char szTemp[128]; INFO_TO_STR("%c", chFileType); INFO_TO_STR("%c%c%c", fMode & RTFS_UNIX_IRUSR ? 'r' : '-', fMode & RTFS_UNIX_IWUSR ? 'w' : '-', fMode & RTFS_UNIX_IXUSR ? 'x' : '-'); INFO_TO_STR("%c%c%c", fMode & RTFS_UNIX_IRGRP ? 'r' : '-', fMode & RTFS_UNIX_IWGRP ? 'w' : '-', fMode & RTFS_UNIX_IXGRP ? 'x' : '-'); INFO_TO_STR("%c%c%c", fMode & RTFS_UNIX_IROTH ? 'r' : '-', fMode & RTFS_UNIX_IWOTH ? 'w' : '-', fMode & RTFS_UNIX_IXOTH ? 'x' : '-'); INFO_TO_STR( " %c%c%c%c%c%c%c%c%c%c%c%c%c%c", fMode & RTFS_DOS_READONLY ? 'R' : '-', fMode & RTFS_DOS_HIDDEN ? 'H' : '-', fMode & RTFS_DOS_SYSTEM ? 'S' : '-', fMode & RTFS_DOS_DIRECTORY ? 'D' : '-', fMode & RTFS_DOS_ARCHIVED ? 'A' : '-', fMode & RTFS_DOS_NT_DEVICE ? 'd' : '-', fMode & RTFS_DOS_NT_NORMAL ? 'N' : '-', fMode & RTFS_DOS_NT_TEMPORARY ? 'T' : '-', fMode & RTFS_DOS_NT_SPARSE_FILE ? 'P' : '-', fMode & RTFS_DOS_NT_REPARSE_POINT ? 'J' : '-', fMode & RTFS_DOS_NT_COMPRESSED ? 'C' : '-', fMode & RTFS_DOS_NT_OFFLINE ? 'O' : '-', fMode & RTFS_DOS_NT_NOT_CONTENT_INDEXED ? 'I' : '-', fMode & RTFS_DOS_NT_ENCRYPTED ? 'E' : '-'); INFO_TO_STR( " %d %4d %4d %10lld %10lld", pObjInfo->Attr.u.Unix.cHardlinks, pObjInfo->Attr.u.Unix.uid, pObjInfo->Attr.u.Unix.gid, pObjInfo->cbObject, pObjInfo->cbAllocated); INFO_TO_STR( " %s %s %s %s", RTTimeSpecToString(&pObjInfo->BirthTime, szTimeBirth, sizeof(szTimeBirth)), RTTimeSpecToString(&pObjInfo->ChangeTime, szTimeChange, sizeof(szTimeChange)), RTTimeSpecToString(&pObjInfo->ModificationTime, szTimeModification, sizeof(szTimeModification)), RTTimeSpecToString(&pObjInfo->AccessTime, szTimeAccess, sizeof(szTimeAccess)) ); #undef INFO_TO_STR return VINF_SUCCESS; } /** * Parses a string which consists of an IPv4 (ww,xx,yy,zz) and a port number (hi,lo), all separated by comma delimiters. * See RFC 959, 4.1.2. * * @returns VBox status code. * @param pszStr String to parse. * @param pAddr Where to store the IPv4 address on success. * @param puPort Where to store the port number on success. */ static int rtFtpParseHostAndPort(const char *pszStr, PRTNETADDRIPV4 pAddr, uint16_t *puPort) { AssertPtrReturn(pszStr, VERR_INVALID_POINTER); AssertPtrReturn(pAddr, VERR_INVALID_POINTER); AssertPtrReturn(puPort, VERR_INVALID_POINTER); char *pszNext; int rc; /* Parse IP (v4). */ /** @todo I don't think IPv6 ever will be a thing here, or will it? */ rc = RTStrToUInt8Ex(pszStr, &pszNext, 10, &pAddr->au8[0]); if (rc != VINF_SUCCESS && rc != VWRN_TRAILING_CHARS) return VERR_INVALID_PARAMETER; if (*pszNext++ != ',') return VERR_INVALID_PARAMETER; rc = RTStrToUInt8Ex(pszNext, &pszNext, 10, &pAddr->au8[1]); if (rc != VINF_SUCCESS && rc != VWRN_TRAILING_CHARS) return VERR_INVALID_PARAMETER; if (*pszNext++ != ',') return VERR_INVALID_PARAMETER; rc = RTStrToUInt8Ex(pszNext, &pszNext, 10, &pAddr->au8[2]); if (rc != VINF_SUCCESS && rc != VWRN_TRAILING_CHARS) return VERR_INVALID_PARAMETER; if (*pszNext++ != ',') return VERR_INVALID_PARAMETER; rc = RTStrToUInt8Ex(pszNext, &pszNext, 10, &pAddr->au8[3]); if (rc != VINF_SUCCESS && rc != VWRN_TRAILING_SPACES && rc != VWRN_TRAILING_CHARS) return VERR_INVALID_PARAMETER; if (*pszNext++ != ',') return VERR_INVALID_PARAMETER; /* Parse port. */ uint8_t uPortHi; rc = RTStrToUInt8Ex(pszNext, &pszNext, 10, &uPortHi); if (rc != VINF_SUCCESS && rc != VWRN_TRAILING_SPACES && rc != VWRN_TRAILING_CHARS) return VERR_INVALID_PARAMETER; if (*pszNext++ != ',') return VERR_INVALID_PARAMETER; uint8_t uPortLo; rc = RTStrToUInt8Ex(pszNext, &pszNext, 10, &uPortLo); if (rc != VINF_SUCCESS && rc != VWRN_TRAILING_SPACES && rc != VWRN_TRAILING_CHARS) return VERR_INVALID_PARAMETER; *puPort = RT_MAKE_U16(uPortLo, uPortHi); return rc; } /** * Duplicates a command argument vector. * * @returns Duplicated argument vector or NULL if failed or no arguments given. Needs to be free'd with rtFtpCmdArgsFree(). * @param cArgs Number of arguments in argument vector. * @param apszArgs Pointer to argument vector to duplicate. */ static char** rtFtpCmdArgsDup(uint8_t cArgs, const char * const *apszArgs) { if (!cArgs) return NULL; char **apszArgsDup = (char **)RTMemAlloc(cArgs * sizeof(char *)); if (!apszArgsDup) { AssertFailed(); return NULL; } int rc2 = VINF_SUCCESS; uint8_t i; for (i = 0; i < cArgs; i++) { apszArgsDup[i] = RTStrDup(apszArgs[i]); if (!apszArgsDup[i]) rc2 = VERR_NO_MEMORY; } if (RT_FAILURE(rc2)) { while (i--) RTStrFree(apszArgsDup[i]); RTMemFree(apszArgsDup); return NULL; } return apszArgsDup; } /** * Frees a command argument vector. * * @param cArgs Number of arguments in argument vector. * @param papszArgs Pointer to argument vector to free. */ static void rtFtpCmdArgsFree(uint8_t cArgs, char **papszArgs) { while (cArgs--) RTStrFree(papszArgs[cArgs]); RTMemFree(papszArgs); } /** * Opens a data connection to the client. * * @returns VBox status code. * @param pDataConn Data connection to open. * @param pAddr Address for the data connection. * @param uPort Port for the data connection. */ static int rtFtpServerDataConnOpen(PRTFTPSERVERDATACONN pDataConn, PRTNETADDRIPV4 pAddr, uint16_t uPort) { LogFlowFuncEnter(); /** @todo Implement IPv6 handling here. */ char szAddress[32]; const ssize_t cchAdddress = RTStrPrintf2(szAddress, sizeof(szAddress), "%RU8.%RU8.%RU8.%RU8", pAddr->au8[0], pAddr->au8[1], pAddr->au8[2], pAddr->au8[3]); AssertReturn(cchAdddress > 0, VERR_NO_MEMORY); int rc = VINF_SUCCESS; /* Shut up MSVC. */ /* Try a bit harder if the data connection is not ready (yet). */ for (int i = 0; i < 10; i++) { rc = RTTcpClientConnect(szAddress, uPort, &pDataConn->hSocket); if (RT_SUCCESS(rc)) break; RTThreadSleep(100); } LogFlowFuncLeaveRC(rc); return rc; } /** * Closes a data connection to the client. * * @returns VBox status code. * @param pDataConn Data connection to close. */ static int rtFtpServerDataConnClose(PRTFTPSERVERDATACONN pDataConn) { int rc = VINF_SUCCESS; if (pDataConn->hSocket != NIL_RTSOCKET) { LogFlowFuncEnter(); rtFtpServerDataConnFlush(pDataConn); rc = RTTcpClientClose(pDataConn->hSocket); pDataConn->hSocket = NIL_RTSOCKET; } LogFlowFuncLeaveRC(rc); return rc; } /** * Writes data to the data connection. * * @returns VBox status code. * @param pDataConn Data connection to write to. * @param pvData Data to write. * @param cbData Size (in bytes) of data to write. * @param pcbWritten How many bytes were written. Optional. */ static int rtFtpServerDataConnWrite(PRTFTPSERVERDATACONN pDataConn, const void *pvData, size_t cbData, size_t *pcbWritten) { int rc = RTTcpWrite(pDataConn->hSocket, pvData, cbData); if (RT_SUCCESS(rc)) { if (pcbWritten) *pcbWritten = cbData; } return rc; } /** * Flushes a data connection. * * @returns VBox status code. * @param pDataConn Data connection to flush. */ static int rtFtpServerDataConnFlush(PRTFTPSERVERDATACONN pDataConn) { int rc = VINF_SUCCESS; size_t cbUsed = RTCircBufUsed(pDataConn->pCircBuf); while (cbUsed) { void *pvBlock; size_t cbBlock; RTCircBufAcquireReadBlock(pDataConn->pCircBuf, cbUsed, &pvBlock, &cbBlock); if (cbBlock) { size_t cbWritten = 0; rc = rtFtpServerDataConnWrite(pDataConn, pvBlock, cbBlock, &cbWritten); if (RT_SUCCESS(rc)) { AssertBreak(cbUsed >= cbWritten); cbUsed -= cbWritten; } RTCircBufReleaseReadBlock(pDataConn->pCircBuf, cbWritten); if (RT_FAILURE(rc)) break; } } return rc; } /** * Checks if flushing a data connection is necessary, and if so, flush it. * * @returns VBox status code. * @param pDataConn Data connection to check / do flushing for. */ static int rtFtpServerDataCheckFlush(PRTFTPSERVERDATACONN pDataConn) { int rc = VINF_SUCCESS; size_t cbUsed = RTCircBufUsed(pDataConn->pCircBuf); if (cbUsed >= _4K) /** @todo Make this more dynamic. */ { rc = rtFtpServerDataConnFlush(pDataConn); } return rc; } /** * Adds new data for a data connection to be sent. * * @returns VBox status code. * @param pDataConn Data connection to add new data to. * @param pvData Pointer to data to add. * @param cbData Size (in bytes) of data to add. */ static int rtFtpServerDataConnAddData(PRTFTPSERVERDATACONN pDataConn, const void *pvData, size_t cbData) { AssertReturn(cbData <= RTCircBufFree(pDataConn->pCircBuf), VERR_BUFFER_OVERFLOW); int rc = VINF_SUCCESS; size_t cbToWrite = cbData; do { void *pvBlock; size_t cbBlock; RTCircBufAcquireWriteBlock(pDataConn->pCircBuf, cbToWrite, &pvBlock, &cbBlock); if (cbBlock) { AssertBreak(cbData >= cbBlock); memcpy(pvBlock, pvData, cbBlock); AssertBreak(cbToWrite >= cbBlock); cbToWrite -= cbBlock; RTCircBufReleaseWriteBlock(pDataConn->pCircBuf, cbBlock); } } while (cbToWrite); if (RT_SUCCESS(rc)) rc = rtFtpServerDataCheckFlush(pDataConn); return rc; } /** * Does a printf-style write on a data connection. * * @returns VBox status code. * @param pDataConn Data connection to write to. * @param pszFormat Format string to send. No (terminal) termination added. */ static int rtFtpServerDataConnPrintf(PRTFTPSERVERDATACONN pDataConn, const char *pszFormat, ...) { va_list args; va_start(args, pszFormat); char *pszFmt = NULL; const int cch = RTStrAPrintfV(&pszFmt, pszFormat, args); va_end(args); AssertReturn(cch > 0, VERR_NO_MEMORY); char *pszMsg = NULL; int rc = RTStrAAppend(&pszMsg, pszFmt); AssertRCReturn(rc, rc); RTStrFree(pszFmt); rc = rtFtpServerDataConnAddData(pDataConn, pszMsg, strlen(pszMsg)); RTStrFree(pszMsg); return rc; } /** * Data connection thread for writing (sending) a file to the client. * * @returns VBox status code. * @param ThreadSelf Thread handle. Unused at the moment. * @param pvUser Pointer to user-provided data. Of type PRTFTPSERVERCLIENT. */ static DECLCALLBACK(int) rtFtpServerDataConnFileWriteThread(RTTHREAD ThreadSelf, void *pvUser) { RT_NOREF(ThreadSelf); PRTFTPSERVERCLIENT pClient = (PRTFTPSERVERCLIENT)pvUser; AssertPtr(pClient); PRTFTPSERVERDATACONN pDataConn = pClient->pDataConn; AssertPtr(pDataConn); LogFlowFuncEnter(); uint32_t cbBuf = _64K; /** @todo Improve this. */ void *pvBuf = RTMemAlloc(cbBuf); if (!pvBuf) return VERR_NO_MEMORY; int rc; /* Set start indicator. */ pDataConn->fStarted = true; RTThreadUserSignal(RTThreadSelf()); AssertPtr(pDataConn->papszArgs); const char *pszFile = pDataConn->papszArgs[0]; AssertPtr(pszFile); void *pvHandle = NULL; /* Opaque handle known to the actual implementation. */ RTFTPSERVER_HANDLE_CALLBACK_VA(pfnOnFileOpen, pszFile, RTFILE_O_READ | RTFILE_O_OPEN | RTFILE_O_DENY_WRITE, &pvHandle); if (RT_SUCCESS(rc)) { LogFlowFunc(("Transfer started\n")); do { size_t cbRead = 0; RTFTPSERVER_HANDLE_CALLBACK_VA(pfnOnFileRead, pvHandle, pvBuf, cbBuf, &cbRead); if ( RT_SUCCESS(rc) && cbRead) { rc = rtFtpServerDataConnWrite(pDataConn, pvBuf, cbRead, NULL /* pcbWritten */); } if ( !cbRead || ASMAtomicReadBool(&pDataConn->fStop)) { break; } } while (RT_SUCCESS(rc)); RTFTPSERVER_HANDLE_CALLBACK_VA(pfnOnFileClose, pvHandle); LogFlowFunc(("Transfer done\n")); } RTMemFree(pvBuf); pvBuf = NULL; pDataConn->fStopped = true; pDataConn->rc = rc; LogFlowFuncLeaveRC(rc); return rc; } /** * Creates a data connection. * * @returns VBox status code. * @param pClient Client to create data connection for. * @param ppDataConn Where to return the (allocated) data connection. */ static int rtFtpServerDataConnCreate(PRTFTPSERVERCLIENT pClient, PRTFTPSERVERDATACONN *ppDataConn) { if (pClient->pDataConn) return VERR_FTP_DATA_CONN_LIMIT_REACHED; PRTFTPSERVERDATACONN pDataConn = (PRTFTPSERVERDATACONN)RTMemAllocZ(sizeof(RTFTPSERVERDATACONN)); if (!pDataConn) return VERR_NO_MEMORY; rtFtpServerDataConnReset(pDataConn); pDataConn->pClient = pClient; /* Use the last configured addr + port. */ pDataConn->Addr = pClient->DataConnAddr; pDataConn->uPort = pClient->uDataConnPort; int rc = RTCircBufCreate(&pDataConn->pCircBuf, _16K); /** @todo Some random value; improve. */ if (RT_SUCCESS(rc)) { *ppDataConn = pDataConn; } LogFlowFuncLeaveRC(VINF_SUCCESS); return rc; } /** * Starts a data connection. * * @returns VBox status code. * @param pDataConn Data connection to start. * @param pfnThread Thread function for the data connection to use. * @param cArgs Number of arguments. * @param apszArgs Array of arguments. */ static int rtFtpServerDataConnStart(PRTFTPSERVERDATACONN pDataConn, PFNRTTHREAD pfnThread, uint8_t cArgs, const char * const *apszArgs) { AssertPtrReturn(pDataConn, VERR_INVALID_POINTER); AssertPtrReturn(pfnThread, VERR_INVALID_POINTER); AssertReturn(!pDataConn->fStarted, VERR_WRONG_ORDER); AssertReturn(!pDataConn->fStop, VERR_WRONG_ORDER); AssertReturn(!pDataConn->fStopped, VERR_WRONG_ORDER); int rc = VINF_SUCCESS; if (cArgs) { pDataConn->papszArgs = rtFtpCmdArgsDup(cArgs, apszArgs); if (!pDataConn->papszArgs) rc = VERR_NO_MEMORY; } if (RT_SUCCESS(rc)) { pDataConn->cArgs = cArgs; rc = rtFtpServerDataConnOpen(pDataConn, &pDataConn->Addr, pDataConn->uPort); if (RT_SUCCESS(rc)) { rc = RTThreadCreate(&pDataConn->hThread, pfnThread, pDataConn->pClient, 0, RTTHREADTYPE_DEFAULT, RTTHREADFLAGS_WAITABLE, "ftpdata"); if (RT_SUCCESS(rc)) { int rc2 = RTThreadUserWait(pDataConn->hThread, 30 * 1000 /* Timeout in ms */); AssertRC(rc2); if (!pDataConn->fStarted) /* Did the thread indicate that it started correctly? */ rc = VERR_FTP_DATA_CONN_INIT_FAILED; } if (RT_FAILURE(rc)) rtFtpServerDataConnClose(pDataConn); } } if (RT_FAILURE(rc)) { rtFtpCmdArgsFree(pDataConn->cArgs, pDataConn->papszArgs); pDataConn->cArgs = 0; pDataConn->papszArgs = NULL; } LogFlowFuncLeaveRC(rc); return rc; } /** * Stops a data connection. * * @returns VBox status code. * @param pDataConn Data connection to stop. */ static int rtFtpServerDataConnStop(PRTFTPSERVERDATACONN pDataConn) { if (!pDataConn) return VINF_SUCCESS; LogFlowFuncEnter(); int rc = VINF_SUCCESS; if (pDataConn->hThread != NIL_RTTHREAD) { /* Set stop indicator. */ pDataConn->fStop = true; int rcThread = VERR_WRONG_ORDER; rc = RTThreadWait(pDataConn->hThread, 30 * 1000 /* Timeout in ms */, &rcThread); } if (RT_SUCCESS(rc)) rtFtpServerDataConnClose(pDataConn); LogFlowFuncLeaveRC(rc); return rc; } /** * Destroys a data connection. * * @param pDataConn Data connection to destroy. The pointer is not valid anymore after successful return. */ static void rtFtpServerDataConnDestroy(PRTFTPSERVERDATACONN pDataConn) { if (!pDataConn) return; LogFlowFuncEnter(); rtFtpServerDataConnClose(pDataConn); rtFtpCmdArgsFree(pDataConn->cArgs, pDataConn->papszArgs); RTCircBufDestroy(pDataConn->pCircBuf); RTMemFree(pDataConn); pDataConn = NULL; LogFlowFuncLeave(); return; } /** * Resets a data connection structure. * * @param pDataConn Data connection structure to reset. */ static void rtFtpServerDataConnReset(PRTFTPSERVERDATACONN pDataConn) { LogFlowFuncEnter(); pDataConn->hSocket = NIL_RTSOCKET; pDataConn->uPort = 20; /* Default port to use. */ pDataConn->hThread = NIL_RTTHREAD; pDataConn->fStarted = false; pDataConn->fStop = false; pDataConn->fStopped = false; pDataConn->rc = VERR_IPE_UNINITIALIZED_STATUS; } /********************************************************************************************************************************* * Command Protocol Handlers * *********************************************************************************************************************************/ static DECLCALLBACK(int) rtFtpServerHandleABOR(PRTFTPSERVERCLIENT pClient, uint8_t cArgs, const char * const *apszArgs) { RT_NOREF(cArgs, apszArgs); int rc = rtFtpServerDataConnClose(pClient->pDataConn); if (RT_SUCCESS(rc)) { rtFtpServerDataConnDestroy(pClient->pDataConn); pClient->pDataConn = NULL; rc = rtFtpServerSendReplyRc(pClient, RTFTPSERVER_REPLY_OKAY); } return rc; } static DECLCALLBACK(int) rtFtpServerHandleCDUP(PRTFTPSERVERCLIENT pClient, uint8_t cArgs, const char * const *apszArgs) { RT_NOREF(cArgs, apszArgs); int rc; RTFTPSERVER_HANDLE_CALLBACK(pfnOnPathUp); if (RT_SUCCESS(rc)) { const size_t cbPath = sizeof(char) * RTFTPSERVER_MAX_PATH; char *pszPath = RTStrAlloc(cbPath); if (pszPath) { RTFTPSERVER_HANDLE_CALLBACK_VA(pfnOnPathGetCurrent, pszPath, cbPath); if (RT_SUCCESS(rc)) rc = rtFtpSetCWD(&pClient->State, pszPath); RTStrFree(pszPath); rc = rtFtpServerSendReplyRc(pClient, RTFTPSERVER_REPLY_OKAY); } else rc = VERR_NO_MEMORY; } if (RT_FAILURE(rc)) { int rc2 = rtFtpServerSendReplyRc(pClient, RTFTPSERVER_REPLY_CONN_REQ_FILE_ACTION_NOT_TAKEN); AssertRC(rc2); } return rc; } static DECLCALLBACK(int) rtFtpServerHandleCWD(PRTFTPSERVERCLIENT pClient, uint8_t cArgs, const char * const *apszArgs) { if (cArgs != 1) return VERR_INVALID_PARAMETER; int rc; const char *pszPath = apszArgs[0]; if (!rtFtpServerPathIsValid(pszPath, false /* fIsAbsolute */)) return VERR_INVALID_PARAMETER; RTFTPSERVER_HANDLE_CALLBACK_VA(pfnOnPathSetCurrent, pszPath); if (RT_SUCCESS(rc)) rc = rtFtpSetCWD(&pClient->State, pszPath); return rtFtpServerSendReplyRc(pClient, RT_SUCCESS(rc) ? RTFTPSERVER_REPLY_OKAY : RTFTPSERVER_REPLY_CONN_REQ_FILE_ACTION_NOT_TAKEN); } static DECLCALLBACK(int) rtFtpServerHandleFEAT(PRTFTPSERVERCLIENT pClient, uint8_t cArgs, const char * const *apszArgs) { RT_NOREF(cArgs, apszArgs); int rc = rtFtpServerSendReplyStr(pClient, "211-BEGIN Features:"); if (RT_SUCCESS(rc)) { rc = rtFtpServerSendReplyStr(pClient, RTFTPSERVER_FEATURES_STRING); if (RT_SUCCESS(rc)) rc = rtFtpServerSendReplyStr(pClient, "211 END Features"); } return rc; } /** * Formats the given user ID according to the specified options. * * @returns pszDst * @param uid The UID to format. * @param pszOwner The owner returned by the FS. * @param pszDst The output buffer. * @param cbDst The output buffer size. */ static const char *rtFtpServerDecimalFormatOwner(RTUID uid, const char *pszOwner, char *pszDst, size_t cbDst) { if (pszOwner) { RTStrCopy(pszDst, cbDst, pszOwner); return pszDst; } if (uid == NIL_RTUID) return ""; RTStrFormatU64(pszDst, cbDst, uid, 10, 0, 0, 0); return pszDst; } /** * Formats the given group ID according to the specified options. * * @returns pszDst * @param gid The GID to format. * @param pszGroup The group returned by the FS. * @param pszDst The output buffer. * @param cbDst The output buffer size. */ static const char *rtFtpServerDecimalFormatGroup(RTGID gid, const char *pszGroup, char *pszDst, size_t cbDst) { if (pszGroup) { RTStrCopy(pszDst, cbDst, pszGroup); return pszDst; } if (gid == NIL_RTGID) return ""; RTStrFormatU64(pszDst, cbDst, gid, 10, 0, 0, 0); return pszDst; } /** * Format file size. */ static const char *rtFtpServerFormatSize(uint64_t cb, char *pszDst, size_t cbDst) { RTStrFormatU64(pszDst, cbDst, cb, 10, 0, 0, 0); return pszDst; } /** * Formats the given timestamp according to (non-standardized) FTP LIST command. * * @returns pszDst * @param pTimestamp The timestamp to format. * @param pszDst The output buffer. * @param cbDst The output buffer size. */ static const char *rtFtpServerFormatTimestamp(PCRTTIMESPEC pTimestamp, char *pszDst, size_t cbDst) { RTTIME Time; RTTimeExplode(&Time, pTimestamp); /* Calc the UTC offset part. */ int32_t offUtc = Time.offUTC; Assert(offUtc <= 840 && offUtc >= -840); char chSign; if (offUtc >= 0) chSign = '+'; else { chSign = '-'; offUtc = -offUtc; } uint32_t offUtcHour = (uint32_t)offUtc / 60; uint32_t offUtcMinute = (uint32_t)offUtc % 60; /** @todo Cache this. */ RTTIMESPEC TimeSpecNow; RTTimeNow(&TimeSpecNow); RTTIME TimeNow; RTTimeExplode(&TimeNow, &TimeSpecNow); /* Only include the year if it's not the same year as today. */ if (TimeNow.i32Year != Time.i32Year) { RTStrPrintf(pszDst, cbDst, "%s %02RU8 %5RU32", g_apszMonths[Time.u8Month], Time.u8MonthDay, Time.i32Year); } else /* ... otherwise include the (rough) time (as GMT). */ { RTStrPrintf(pszDst, cbDst, "%s %02RU8 %02RU32:%02RU32", g_apszMonths[Time.u8Month], Time.u8MonthDay, offUtcHour, offUtcMinute); } return pszDst; } /** * Format name, i.e. escape, hide, quote stuff. */ static const char *rtFtpServerFormatName(const char *pszName, char *pszDst, size_t cbDst) { /** @todo implement name formatting. */ RT_NOREF(pszDst, cbDst); return pszName; } /** * Figures out the length for a 32-bit number when formatted as decimal. * @returns Number of digits. * @param uValue The number. */ DECLINLINE(size_t) rtFtpServerDecimalFormatLengthU32(uint32_t uValue) { if (uValue < 10) return 1; if (uValue < 100) return 2; if (uValue < 1000) return 3; if (uValue < 10000) return 4; if (uValue < 100000) return 5; if (uValue < 1000000) return 6; if (uValue < 10000000) return 7; if (uValue < 100000000) return 8; if (uValue < 1000000000) return 9; return 10; } /** * Allocates a new directory collection. * * @returns The collection allocated. */ static PRTFTPDIRCOLLECTION rtFtpServerDataConnDirCollAlloc(void) { return (PRTFTPDIRCOLLECTION)RTMemAllocZ(sizeof(RTFTPDIRCOLLECTION)); } /** * Frees a directory collection and its entries. * * @param pCollection The collection to free. */ static void rtFtpServerDataConnDirCollFree(PRTFTPDIRCOLLECTION pCollection) { PPRTFTPDIRENTRY papEntries = pCollection->papEntries; size_t j = pCollection->cEntries; while (j-- > 0) { RTMemFree(papEntries[j]); papEntries[j] = NULL; } RTMemFree(papEntries); pCollection->papEntries = NULL; pCollection->cEntries = 0; pCollection->cEntriesAllocated = 0; RTMemFree(pCollection); } /** * Adds one entry to a collection. * * @returns VBox status code. * @param pCollection The collection to add entry to. * @param pszEntry The entry name. * @param pInfo The entry info. * @param pszOwner The owner name if available, otherwise NULL. * @param pszGroup The group anme if available, otherwise NULL. * @param pszTarget The symbolic link target if applicable and * available, otherwise NULL. */ static int rtFtpServerDataConnDirCollAddEntry(PRTFTPDIRCOLLECTION pCollection, const char *pszEntry, PRTFSOBJINFO pInfo, const char *pszOwner, const char *pszGroup, const char *pszTarget) { /* Filter out entries we don't want to report to the client, even if they were reported by the actual implementation. */ if ( !RTStrCmp(pszEntry, ".") || !RTStrCmp(pszEntry, "..")) { return VINF_SUCCESS; } /* Anything else besides files and directores is not allowed; just don't show them at all for the moment. */ switch (pInfo->Attr.fMode & RTFS_TYPE_MASK) { case RTFS_TYPE_DIRECTORY: RT_FALL_THROUGH(); case RTFS_TYPE_FILE: break; default: return VINF_SUCCESS; } /* Make sure there is space in the collection for the new entry. */ if (pCollection->cEntries >= pCollection->cEntriesAllocated) { size_t cNew = pCollection->cEntriesAllocated ? pCollection->cEntriesAllocated * 2 : 16; void *pvNew = RTMemRealloc(pCollection->papEntries, cNew * sizeof(pCollection->papEntries[0])); if (!pvNew) return VERR_NO_MEMORY; pCollection->papEntries = (PPRTFTPDIRENTRY)pvNew; pCollection->cEntriesAllocated = cNew; } /* Create and insert a new entry. */ size_t const cchEntry = strlen(pszEntry); size_t const cbOwner = pszOwner ? strlen(pszOwner) + 1 : 0; size_t const cbGroup = pszGroup ? strlen(pszGroup) + 1 : 0; size_t const cbTarget = pszTarget ? strlen(pszTarget) + 1 : 0; size_t const cbEntry = RT_UOFFSETOF_DYN(RTFTPDIRENTRY, szName[cchEntry + 1 + cbOwner + cbGroup + cbTarget]); PRTFTPDIRENTRY pEntry = (PRTFTPDIRENTRY)RTMemAlloc(cbEntry); if (pEntry) { pEntry->Info = *pInfo; pEntry->pszTarget = NULL; /** @todo symbolic links. */ pEntry->pszOwner = NULL; pEntry->pszGroup = NULL; pEntry->cchName = cchEntry; memcpy(pEntry->szName, pszEntry, cchEntry); pEntry->szName[cchEntry] = '\0'; char *psz = &pEntry->szName[cchEntry + 1]; if (pszTarget) { pEntry->pszTarget = psz; memcpy(psz, pszTarget, cbTarget); psz += cbTarget; } if (pszOwner) { pEntry->pszOwner = psz; memcpy(psz, pszOwner, cbOwner); psz += cbOwner; } if (pszGroup) { pEntry->pszGroup = psz; memcpy(psz, pszGroup, cbGroup); } pCollection->papEntries[pCollection->cEntries++] = pEntry; pCollection->cbTotalAllocated += pEntry->Info.cbAllocated; pCollection->cbTotalFiles += pEntry->Info.cbObject; return VINF_SUCCESS; } return VERR_NO_MEMORY; } /** @callback_method_impl{FNRTSORTCMP, Name} */ static DECLCALLBACK(int) rtFtpServerCollEntryCmpName(void const *pvElement1, void const *pvElement2, void *pvUser) { RT_NOREF(pvUser); PRTFTPDIRENTRY pEntry1 = (PRTFTPDIRENTRY)pvElement1; PRTFTPDIRENTRY pEntry2 = (PRTFTPDIRENTRY)pvElement2; return RTStrCmp(pEntry1->szName, pEntry2->szName); } /** @callback_method_impl{FNRTSORTCMP, Dirs first + Name} */ static DECLCALLBACK(int) rtFtpServerCollEntryCmpDirFirstName(void const *pvElement1, void const *pvElement2, void *pvUser) { RT_NOREF(pvUser); PRTFTPDIRENTRY pEntry1 = (PRTFTPDIRENTRY)pvElement1; PRTFTPDIRENTRY pEntry2 = (PRTFTPDIRENTRY)pvElement2; int iDiff = !RTFS_IS_DIRECTORY(pEntry1->Info.Attr.fMode) - !RTFS_IS_DIRECTORY(pEntry2->Info.Attr.fMode); if (!iDiff) iDiff = rtFtpServerCollEntryCmpName(pEntry1, pEntry2, pvUser); return iDiff; } /** * Sorts a given directory collection according to the FTP server's LIST style. * * @param pCollection Collection to sort. */ static void rtFtpServerCollSort(PRTFTPDIRCOLLECTION pCollection) { PFNRTSORTCMP pfnCmp = rtFtpServerCollEntryCmpDirFirstName; if (pfnCmp) RTSortApvShell((void **)pCollection->papEntries, pCollection->cEntries, pfnCmp, NULL); } /** * Writes a directory collection to a specific data connection. * * @returns VBox status code. * @param pDataConn Data connection to write directory collection to. * @param pCollection Collection to write. * @param pszTmp Temporary buffer used for writing. * @param cbTmp Size (in bytes) of temporary buffer used for writing. */ static int rtFtpServerDataConnDirCollWrite(PRTFTPSERVERDATACONN pDataConn, PRTFTPDIRCOLLECTION pCollection, char *pszTmp, size_t cbTmp) { size_t cchSizeCol = 4; size_t cchLinkCol = 1; size_t cchUidCol = 1; size_t cchGidCol = 1; size_t i = pCollection->cEntries; while (i-- > 0) { PRTFTPDIRENTRY pEntry = pCollection->papEntries[i]; rtFtpServerFormatSize(pEntry->Info.cbObject, pszTmp, cbTmp); size_t cchTmp = strlen(pszTmp); if (cchTmp > cchSizeCol) cchSizeCol = cchTmp; cchTmp = rtFtpServerDecimalFormatLengthU32(pEntry->Info.Attr.u.Unix.cHardlinks) + 1; if (cchTmp > cchLinkCol) cchLinkCol = cchTmp; rtFtpServerDecimalFormatOwner(pEntry->Info.Attr.u.Unix.uid, pEntry->pszOwner, pszTmp, cbTmp); cchTmp = strlen(pszTmp); if (cchTmp > cchUidCol) cchUidCol = cchTmp; rtFtpServerDecimalFormatGroup(pEntry->Info.Attr.u.Unix.gid, pEntry->pszGroup, pszTmp, cbTmp); cchTmp = strlen(pszTmp); if (cchTmp > cchGidCol) cchGidCol = cchTmp; } size_t offTime = RT_UOFFSETOF(RTFTPDIRENTRY, Info.ModificationTime); /* * Display the entries. */ for (i = 0; i < pCollection->cEntries; i++) { PRTFTPDIRENTRY pEntry = pCollection->papEntries[i]; RTFMODE fMode = pEntry->Info.Attr.fMode; switch (fMode & RTFS_TYPE_MASK) { case RTFS_TYPE_FIFO: rtFtpServerDataConnPrintf(pDataConn, "f"); break; case RTFS_TYPE_DEV_CHAR: rtFtpServerDataConnPrintf(pDataConn, "c"); break; case RTFS_TYPE_DIRECTORY: rtFtpServerDataConnPrintf(pDataConn, "d"); break; case RTFS_TYPE_DEV_BLOCK: rtFtpServerDataConnPrintf(pDataConn, "b"); break; case RTFS_TYPE_FILE: rtFtpServerDataConnPrintf(pDataConn, "-"); break; case RTFS_TYPE_SYMLINK: rtFtpServerDataConnPrintf(pDataConn, "l"); break; case RTFS_TYPE_SOCKET: rtFtpServerDataConnPrintf(pDataConn, "s"); break; case RTFS_TYPE_WHITEOUT: rtFtpServerDataConnPrintf(pDataConn, "w"); break; default: rtFtpServerDataConnPrintf(pDataConn, "?"); AssertFailed(); break; } rtFtpServerDataConnPrintf(pDataConn, "%c%c%c", fMode & RTFS_UNIX_IRUSR ? 'r' : '-', fMode & RTFS_UNIX_IWUSR ? 'w' : '-', fMode & RTFS_UNIX_IXUSR ? 'x' : '-'); rtFtpServerDataConnPrintf(pDataConn, "%c%c%c", fMode & RTFS_UNIX_IRGRP ? 'r' : '-', fMode & RTFS_UNIX_IWGRP ? 'w' : '-', fMode & RTFS_UNIX_IXGRP ? 'x' : '-'); rtFtpServerDataConnPrintf(pDataConn, "%c%c%c", fMode & RTFS_UNIX_IROTH ? 'r' : '-', fMode & RTFS_UNIX_IWOTH ? 'w' : '-', fMode & RTFS_UNIX_IXOTH ? 'x' : '-'); rtFtpServerDataConnPrintf(pDataConn, " %*u", cchLinkCol, pEntry->Info.Attr.u.Unix.cHardlinks); if (cchUidCol) rtFtpServerDataConnPrintf(pDataConn, " %*s", cchUidCol, rtFtpServerDecimalFormatOwner(pEntry->Info.Attr.u.Unix.uid, pEntry->pszOwner, pszTmp, cbTmp)); if (cchGidCol) rtFtpServerDataConnPrintf(pDataConn, " %*s", cchGidCol, rtFtpServerDecimalFormatGroup(pEntry->Info.Attr.u.Unix.gid, pEntry->pszGroup, pszTmp, cbTmp)); rtFtpServerDataConnPrintf(pDataConn, "%*s", cchSizeCol, rtFtpServerFormatSize(pEntry->Info.cbObject, pszTmp, cbTmp)); PCRTTIMESPEC pTime = (PCRTTIMESPEC)((uintptr_t)pEntry + offTime); rtFtpServerDataConnPrintf(pDataConn," %s", rtFtpServerFormatTimestamp(pTime, pszTmp, cbTmp)); rtFtpServerDataConnPrintf(pDataConn," %s\r\n", rtFtpServerFormatName(pEntry->szName, pszTmp, cbTmp)); } return VINF_SUCCESS; } /** * Thread for handling the LIST command's output in a separate data connection. * * @returns VBox status code. * @param ThreadSelf Thread handle. Unused. * @param pvUser User-provided arguments. Of type PRTFTPSERVERCLIENT. */ static DECLCALLBACK(int) rtFtpServerDataConnListThread(RTTHREAD ThreadSelf, void *pvUser) { RT_NOREF(ThreadSelf); PRTFTPSERVERCLIENT pClient = (PRTFTPSERVERCLIENT)pvUser; AssertPtr(pClient); PRTFTPSERVERDATACONN pDataConn = pClient->pDataConn; AssertPtr(pDataConn); LogFlowFuncEnter(); int rc; char szTmp[RTPATH_MAX * 2]; PRTFTPDIRCOLLECTION pColl = rtFtpServerDataConnDirCollAlloc(); AssertPtrReturn(pColl, VERR_NO_MEMORY); /* Set start indicator. */ pDataConn->fStarted = true; RTThreadUserSignal(RTThreadSelf()); /* The first argument might indicate a directory to list. * If no argument is given, the implementation must use the last directory set. */ char *pszPath = RTStrDup( pDataConn->cArgs == 1 ? pDataConn->papszArgs[0] : pDataConn->pClient->State.pszCWD); /** @todo Needs locking. */ AssertPtrReturn(pszPath, VERR_NO_MEMORY); /* The paths already have been validated in the actual command handlers. */ void *pvHandle = NULL; /* Shut up MSVC. */ RTFTPSERVER_HANDLE_CALLBACK_VA(pfnOnDirOpen, pszPath, &pvHandle); for (;;) { RTFSOBJINFO objInfo; RT_ZERO(objInfo); char *pszEntry = NULL; char *pszOwner = NULL; char *pszGroup = NULL; char *pszTarget = NULL; RTFTPSERVER_HANDLE_CALLBACK_VA(pfnOnDirRead, pvHandle, &pszEntry, &objInfo, &pszOwner, &pszGroup, &pszTarget); if (RT_SUCCESS(rc)) { int rc2 = rtFtpServerDataConnDirCollAddEntry(pColl, pszEntry, &objInfo, pszOwner, pszGroup, pszTarget); RTStrFree(pszEntry); pszEntry = NULL; RTStrFree(pszOwner); pszOwner = NULL; RTStrFree(pszGroup); pszGroup = NULL; RTStrFree(pszTarget); pszTarget = NULL; if (RT_SUCCESS(rc)) rc = rc2; } else { if (rc == VERR_NO_MORE_FILES) { rc = VINF_SUCCESS; break; } } if (RT_FAILURE(rc)) break; if (ASMAtomicReadBool(&pDataConn->fStop)) break; } RTFTPSERVER_HANDLE_CALLBACK_VA(pfnOnDirClose, pvHandle); pvHandle = NULL; rtFtpServerCollSort(pColl); if (RT_SUCCESS(rc)) { int rc2 = rtFtpServerDataConnDirCollWrite(pDataConn, pColl, szTmp, sizeof(szTmp)); AssertRC(rc2); } rtFtpServerDataConnDirCollFree(pColl); RTStrFree(pszPath); pDataConn->fStopped = true; pDataConn->rc = rc; LogFlowFuncLeaveRC(rc); return rc; } static DECLCALLBACK(int) rtFtpServerHandleLIST(PRTFTPSERVERCLIENT pClient, uint8_t cArgs, const char * const *apszArgs) { /* If no argument is given, use the server's CWD as the path. */ const char *pszPath = cArgs ? apszArgs[0] : pClient->State.pszCWD; AssertPtr(pszPath); int rc = VINF_SUCCESS; if (!rtFtpServerPathIsValid(pszPath, false /* fIsAbsolute */)) { int rc2 = rtFtpServerSendReplyRc(pClient, RTFTPSERVER_REPLY_CONN_REQ_FILE_ACTION_NOT_TAKEN); AssertRC(rc2); } else { RTFTPSERVER_HANDLE_CALLBACK_VA(pfnOnFileStat, pszPath, NULL /* PRTFSOBJINFO */); if (RT_SUCCESS(rc)) { if (pClient->pDataConn == NULL) { rc = rtFtpServerDataConnCreate(pClient, &pClient->pDataConn); if (RT_SUCCESS(rc)) rc = rtFtpServerDataConnStart(pClient->pDataConn, rtFtpServerDataConnListThread, cArgs, apszArgs); int rc2 = rtFtpServerSendReplyRc( pClient, RT_SUCCESS(rc) ? RTFTPSERVER_REPLY_DATACONN_ALREADY_OPEN : RTFTPSERVER_REPLY_CANT_OPEN_DATA_CONN); AssertRC(rc2); } else { int rc2 = rtFtpServerSendReplyRc(pClient, RTFTPSERVER_REPLY_DATACONN_ALREADY_OPEN); AssertRC(rc2); } } else { int rc2 = rtFtpServerSendReplyRc(pClient, RTFTPSERVER_REPLY_CONN_REQ_FILE_ACTION_NOT_TAKEN); AssertRC(rc2); } } return rc; } static DECLCALLBACK(int) rtFtpServerHandleMODE(PRTFTPSERVERCLIENT pClient, uint8_t cArgs, const char * const *apszArgs) { RT_NOREF(pClient, cArgs, apszArgs); /** @todo Anything to do here? */ return VINF_SUCCESS; } static DECLCALLBACK(int) rtFtpServerHandleNOOP(PRTFTPSERVERCLIENT pClient, uint8_t cArgs, const char * const *apszArgs) { RT_NOREF(cArgs, apszArgs); /* Save timestamp of last command sent. */ pClient->State.tsLastCmdMs = RTTimeMilliTS(); return rtFtpServerSendReplyRc(pClient, RTFTPSERVER_REPLY_OKAY); } static DECLCALLBACK(int) rtFtpServerHandlePASS(PRTFTPSERVERCLIENT pClient, uint8_t cArgs, const char * const *apszArgs) { if (cArgs != 1) return rtFtpServerSendReplyRc(pClient, RTFTPSERVER_REPLY_ERROR_INVALID_PARAMETERS); const char *pszPassword = apszArgs[0]; AssertPtrReturn(pszPassword, VERR_INVALID_PARAMETER); int rc = rtFtpServerAuthenticate(pClient, pClient->State.pszUser, pszPassword); if (RT_SUCCESS(rc)) { rc = rtFtpServerSendReplyRc(pClient, RTFTPSERVER_REPLY_LOGGED_IN_PROCEED); } else { pClient->State.cFailedLoginAttempts++; int rc2 = rtFtpServerSendReplyRc(pClient, RTFTPSERVER_REPLY_NOT_LOGGED_IN); if (RT_SUCCESS(rc)) rc = rc2; } return rc; } static DECLCALLBACK(int) rtFtpServerHandlePORT(PRTFTPSERVERCLIENT pClient, uint8_t cArgs, const char * const *apszArgs) { if (cArgs != 1) return rtFtpServerSendReplyRc(pClient, RTFTPSERVER_REPLY_ERROR_INVALID_PARAMETERS); RTFTPSERVER_REPLY rcClient; int rc = rtFtpParseHostAndPort(apszArgs[0], &pClient->DataConnAddr, &pClient->uDataConnPort); if (RT_SUCCESS(rc)) rcClient = RTFTPSERVER_REPLY_OKAY; else rcClient = RTFTPSERVER_REPLY_ERROR_INVALID_PARAMETERS; int rc2 = rtFtpServerSendReplyRc(pClient, rcClient); if (RT_SUCCESS(rc)) rc = rc2; return rc; } static DECLCALLBACK(int) rtFtpServerHandlePWD(PRTFTPSERVERCLIENT pClient, uint8_t cArgs, const char * const *apszArgs) { RT_NOREF(cArgs, apszArgs); int rc; char szPWD[RTPATH_MAX]; RTFTPSERVER_HANDLE_CALLBACK_VA(pfnOnPathGetCurrent, szPWD, sizeof(szPWD)); if (RT_SUCCESS(rc)) rc = rtFtpServerSendReplyRcEx(pClient, RTFTPSERVER_REPLY_PATHNAME_OK, "\"%s\"", szPWD); /* See RFC 959, APPENDIX II. */ return rc; } static DECLCALLBACK(int) rtFtpServerHandleOPTS(PRTFTPSERVERCLIENT pClient, uint8_t cArgs, const char * const *apszArgs) { RT_NOREF(cArgs, apszArgs); int rc = VINF_SUCCESS; int rc2 = rtFtpServerSendReplyRc(pClient, RTFTPSERVER_REPLY_OKAY); if (RT_SUCCESS(rc)) rc = rc2; return rc; } static DECLCALLBACK(int) rtFtpServerHandleQUIT(PRTFTPSERVERCLIENT pClient, uint8_t cArgs, const char * const *apszArgs) { RT_NOREF(cArgs, apszArgs); int rc = VINF_SUCCESS; if (pClient->pDataConn) { rc = rtFtpServerDataConnClose(pClient->pDataConn); if (RT_SUCCESS(rc)) { rtFtpServerDataConnDestroy(pClient->pDataConn); pClient->pDataConn = NULL; } } int rc2 = rtFtpServerSendReplyRc(pClient, RTFTPSERVER_REPLY_OKAY); if (RT_SUCCESS(rc)) rc = rc2; return rc; } static DECLCALLBACK(int) rtFtpServerHandleRETR(PRTFTPSERVERCLIENT pClient, uint8_t cArgs, const char * const *apszArgs) { if (cArgs != 1) /* File name needs to be present. */ return VERR_INVALID_PARAMETER; int rc; const char *pszPath = apszArgs[0]; RTFTPSERVER_HANDLE_CALLBACK_VA(pfnOnFileStat, pszPath, NULL /* PRTFSOBJINFO */); if (RT_SUCCESS(rc)) { if (RT_SUCCESS(rc)) { if (pClient->pDataConn == NULL) { rc = rtFtpServerDataConnCreate(pClient, &pClient->pDataConn); if (RT_SUCCESS(rc)) rc = rtFtpServerDataConnStart(pClient->pDataConn, rtFtpServerDataConnFileWriteThread, cArgs, apszArgs); int rc2 = rtFtpServerSendReplyRc( pClient, RT_SUCCESS(rc) ? RTFTPSERVER_REPLY_DATACONN_ALREADY_OPEN : RTFTPSERVER_REPLY_CANT_OPEN_DATA_CONN); AssertRC(rc2); } else { int rc2 = rtFtpServerSendReplyRc(pClient, RTFTPSERVER_REPLY_DATACONN_ALREADY_OPEN); AssertRC(rc2); } } else { int rc2 = rtFtpServerSendReplyRc(pClient, RTFTPSERVER_REPLY_CONN_REQ_FILE_ACTION_NOT_TAKEN); AssertRC(rc2); } } if (RT_FAILURE(rc)) { int rc2 = rtFtpServerSendReplyRc(pClient, RTFTPSERVER_REPLY_REQ_ACTION_NOT_TAKEN); AssertRC(rc2); } return rc; } static DECLCALLBACK(int) rtFtpServerHandleSIZE(PRTFTPSERVERCLIENT pClient, uint8_t cArgs, const char * const *apszArgs) { if (cArgs != 1) return VERR_INVALID_PARAMETER; int rc; const char *pszPath = apszArgs[0]; uint64_t uSize = 0; RTFTPSERVER_HANDLE_CALLBACK_VA(pfnOnFileGetSize, pszPath, &uSize); if (RT_SUCCESS(rc)) { rc = rtFtpServerSendReplyStr(pClient, "213 %RU64\r\n", uSize); } else { int rc2 = rtFtpServerSendReplyRc(pClient, RTFTPSERVER_REPLY_REQ_ACTION_NOT_TAKEN); AssertRC(rc2); } return rc; } static DECLCALLBACK(int) rtFtpServerHandleSTAT(PRTFTPSERVERCLIENT pClient, uint8_t cArgs, const char * const *apszArgs) { if (cArgs != 1) return VERR_INVALID_PARAMETER; int rc; RTFSOBJINFO objInfo; RT_ZERO(objInfo); const char *pszPath = apszArgs[0]; RTFTPSERVER_HANDLE_CALLBACK_VA(pfnOnFileStat, pszPath, &objInfo); if (RT_SUCCESS(rc)) { char szFsObjInfo[_4K]; /** @todo Check this size. */ rc = rtFtpServerFsObjInfoToStr(&objInfo, szFsObjInfo, sizeof(szFsObjInfo)); if (RT_SUCCESS(rc)) { char szFsPathInfo[RTPATH_MAX + 16]; const ssize_t cchPathInfo = RTStrPrintf2(szFsPathInfo, sizeof(szFsPathInfo), " %2zu %s\n", strlen(pszPath), pszPath); if (cchPathInfo > 0) { rc = RTStrCat(szFsObjInfo, sizeof(szFsObjInfo), szFsPathInfo); if (RT_SUCCESS(rc)) rc = rtFtpServerSendReplyStr(pClient, szFsObjInfo); } else rc = VERR_BUFFER_OVERFLOW; } } if (RT_FAILURE(rc)) { int rc2 = rtFtpServerSendReplyRc(pClient, RTFTPSERVER_REPLY_REQ_ACTION_NOT_TAKEN); AssertRC(rc2); } return rc; } static DECLCALLBACK(int) rtFtpServerHandleSTRU(PRTFTPSERVERCLIENT pClient, uint8_t cArgs, const char * const *apszArgs) { if (cArgs != 1) return VERR_INVALID_PARAMETER; const char *pszType = apszArgs[0]; int rc; if (!RTStrICmp(pszType, "F")) { pClient->State.enmStructType = RTFTPSERVER_STRUCT_TYPE_FILE; rc = rtFtpServerSendReplyRc(pClient, RTFTPSERVER_REPLY_OKAY); } else rc = VERR_NOT_IMPLEMENTED; return rc; } static DECLCALLBACK(int) rtFtpServerHandleSYST(PRTFTPSERVERCLIENT pClient, uint8_t cArgs, const char * const *apszArgs) { RT_NOREF(cArgs, apszArgs); char szOSInfo[64]; int rc = RTSystemQueryOSInfo(RTSYSOSINFO_PRODUCT, szOSInfo, sizeof(szOSInfo)); if (RT_SUCCESS(rc)) rc = rtFtpServerSendReplyStr(pClient, "215 %s", szOSInfo); return rc; } static DECLCALLBACK(int) rtFtpServerHandleTYPE(PRTFTPSERVERCLIENT pClient, uint8_t cArgs, const char * const *apszArgs) { if (cArgs != 1) return VERR_INVALID_PARAMETER; const char *pszType = apszArgs[0]; int rc = VINF_SUCCESS; if (!RTStrICmp(pszType, "A")) { pClient->State.enmDataType = RTFTPSERVER_DATA_TYPE_ASCII; } else if (!RTStrICmp(pszType, "I")) /* Image (binary). */ { pClient->State.enmDataType = RTFTPSERVER_DATA_TYPE_IMAGE; } else /** @todo Support "E" (EBCDIC) and/or "L " (custom)? */ rc = VERR_NOT_IMPLEMENTED; if (RT_SUCCESS(rc)) rc = rtFtpServerSendReplyRc(pClient, RTFTPSERVER_REPLY_OKAY); return rc; } static DECLCALLBACK(int) rtFtpServerHandleUSER(PRTFTPSERVERCLIENT pClient, uint8_t cArgs, const char * const *apszArgs) { if (cArgs != 1) return VERR_INVALID_PARAMETER; const char *pszUser = apszArgs[0]; AssertPtrReturn(pszUser, VERR_INVALID_PARAMETER); rtFtpServerClientStateReset(&pClient->State); int rc = rtFtpServerLookupUser(pClient, pszUser); if (RT_SUCCESS(rc)) { pClient->State.pszUser = RTStrDup(pszUser); AssertPtrReturn(pClient->State.pszUser, VERR_NO_MEMORY); rc = rtFtpServerSendReplyRc(pClient, RTFTPSERVER_REPLY_USERNAME_OKAY_NEED_PASSWORD); } else { pClient->State.cFailedLoginAttempts++; int rc2 = rtFtpServerSendReplyRc(pClient, RTFTPSERVER_REPLY_NOT_LOGGED_IN); if (RT_SUCCESS(rc)) rc = rc2; } return rc; } /********************************************************************************************************************************* * Internal server functions * *********************************************************************************************************************************/ /** * Parses FTP command arguments handed in by the client. * * @returns VBox status code. * @param pszCmdParms Pointer to command arguments, if any. Can be NULL if no arguments are given. * @param pcArgs Returns the number of parsed arguments, separated by a space (hex 0x20). * @param ppapszArgs Returns the string array of parsed arguments. Needs to be free'd with rtFtpServerCmdArgsFree(). */ static int rtFtpServerCmdArgsParse(const char *pszCmdParms, uint8_t *pcArgs, char ***ppapszArgs) { *pcArgs = 0; *ppapszArgs = NULL; if (!pszCmdParms) /* No parms given? Bail out early. */ return VINF_SUCCESS; /** @todo Anything else to do here? */ /** @todo Check if quoting is correct. */ int cArgs = 0; int rc = RTGetOptArgvFromString(ppapszArgs, &cArgs, pszCmdParms, RTGETOPTARGV_CNV_QUOTE_MS_CRT, " " /* Separators */); if (RT_SUCCESS(rc)) { if (cArgs <= UINT8_MAX) { *pcArgs = (uint8_t)cArgs; } else rc = VERR_INVALID_PARAMETER; } return rc; } /** * Frees a formerly argument string array parsed by rtFtpServerCmdArgsParse(). * * @param ppapszArgs Argument string array to free. */ static void rtFtpServerCmdArgsFree(char **ppapszArgs) { RTGetOptArgvFree(ppapszArgs); } /** * Main function for processing client commands for the control connection. * * @returns VBox status code. * @param pClient Client to process commands for. * @param pszCmd Command string to parse and handle. * @param cbCmd Size (in bytes) of command string. */ static int rtFtpServerProcessCommands(PRTFTPSERVERCLIENT pClient, char *pszCmd, size_t cbCmd) { /* Make sure to terminate the string in any case. */ pszCmd[RT_MIN(RTFTPSERVER_MAX_CMD_LEN, cbCmd)] = '\0'; /* A tiny bit of sanitation. */ RTStrStripL(pszCmd); /* First, terminate string by finding the command end marker (telnet style). */ /** @todo Not sure if this is entirely correct and/or needs tweaking; good enough for now as it seems. */ char *pszCmdEnd = RTStrStr(pszCmd, "\r\n"); if (pszCmdEnd) *pszCmdEnd = '\0'; /* Reply which gets sent back to the client. */ RTFTPSERVER_REPLY rcClient = RTFTPSERVER_REPLY_INVALID; int rcCmd = VINF_SUCCESS; uint8_t cArgs = 0; char **papszArgs = NULL; int rc = rtFtpServerCmdArgsParse(pszCmd, &cArgs, &papszArgs); if ( RT_SUCCESS(rc) && cArgs) /* At least the actual command (without args) must be present. */ { LogFlowFunc(("Handling command '%s'\n", papszArgs[0])); for (uint8_t a = 0; a < cArgs; a++) LogFlowFunc(("\targ[%RU8] = '%s'\n", a, papszArgs[a])); unsigned i = 0; for (; i < RT_ELEMENTS(g_aCmdMap); i++) { const RTFTPSERVERCMD_ENTRY *pCmdEntry = &g_aCmdMap[i]; if (!RTStrICmp(papszArgs[0], pCmdEntry->szCmd)) { /* Some commands need a valid user before they can be executed. */ if ( pCmdEntry->fNeedsUser && pClient->State.pszUser == NULL) { rcClient = RTFTPSERVER_REPLY_NOT_LOGGED_IN; break; } /* Save timestamp of last command sent. */ pClient->State.tsLastCmdMs = RTTimeMilliTS(); /* Hand in arguments only without the actual command. */ rcCmd = pCmdEntry->pfnCmd(pClient, cArgs - 1, cArgs > 1 ? &papszArgs[1] : NULL); if (RT_FAILURE(rcCmd)) { LogFunc(("Handling command '%s' failed with %Rrc\n", papszArgs[0], rcCmd)); switch (rcCmd) { case VERR_INVALID_PARAMETER: RT_FALL_THROUGH(); case VERR_INVALID_POINTER: rcClient = RTFTPSERVER_REPLY_ERROR_INVALID_PARAMETERS; break; case VERR_NOT_IMPLEMENTED: rcClient = RTFTPSERVER_REPLY_ERROR_CMD_NOT_IMPL; break; default: break; } } break; } } rtFtpServerCmdArgsFree(papszArgs); if (i == RT_ELEMENTS(g_aCmdMap)) { LogFlowFunc(("Command not implemented\n")); Assert(rcClient == RTFTPSERVER_REPLY_INVALID); rcClient = RTFTPSERVER_REPLY_ERROR_CMD_NOT_IMPL; } const bool fDisconnect = g_aCmdMap[i].enmCmd == RTFTPSERVERCMD_QUIT || pClient->State.cFailedLoginAttempts >= 3; /** @todo Make this dynamic. */ if (fDisconnect) { RTFTPSERVER_HANDLE_CALLBACK_VA(pfnOnUserDisconnect, pClient->State.pszUser); rtFtpServerClientStateReset(&pClient->State); Assert(rcClient == RTFTPSERVER_REPLY_INVALID); rcClient = RTFTPSERVER_REPLY_CLOSING_CTRL_CONN; } } else rcClient = RTFTPSERVER_REPLY_ERROR_INVALID_PARAMETERS; if (rcClient != RTFTPSERVER_REPLY_INVALID) { int rc2 = rtFtpServerSendReplyRc(pClient, rcClient); if (RT_SUCCESS(rc)) rc = rc2; } LogFlowFuncLeaveRC(rc); return rc; } /** * Main loop for processing client commands. * * @returns VBox status code. * @param pClient Client to process commands for. */ static int rtFtpServerClientMain(PRTFTPSERVERCLIENT pClient) { int rc; size_t cbRead; char szCmd[RTFTPSERVER_MAX_CMD_LEN + 1]; for (;;) { rc = RTTcpSelectOne(pClient->hSocket, 200 /* ms */); /** @todo Can we improve here? Using some poll events or so? */ if (RT_SUCCESS(rc)) { rc = RTTcpReadNB(pClient->hSocket, szCmd, sizeof(szCmd), &cbRead); if ( RT_SUCCESS(rc) && cbRead) { AssertBreakStmt(cbRead <= sizeof(szCmd), rc = VERR_BUFFER_OVERFLOW); rc = rtFtpServerProcessCommands(pClient, szCmd, cbRead); } } else if (rc == VERR_TIMEOUT) rc = VINF_SUCCESS; else break; /* * Handle data connection replies. */ if (pClient->pDataConn) { if ( ASMAtomicReadBool(&pClient->pDataConn->fStarted) && ASMAtomicReadBool(&pClient->pDataConn->fStopped)) { Assert(pClient->pDataConn->rc != VERR_IPE_UNINITIALIZED_STATUS); int rc2 = rtFtpServerSendReplyRc(pClient, RT_SUCCESS(pClient->pDataConn->rc) ? RTFTPSERVER_REPLY_CLOSING_DATA_CONN : RTFTPSERVER_REPLY_CONN_REQ_FILE_ACTION_NOT_TAKEN); AssertRC(rc2); rc = rtFtpServerDataConnStop(pClient->pDataConn); if (RT_SUCCESS(rc)) { rtFtpServerDataConnDestroy(pClient->pDataConn); pClient->pDataConn = NULL; } } } } /* Make sure to destroy all data connections. */ rtFtpServerDataConnDestroy(pClient->pDataConn); pClient->pDataConn = NULL; LogFlowFuncLeaveRC(rc); return rc; } /** * Resets the client's state. * * @param pState Client state to reset. */ static void rtFtpServerClientStateReset(PRTFTPSERVERCLIENTSTATE pState) { LogFlowFuncEnter(); RTStrFree(pState->pszUser); pState->pszUser = NULL; int rc2 = rtFtpSetCWD(pState, "/"); AssertRC(rc2); pState->cFailedLoginAttempts = 0; pState->tsLastCmdMs = RTTimeMilliTS(); pState->enmDataType = RTFTPSERVER_DATA_TYPE_ASCII; pState->enmStructType = RTFTPSERVER_STRUCT_TYPE_FILE; } /** * Per-client thread for serving the server's control connection. * * @returns VBox status code. * @param hSocket Socket handle to use for the control connection. * @param pvUser User-provided arguments. Of type PRTFTPSERVERINTERNAL. */ static DECLCALLBACK(int) rtFtpServerClientThread(RTSOCKET hSocket, void *pvUser) { PRTFTPSERVERINTERNAL pThis = (PRTFTPSERVERINTERNAL)pvUser; RTFTPSERVER_VALID_RETURN(pThis); RTFTPSERVERCLIENT Client; RT_ZERO(Client); Client.pServer = pThis; Client.hSocket = hSocket; LogFlowFunc(("New client connected\n")); rtFtpServerClientStateReset(&Client.State); /* * Send welcome message. * Note: Some clients (like FileZilla / Firefox) expect a message together with the reply code, * so make sure to include at least *something*. */ int rc = rtFtpServerSendReplyRcEx(&Client, RTFTPSERVER_REPLY_READY_FOR_NEW_USER, "Welcome!"); if (RT_SUCCESS(rc)) { ASMAtomicIncU32(&pThis->cClients); rc = rtFtpServerClientMain(&Client); ASMAtomicDecU32(&pThis->cClients); } rtFtpServerClientStateReset(&Client.State); return rc; } RTR3DECL(int) RTFtpServerCreate(PRTFTPSERVER phFTPServer, const char *pszAddress, uint16_t uPort, PRTFTPSERVERCALLBACKS pCallbacks, void *pvUser, size_t cbUser) { AssertPtrReturn(phFTPServer, VERR_INVALID_POINTER); AssertPtrReturn(pszAddress, VERR_INVALID_POINTER); AssertReturn (uPort, VERR_INVALID_PARAMETER); AssertPtrReturn(pCallbacks, VERR_INVALID_POINTER); /* pvUser is optional. */ int rc; PRTFTPSERVERINTERNAL pThis = (PRTFTPSERVERINTERNAL)RTMemAllocZ(sizeof(RTFTPSERVERINTERNAL)); if (pThis) { pThis->u32Magic = RTFTPSERVER_MAGIC; pThis->Callbacks = *pCallbacks; pThis->pvUser = pvUser; pThis->cbUser = cbUser; rc = RTTcpServerCreate(pszAddress, uPort, RTTHREADTYPE_DEFAULT, "ftpsrv", rtFtpServerClientThread, pThis /* pvUser */, &pThis->pTCPServer); if (RT_SUCCESS(rc)) { *phFTPServer = (RTFTPSERVER)pThis; } } else rc = VERR_NO_MEMORY; return rc; } RTR3DECL(int) RTFtpServerDestroy(RTFTPSERVER hFTPServer) { if (hFTPServer == NIL_RTFTPSERVER) return VINF_SUCCESS; PRTFTPSERVERINTERNAL pThis = hFTPServer; RTFTPSERVER_VALID_RETURN(pThis); AssertPtr(pThis->pTCPServer); int rc = RTTcpServerDestroy(pThis->pTCPServer); if (RT_SUCCESS(rc)) { pThis->u32Magic = RTFTPSERVER_MAGIC_DEAD; RTMemFree(pThis); } return rc; }