diff options
Diffstat (limited to 'src/VBox/Runtime/generic/ftp-server.cpp')
-rw-r--r-- | src/VBox/Runtime/generic/ftp-server.cpp | 2578 |
1 files changed, 2578 insertions, 0 deletions
diff --git a/src/VBox/Runtime/generic/ftp-server.cpp b/src/VBox/Runtime/generic/ftp-server.cpp new file mode 100644 index 00000000..07de3d39 --- /dev/null +++ b/src/VBox/Runtime/generic/ftp-server.cpp @@ -0,0 +1,2578 @@ +/* $Id: ftp-server.cpp $ */ +/** @file + * Generic FTP server (RFC 959) implementation. + * Partly also implements RFC 3659 (Extensions to FTP, for "SIZE", ++). + */ + +/* + * Copyright (C) 2020 Oracle Corporation + * + * This file is part of VirtualBox Open Source Edition (OSE), as + * available from http://www.virtualbox.org. This file is free software; + * you can redistribute it and/or modify it under the terms of the GNU + * General Public License (GPL) as published by the Free Software + * Foundation, in version 2 as it comes in the "COPYING" file of the + * VirtualBox OSE distribution. VirtualBox OSE is distributed in the + * hope that it will be useful, but WITHOUT ANY WARRANTY of any kind. + * + * The contents of this file may alternatively be used under the terms + * of the Common Development and Distribution License Version 1.0 + * (CDDL) only, as it comes in the "COPYING.CDDL" file of the + * VirtualBox OSE 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. + */ + +/** + * 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. + */ + + +/********************************************************************************************************************************* +* Header Files * +*********************************************************************************************************************************/ +#define LOG_GROUP RTLOGGROUP_FTP +#include <iprt/ftp.h> +#include "internal/iprt.h" +#include "internal/magics.h" + +#include <iprt/asm.h> +#include <iprt/assert.h> +#include <iprt/circbuf.h> +#include <iprt/err.h> +#include <iprt/file.h> /* For file mode flags. */ +#include <iprt/getopt.h> +#include <iprt/mem.h> +#include <iprt/log.h> +#include <iprt/path.h> +#include <iprt/poll.h> +#include <iprt/socket.h> +#include <iprt/sort.h> +#include <iprt/string.h> +#include <iprt/system.h> +#include <iprt/tcp.h> + + +/********************************************************************************************************************************* +* 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) + +/** Supported FTP server command IDs. + * Alphabetically, named after their official command names. */ +typedef enum RTFTPSERVER_CMD +{ + /** Invalid command, do not use. Always must come first. */ + RTFTPSERVER_CMD_INVALID = 0, + /** Aborts the current command on the server. */ + RTFTPSERVER_CMD_ABOR, + /** Changes the current working directory. */ + RTFTPSERVER_CMD_CDUP, + /** Changes the current working directory. */ + RTFTPSERVER_CMD_CWD, + /** Reports features supported by the server. */ + RTFTPSERVER_CMD_FEAT, + /** Lists a directory. */ + RTFTPSERVER_CMD_LIST, + /** Sets the transfer mode. */ + RTFTPSERVER_CMD_MODE, + /** Sends a nop ("no operation") to the server. */ + RTFTPSERVER_CMD_NOOP, + /** Sets the password for authentication. */ + RTFTPSERVER_CMD_PASS, + /** Sets the port to use for the data connection. */ + RTFTPSERVER_CMD_PORT, + /** Gets the current working directory. */ + RTFTPSERVER_CMD_PWD, + /** Get options. Needed in conjunction with the FEAT command. */ + RTFTPSERVER_CMD_OPTS, + /** Terminates the session (connection). */ + RTFTPSERVER_CMD_QUIT, + /** Retrieves a specific file. */ + RTFTPSERVER_CMD_RETR, + /** Retrieves the size of a file. */ + RTFTPSERVER_CMD_SIZE, + /** Retrieves the current status of a transfer. */ + RTFTPSERVER_CMD_STAT, + /** Sets the structure type to use. */ + RTFTPSERVER_CMD_STRU, + /** Gets the server's OS info. */ + RTFTPSERVER_CMD_SYST, + /** Sets the (data) representation type. */ + RTFTPSERVER_CMD_TYPE, + /** Sets the user name for authentication. */ + RTFTPSERVER_CMD_USER, + /** End marker. */ + RTFTPSERVER_CMD_LAST, + /** The usual 32-bit hack. */ + RTFTPSERVER_CMD_32BIT_HACK = 0x7fffffff +} RTFTPSERVER_CMD; + +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 DECLCALLBACK(int) FNRTFTPSERVERCMD(PRTFTPSERVERCLIENT pClient, uint8_t cArgs, const char * const *apcszArgs); +/** Pointer to a FNRTFTPSERVERCMD(). */ +typedef FNRTFTPSERVERCMD *PFNRTFTPSERVERCMD; + +/** 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); \ + } \ + else \ + 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__); \ + } \ + else \ + return VERR_NOT_IMPLEMENTED; \ + } while (0) + + +/********************************************************************************************************************************* +* Defined Constants And Macros * +*********************************************************************************************************************************/ + +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 *apcszArgs); +static int rtFtpServerDataConnStop(PRTFTPSERVERDATACONN pDataConn); +static void rtFtpServerDataConnDestroy(PRTFTPSERVERDATACONN pDataConn); +static int rtFtpServerDataConnFlush(PRTFTPSERVERDATACONN pDataConn); + +static void rtFtpServerClientStateReset(PRTFTPSERVERCLIENTSTATE pState); + +/** + * Function prototypes for 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 RTFTPSERVER_CMD_ENTRY +{ + /** Command ID. */ + RTFTPSERVER_CMD 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; +} RTFTPSERVER_CMD_ENTRY; +/** Pointer to a command entry. */ +typedef RTFTPSERVER_CMD_ENTRY *PRTFTPSERVER_CMD_ENTRY; + +/** + * Table of handled commands. + */ +const RTFTPSERVER_CMD_ENTRY g_aCmdMap[] = +{ + { RTFTPSERVER_CMD_ABOR, "ABOR", true, rtFtpServerHandleABOR }, + { RTFTPSERVER_CMD_CDUP, "CDUP", true, rtFtpServerHandleCDUP }, + { RTFTPSERVER_CMD_CWD, "CWD", true, rtFtpServerHandleCWD }, + { RTFTPSERVER_CMD_FEAT, "FEAT", false, rtFtpServerHandleFEAT }, + { RTFTPSERVER_CMD_LIST, "LIST", true, rtFtpServerHandleLIST }, + { RTFTPSERVER_CMD_MODE, "MODE", true, rtFtpServerHandleMODE }, + { RTFTPSERVER_CMD_NOOP, "NOOP", true, rtFtpServerHandleNOOP }, + { RTFTPSERVER_CMD_PASS, "PASS", false, rtFtpServerHandlePASS }, + { RTFTPSERVER_CMD_PORT, "PORT", true, rtFtpServerHandlePORT }, + { RTFTPSERVER_CMD_PWD, "PWD", true, rtFtpServerHandlePWD }, + { RTFTPSERVER_CMD_OPTS, "OPTS", false, rtFtpServerHandleOPTS }, + { RTFTPSERVER_CMD_QUIT, "QUIT", false, rtFtpServerHandleQUIT }, + { RTFTPSERVER_CMD_RETR, "RETR", true, rtFtpServerHandleRETR }, + { RTFTPSERVER_CMD_SIZE, "SIZE", true, rtFtpServerHandleSIZE }, + { RTFTPSERVER_CMD_STAT, "STAT", true, rtFtpServerHandleSTAT }, + { RTFTPSERVER_CMD_STRU, "STRU", true, rtFtpServerHandleSTRU }, + { RTFTPSERVER_CMD_SYST, "SYST", false, rtFtpServerHandleSYST }, + { RTFTPSERVER_CMD_TYPE, "TYPE", true, rtFtpServerHandleTYPE }, + { RTFTPSERVER_CMD_USER, "USER", false, rtFtpServerHandleUSER }, + { RTFTPSERVER_CMD_LAST, "", 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 pcszFormat Format string of message to send with the reply code. + */ +static int rtFtpServerSendReplyRcEx(PRTFTPSERVERCLIENT pClient, RTFTPSERVER_REPLY enmReply, + const char *pcszFormat, ...) +{ + char *pszMsg = NULL; + + va_list args; + va_start(args, pcszFormat); + char *pszFmt = NULL; + const int cch = RTStrAPrintfV(&pszFmt, pcszFormat, 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 pcszFormat Format to reply. + * @param ... Format arguments. + */ +static int rtFtpServerSendReplyStr(PRTFTPSERVERCLIENT pClient, const char *pcszFormat, ...) +{ + va_list args; + va_start(args, pcszFormat); + char *psz = NULL; + const int cch = RTStrAPrintfV(&psz, pcszFormat, 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 pcszPath Path to check. + * @param fIsAbsolute Whether the path to check is an absolute path or not. + */ +static bool rtFtpServerPathIsValid(const char *pcszPath, bool fIsAbsolute) +{ + if (!pcszPath) + return false; + + bool fIsValid = strlen(pcszPath) + && RTStrIsValidEncoding(pcszPath) + && RTStrStr(pcszPath, "..") == NULL; /** @todo Very crude for now -- improve this. */ + if ( fIsValid + && fIsAbsolute) + { + RTFSOBJINFO objInfo; + int rc2 = RTPathQueryInfo(pcszPath, &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(("pcszPath=%s -> %RTbool\n", pcszPath, 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 pcszPath Working directory to set. + */ +static int rtFtpSetCWD(PRTFTPSERVERCLIENTSTATE pState, const char *pcszPath) +{ + RTStrFree(pState->pszCWD); + + if (!rtFtpServerPathIsValid(pcszPath, false /* fIsAbsolute */)) + return VERR_INVALID_PARAMETER; + + pState->pszCWD = RTStrDup(pcszPath); + + 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 pcszUser User name to look up. + */ +static int rtFtpServerLookupUser(PRTFTPSERVERCLIENT pClient, const char *pcszUser) +{ + RTFTPSERVER_HANDLE_CALLBACK_VA_RET(pfnOnUserConnect, pcszUser); +} + +/** + * Handles the actual client authentication. + * + * @returns VBox status code, or VERR_ACCESS_DENIED if authentication failed. + * @param pClient Client to authenticate. + * @param pcszUser User name to authenticate with. + * @param pcszPassword Password to authenticate with. + */ +static int rtFtpServerAuthenticate(PRTFTPSERVERCLIENT pClient, const char *pcszUser, const char *pcszPassword) +{ + RTFTPSERVER_HANDLE_CALLBACK_VA_RET(pfnOnUserAuthenticate, pcszUser, pcszPassword); +} + +/** + * 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 pcszStr 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 *pcszStr, PRTNETADDRIPV4 pAddr, uint16_t *puPort) +{ + AssertPtrReturn(pcszStr, 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(pcszStr, &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 apcszArgs Pointer to argument vector to duplicate. + */ +static char** rtFtpCmdArgsDup(uint8_t cArgs, const char * const *apcszArgs) +{ + if (!cArgs) + return NULL; + + char **apcszArgsDup = (char **)RTMemAlloc(cArgs * sizeof(char *)); + if (!apcszArgsDup) + { + AssertFailed(); + return NULL; + } + + int rc2 = VINF_SUCCESS; + + uint8_t i; + for (i = 0; i < cArgs; i++) + { + apcszArgsDup[i] = RTStrDup(apcszArgs[i]); + if (!apcszArgsDup[i]) + rc2 = VERR_NO_MEMORY; + } + + if (RT_FAILURE(rc2)) + { + while (i--) + RTStrFree(apcszArgsDup[i]); + + RTMemFree(apcszArgsDup); + return NULL; + } + + return apcszArgsDup; +} + +/** + * Frees a command argument vector. + * + * @param cArgs Number of arguments in argument vector. + * @param papcszArgs Pointer to argument vector to free. + */ +static void rtFtpCmdArgsFree(uint8_t cArgs, char **papcszArgs) +{ + while (cArgs--) + RTStrFree(papcszArgs[cArgs]); + + RTMemFree(papcszArgs); +} + +/** + * 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 pcszFormat Format string to send. No (terminal) termination added. + */ +static int rtFtpServerDataConnPrintf(PRTFTPSERVERDATACONN pDataConn, const char *pcszFormat, ...) +{ + va_list args; + va_start(args, pcszFormat); + char *pszFmt = NULL; + const int cch = RTStrAPrintfV(&pszFmt, pcszFormat, 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 *pcszFile = pDataConn->papszArgs[0]; + AssertPtr(pcszFile); + + void *pvHandle = NULL; /* Opaque handle known to the actual implementation. */ + + RTFTPSERVER_HANDLE_CALLBACK_VA(pfnOnFileOpen, pcszFile, + 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 apcszArgs Array of arguments. + */ +static int rtFtpServerDataConnStart(PRTFTPSERVERDATACONN pDataConn, PFNRTTHREAD pfnThread, + uint8_t cArgs, const char * const *apcszArgs) +{ + 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, apcszArgs); + 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. + * + * @returns VBox status code. + * @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. + * + * @returns VBox status code. + * @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 int rtFtpServerHandleABOR(PRTFTPSERVERCLIENT pClient, uint8_t cArgs, const char * const *apcszArgs) +{ + RT_NOREF(cArgs, apcszArgs); + + int rc = rtFtpServerDataConnClose(pClient->pDataConn); + if (RT_SUCCESS(rc)) + { + rtFtpServerDataConnDestroy(pClient->pDataConn); + pClient->pDataConn = NULL; + + rc = rtFtpServerSendReplyRc(pClient, RTFTPSERVER_REPLY_OKAY); + } + + return rc; +} + +static int rtFtpServerHandleCDUP(PRTFTPSERVERCLIENT pClient, uint8_t cArgs, const char * const *apcszArgs) +{ + RT_NOREF(cArgs, apcszArgs); + + 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 int rtFtpServerHandleCWD(PRTFTPSERVERCLIENT pClient, uint8_t cArgs, const char * const *apcszArgs) +{ + if (cArgs != 1) + return VERR_INVALID_PARAMETER; + + int rc; + + const char *pcszPath = apcszArgs[0]; + + if (!rtFtpServerPathIsValid(pcszPath, false /* fIsAbsolute */)) + return VERR_INVALID_PARAMETER; + + RTFTPSERVER_HANDLE_CALLBACK_VA(pfnOnPathSetCurrent, pcszPath); + + if (RT_SUCCESS(rc)) + rc = rtFtpSetCWD(&pClient->State, pcszPath); + + return rtFtpServerSendReplyRc(pClient, + RT_SUCCESS(rc) + ? RTFTPSERVER_REPLY_OKAY : RTFTPSERVER_REPLY_CONN_REQ_FILE_ACTION_NOT_TAKEN); +} + +static int rtFtpServerHandleFEAT(PRTFTPSERVERCLIENT pClient, uint8_t cArgs, const char * const *apcszArgs) +{ + RT_NOREF(cArgs, apcszArgs); + + 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 "<Nil>"; + + 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 "<Nil>"; + + 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 int rtFtpServerHandleLIST(PRTFTPSERVERCLIENT pClient, uint8_t cArgs, const char * const *apcszArgs) +{ + /* If no argument is given, use the server's CWD as the path. */ + const char *pcszPath = cArgs ? apcszArgs[0] : pClient->State.pszCWD; + AssertPtr(pcszPath); + + int rc = VINF_SUCCESS; + + if (!rtFtpServerPathIsValid(pcszPath, false /* fIsAbsolute */)) + { + int rc2 = rtFtpServerSendReplyRc(pClient, RTFTPSERVER_REPLY_CONN_REQ_FILE_ACTION_NOT_TAKEN); + AssertRC(rc2); + } + else + { + RTFTPSERVER_HANDLE_CALLBACK_VA(pfnOnFileStat, pcszPath, 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, apcszArgs); + + 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 int rtFtpServerHandleMODE(PRTFTPSERVERCLIENT pClient, uint8_t cArgs, const char * const *apcszArgs) +{ + RT_NOREF(pClient, cArgs, apcszArgs); + + /** @todo Anything to do here? */ + return VINF_SUCCESS; +} + +static int rtFtpServerHandleNOOP(PRTFTPSERVERCLIENT pClient, uint8_t cArgs, const char * const *apcszArgs) +{ + RT_NOREF(cArgs, apcszArgs); + + /* Save timestamp of last command sent. */ + pClient->State.tsLastCmdMs = RTTimeMilliTS(); + + return rtFtpServerSendReplyRc(pClient, RTFTPSERVER_REPLY_OKAY); +} + +static int rtFtpServerHandlePASS(PRTFTPSERVERCLIENT pClient, uint8_t cArgs, const char * const *apcszArgs) +{ + if (cArgs != 1) + return rtFtpServerSendReplyRc(pClient, RTFTPSERVER_REPLY_ERROR_INVALID_PARAMETERS); + + const char *pcszPassword = apcszArgs[0]; + AssertPtrReturn(pcszPassword, VERR_INVALID_PARAMETER); + + int rc = rtFtpServerAuthenticate(pClient, pClient->State.pszUser, pcszPassword); + 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 int rtFtpServerHandlePORT(PRTFTPSERVERCLIENT pClient, uint8_t cArgs, const char * const *apcszArgs) +{ + if (cArgs != 1) + return rtFtpServerSendReplyRc(pClient, RTFTPSERVER_REPLY_ERROR_INVALID_PARAMETERS); + + RTFTPSERVER_REPLY rcClient; + + int rc = rtFtpParseHostAndPort(apcszArgs[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 int rtFtpServerHandlePWD(PRTFTPSERVERCLIENT pClient, uint8_t cArgs, const char * const *apcszArgs) +{ + RT_NOREF(cArgs, apcszArgs); + + 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 int rtFtpServerHandleOPTS(PRTFTPSERVERCLIENT pClient, uint8_t cArgs, const char * const *apcszArgs) +{ + RT_NOREF(cArgs, apcszArgs); + + int rc = VINF_SUCCESS; + + int rc2 = rtFtpServerSendReplyRc(pClient, RTFTPSERVER_REPLY_OKAY); + if (RT_SUCCESS(rc)) + rc = rc2; + + return rc; +} + +static int rtFtpServerHandleQUIT(PRTFTPSERVERCLIENT pClient, uint8_t cArgs, const char * const *apcszArgs) +{ + RT_NOREF(cArgs, apcszArgs); + + 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 int rtFtpServerHandleRETR(PRTFTPSERVERCLIENT pClient, uint8_t cArgs, const char * const *apcszArgs) +{ + if (cArgs != 1) /* File name needs to be present. */ + return VERR_INVALID_PARAMETER; + + int rc; + + const char *pcszPath = apcszArgs[0]; + + RTFTPSERVER_HANDLE_CALLBACK_VA(pfnOnFileStat, pcszPath, 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, apcszArgs); + + 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 int rtFtpServerHandleSIZE(PRTFTPSERVERCLIENT pClient, uint8_t cArgs, const char * const *apcszArgs) +{ + if (cArgs != 1) + return VERR_INVALID_PARAMETER; + + int rc; + + const char *pcszPath = apcszArgs[0]; + uint64_t uSize = 0; + + RTFTPSERVER_HANDLE_CALLBACK_VA(pfnOnFileGetSize, pcszPath, &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 int rtFtpServerHandleSTAT(PRTFTPSERVERCLIENT pClient, uint8_t cArgs, const char * const *apcszArgs) +{ + if (cArgs != 1) + return VERR_INVALID_PARAMETER; + + int rc; + + RTFSOBJINFO objInfo; + RT_ZERO(objInfo); + + const char *pcszPath = apcszArgs[0]; + + RTFTPSERVER_HANDLE_CALLBACK_VA(pfnOnFileStat, pcszPath, &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(pcszPath), pcszPath); + 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 int rtFtpServerHandleSTRU(PRTFTPSERVERCLIENT pClient, uint8_t cArgs, const char * const *apcszArgs) +{ + if (cArgs != 1) + return VERR_INVALID_PARAMETER; + + const char *pcszType = apcszArgs[0]; + + int rc; + + if (!RTStrICmp(pcszType, "F")) + { + pClient->State.enmStructType = RTFTPSERVER_STRUCT_TYPE_FILE; + + rc = rtFtpServerSendReplyRc(pClient, RTFTPSERVER_REPLY_OKAY); + } + else + rc = VERR_NOT_IMPLEMENTED; + + return rc; +} + +static int rtFtpServerHandleSYST(PRTFTPSERVERCLIENT pClient, uint8_t cArgs, const char * const *apcszArgs) +{ + RT_NOREF(cArgs, apcszArgs); + + char szOSInfo[64]; + int rc = RTSystemQueryOSInfo(RTSYSOSINFO_PRODUCT, szOSInfo, sizeof(szOSInfo)); + if (RT_SUCCESS(rc)) + rc = rtFtpServerSendReplyStr(pClient, "215 %s", szOSInfo); + + return rc; +} + +static int rtFtpServerHandleTYPE(PRTFTPSERVERCLIENT pClient, uint8_t cArgs, const char * const *apcszArgs) +{ + if (cArgs != 1) + return VERR_INVALID_PARAMETER; + + const char *pcszType = apcszArgs[0]; + + int rc = VINF_SUCCESS; + + if (!RTStrICmp(pcszType, "A")) + { + pClient->State.enmDataType = RTFTPSERVER_DATA_TYPE_ASCII; + } + else if (!RTStrICmp(pcszType, "I")) /* Image (binary). */ + { + pClient->State.enmDataType = RTFTPSERVER_DATA_TYPE_IMAGE; + } + else /** @todo Support "E" (EBCDIC) and/or "L <size>" (custom)? */ + rc = VERR_NOT_IMPLEMENTED; + + if (RT_SUCCESS(rc)) + rc = rtFtpServerSendReplyRc(pClient, RTFTPSERVER_REPLY_OKAY); + + return rc; +} + +static int rtFtpServerHandleUSER(PRTFTPSERVERCLIENT pClient, uint8_t cArgs, const char * const *apcszArgs) +{ + if (cArgs != 1) + return VERR_INVALID_PARAMETER; + + const char *pcszUser = apcszArgs[0]; + AssertPtrReturn(pcszUser, VERR_INVALID_PARAMETER); + + rtFtpServerClientStateReset(&pClient->State); + + int rc = rtFtpServerLookupUser(pClient, pcszUser); + if (RT_SUCCESS(rc)) + { + pClient->State.pszUser = RTStrDup(pcszUser); + 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 pcszCmdParms 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 ppapcszArgs Returns the string array of parsed arguments. Needs to be free'd with rtFtpServerCmdArgsFree(). + */ +static int rtFtpServerCmdArgsParse(const char *pcszCmdParms, uint8_t *pcArgs, char ***ppapcszArgs) +{ + *pcArgs = 0; + *ppapcszArgs = NULL; + + if (!pcszCmdParms) /* 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(ppapcszArgs, &cArgs, pcszCmdParms, 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 ppapcszArgs Argument string array to free. + */ +static void rtFtpServerCmdArgsFree(char **ppapcszArgs) +{ + RTGetOptArgvFree(ppapcszArgs); +} + +/** + * Main function for processing client commands for the control connection. + * + * @returns VBox status code. + * @param pClient Client to process commands for. + * @param pcszCmd Command string to parse and handle. + * @param cbCmd Size (in bytes) of command string. + */ +static int rtFtpServerProcessCommands(PRTFTPSERVERCLIENT pClient, char *pcszCmd, size_t cbCmd) +{ + /* Make sure to terminate the string in any case. */ + pcszCmd[RT_MIN(RTFTPSERVER_MAX_CMD_LEN, cbCmd)] = '\0'; + + /* A tiny bit of sanitation. */ + RTStrStripL(pcszCmd); + + /* 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 = RTStrIStr(pcszCmd, "\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(pcszCmd, &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 RTFTPSERVER_CMD_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 == RTFTPSERVER_CMD_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 rtFtpServerProcessCommands(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; + + if (RT_FAILURE(rc)) + 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 = rtFtpServerProcessCommands(&Client); + + ASMAtomicDecU32(&pThis->cClients); + } + + rtFtpServerClientStateReset(&Client.State); + + return rc; +} + +RTR3DECL(int) RTFtpServerCreate(PRTFTPSERVER phFTPServer, const char *pcszAddress, uint16_t uPort, + PRTFTPSERVERCALLBACKS pCallbacks, void *pvUser, size_t cbUser) +{ + AssertPtrReturn(phFTPServer, VERR_INVALID_POINTER); + AssertPtrReturn(pcszAddress, 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(pcszAddress, 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; +} + |