Files
FileManager/Core/ftpServer.cpp
T
2025-12-15 00:18:06 -08:00

1336 lines
52 KiB
C++

#include "ftpServer.h"
#include "socketUtility.h"
#include "fileSystem.h"
#include "driveManager.h"
#include <algorithm>
#include <string>
#include "stringUtility.h"
#include "utils.h"
#include <StdIO.h>
#include <xtl.h>
#include "ftpCrc32.h"
#include "../Configuration.h"
namespace {
LONG activeConnections = 0;
int mMaxConnections;
int mCommandTimeout;
int mConnectTimeout;
bool mStopRequested;
uint64_t sListen;
sockaddr_in saiListen;
HANDLE mListenThreadHandle;
int incrementConnections() {
return (int)_InterlockedIncrement(&activeConnections);
}
void decrementConnections() {
_InterlockedDecrement(&activeConnections);
}
char* cleanVirtualPath(const char* virtualPath) {
const char* in = virtualPath;
char* buf = new char[strlen(in) + 4];
buf[0] = '\0';
buf[1] = '\0';
buf[2] = '\0';
char* out = buf + 3;
do {
*out = *in;
if (*out == '\\')
*out = '/'; // convert backslashes to forward slashes
if ((*out == '\0') || (*out == '/')) {
if (out[-1] == '.') { // output ends with "."
if (out[-2] == '\0')
--out; // entire output is "."
else if (out[-2] == '/') { // output ends with "/."
if (out[-3] == '\0')
--out; // entire output is "/."
else
out -= 2;
} else if (out[-2] == '.') { // output ends with ".."
if (out[-3] == '\0')
out -= 2; // entire output is ".."
else if (out[-3] == '/') { // output ends with "/.."
if (out[-4] == '\0')
out -= 2; // entire output is "/.."
else {
out -= 3;
while ((out[-1] != '\0') && (out[-1] != '/'))
--out;
}
}
} else
++in;
} else {
++in;
if (out[-1] != '/')
++out;
}
} else
++in, ++out;
} while (in[-1] != '\0');
char* result = _strdup(buf + 3);
delete[] buf;
return result;
}
char* resolveRelative(const char* currentVirtual, const char* relativeVirtual) {
if (relativeVirtual[0] != '/') {
char* newVirtual = stringUtility::formatString("%s\\%s", currentVirtual, relativeVirtual);
char* result = cleanVirtualPath(newVirtual);
free(newVirtual);
return result;
} else {
char* result = cleanVirtualPath(relativeVirtual);
return result;
}
}
static bool IsDriveLetterPath(const char* path) {
// "/E:/something"
if ((path[0] == '/' || path[0] == '\\') &&
((path[1] >= 'A' && path[1] <= 'Z') || (path[1] >= 'a' && path[1] <= 'z')) &&
path[2] == ':' &&
(path[3] == '/' || path[3] == '\\' || path[3] == '\0')) return true;
// "/E/something"
if ((path[0] == '/' || path[0] == '\\') &&
((path[1] >= 'A' && path[1] <= 'Z') || (path[1] >= 'a' && path[1] <= 'z')) &&
(path[2] == '/' || path[2] == '\\' || path[2] == '\0')) return true;
return false;
}
pointerVector<fileSystem::FileInfoDetail*>* getDirectoryListing(const char* virtualPath) {
if (stringUtility::equals(virtualPath, "/", false) == true) {
pointerVector<fileSystem::FileInfoDetail*>* fileInfoDetails =
new pointerVector<fileSystem::FileInfoDetail*>(true);
pointerVector<char*>* drives = driveManager::getMountedDrives();
for (size_t i = 0; i < drives->count(); i++) {
fileSystem::FileInfoDetail* fileInfoDetail = new fileSystem::FileInfoDetail();
char* mountPoint = stringUtility::formatString("%s:", drives->get(i));
char* mountAlias = driveManager::convertMountPointToMountPointAlias(mountPoint);
mountAlias[strlen(mountAlias) - 1] = '\0';
fileInfoDetail->isDirectory = true;
fileInfoDetail->isFile = false;
fileInfoDetail->isVirtual = true;
fileInfoDetail->path = mountAlias;
fileInfoDetail->accessTime.day = 1;
fileInfoDetail->accessTime.month = 1;
fileInfoDetail->accessTime.year = 2000;
fileInfoDetail->accessTime.minute = 0;
fileInfoDetail->accessTime.hour = 0;
fileInfoDetail->accessTime.second = 0;
fileInfoDetail->writeTime.day = 1;
fileInfoDetail->writeTime.month = 1;
fileInfoDetail->writeTime.year = 2000;
fileInfoDetail->writeTime.minute = 0;
fileInfoDetail->writeTime.hour = 0;
fileInfoDetail->writeTime.second = 0;
fileInfoDetails->add(fileInfoDetail);
free(mountPoint);
}
delete (drives);
return fileInfoDetails;
}
if (driveManager::ftpPathMounted(virtualPath) == false) {
return NULL;
}
char* ftpPath = driveManager::mapFtpPath(virtualPath);
pointerVector<fileSystem::FileInfoDetail*>* fileInfoDetails = fileSystem::fileGetFileInfoDetails(ftpPath);
;
free(ftpPath);
if (fileInfoDetails == NULL) {
return NULL;
}
for (size_t i = 0; i < fileInfoDetails->count(); i++) {
fileSystem::FileInfoDetail* fileInfoDetail = fileInfoDetails->get(i);
utils::swapString(&fileInfoDetail->path, fileSystem::getFileName(fileInfoDetail->path));
}
return fileInfoDetails;
}
} // namespace
bool WINAPI ftpServer::connectionThread(uint64_t sCmd) {
uint64_t sData = 0;
uint64_t sPasv = 0;
char outputBuffer[1024];
char szPeerName[64];
char szCmd[512];
char* pszParam;
char* user = NULL;
char* currentVirtual = NULL;
char* rnfr = NULL;
uint32_t dw = 0;
uint32_t dwRestOffset = 0;
ReceiveStatus status = ReceiveStatus_OK;
bool isLoggedIn = false;
fileSystem::FileTime fileTime;
UINT_PTR i;
SOCKADDR_IN saiCmd;
memset(&saiCmd, 0, sizeof(SOCKADDR_IN));
SOCKADDR_IN saiCmdPeer;
memset(&saiCmdPeer, 0, sizeof(SOCKADDR_IN));
SOCKADDR_IN saiPasv;
memset(&saiPasv, 0, sizeof(SOCKADDR_IN));
SOCKADDR_IN saiData;
memset(&saiData, 0, sizeof(SOCKADDR_IN));
// Get peer address
dw = sizeof(SOCKADDR_IN);
getpeername((SOCKET)sCmd, (SOCKADDR*)&saiCmdPeer, (int*)&dw);
sprintf(szPeerName, "%u.%u.%u.%u", saiCmdPeer.sin_addr.S_un.S_un_b.s_b1, saiCmdPeer.sin_addr.S_un.S_un_b.s_b2,
saiCmdPeer.sin_addr.S_un.S_un_b.s_b3, saiCmdPeer.sin_addr.S_un.S_un_b.s_b4);
// Send greeting
socketSendString(sCmd, "220-%s\r\n220-You are connecting from %s:%u.\r\n220 Proceed with login.\r\n", FTP_SERVER_ID,
szPeerName, ntohs(saiCmdPeer.sin_port));
socketUtility::getSocketName(sCmd, &saiCmd);
// Command processing loop
while (mStopRequested == false) {
const int result = socketUtility::getReadStatus(sCmd);
if (result != 1) {
Sleep(50);
continue;
}
status = socketReceiveString(sCmd, szCmd, 512, &dw);
if (status == ReceiveStatus_Network_Error) {
socketSendString(sCmd, "421 Network error.\r\n");
break;
} else if (status == ReceiveStatus_Timeout) {
socketSendString(sCmd, "421 Connection timed out.\r\n");
break;
} else if (status == ReceiveStatus_Invalid_Data) {
socketSendString(sCmd, "500 Malformed request.\r\n");
continue;
} else if (status == ReceiveStatus_Insufficient_Buffer) {
socketSendString(sCmd, "500 Command line too long.\r\n");
continue;
}
if (pszParam = strchr(szCmd, ' ')) {
*(pszParam++) = 0;
} else {
pszParam = szCmd + strlen(szCmd);
}
if (!_stricmp(szCmd, "USER")) {
if (!*pszParam) {
socketSendString(sCmd, "501 Syntax error in parameters or arguments.\r\n");
continue;
} else if (isLoggedIn) {
socketSendString(sCmd, "503 Already logged in. Use REIN to change users.\r\n");
continue;
} else {
utils::swapString(&user, _strdup(pszParam));
if (stringUtility::equals(user, "xbox", false) == true) {
socketSendString(sCmd, "331 Need password for user \"%s\".\r\n", user);
continue;
}
}
}
if (!_stricmp(szCmd, "PASS")) {
if (user == NULL) {
socketSendString(sCmd, "503 Bad sequence of commands. Send USER first.\r\n");
} else if (isLoggedIn) {
socketSendString(sCmd, "503 Already logged in. Use REIN to change users.\r\n");
} else {
if (stringUtility::equals(user, "xbox", false) && stringUtility::equals(pszParam, "xbox", false)) {
if (incrementConnections() <= mMaxConnections) {
isLoggedIn = true;
utils::swapString(&currentVirtual, _strdup("/"));
socketSendString(sCmd, "230 User \"%s\" logged in.\r\n", user);
} else {
decrementConnections();
socketSendString(sCmd, "421 Your login was refused due to a server connection limit.\r\n");
break;
}
} else {
socketSendString(sCmd, "530 Incorrect password.\r\n");
}
}
}
else if (!_stricmp(szCmd, "REIN")) {
if (isLoggedIn) {
isLoggedIn = false;
decrementConnections();
socketSendString(sCmd, "220-User \"%s\" logged out.\r\n", user);
free(user);
user = NULL;
}
socketSendString(sCmd, "220 REIN command successful.\r\n");
}
else if (!_stricmp(szCmd, "HELP")) {
socketSendString(sCmd, "214 For help, please cry to mom.\r\n");
}
else if (!_stricmp(szCmd, "FEAT")) {
socketSendString(
sCmd, "211-Extensions supported:\r\n SIZE\r\n REST STREAM\r\n MDTM\r\n TVFS\r\n XCRC\r\n211 END\r\n");
}
// else if (!_stricmp(szCmd, "SYST")) {
// sprintf(szOutput, "215 Windows_NT\r\n");
// socketSendString(sCmd, szOutput);
// }
else if (!_stricmp(szCmd, "QUIT")) {
if (isLoggedIn) {
isLoggedIn = false;
decrementConnections();
socketSendString(sCmd, "221-User \"%s\" logged out.\r\n", user);
}
socketSendString(sCmd, "221 Goodbye!\r\n");
break;
}
else if (!_stricmp(szCmd, "NOOP")) {
socketSendString(sCmd, "200 NOOP command successful.\r\n");
}
else if (!_stricmp(szCmd, "PWD") || !_stricmp(szCmd, "XPWD")) {
if (!isLoggedIn) {
socketSendString(sCmd, "530 Not logged in.\r\n");
} else {
socketSendString(sCmd, "257 \"%s\" is current directory.\r\n", currentVirtual);
}
}
else if (!_stricmp(szCmd, "CWD") || !_stricmp(szCmd, "XCWD")) {
if (!*pszParam) {
socketSendString(sCmd, "501 Syntax error in parameters or arguments.\r\n");
} else if (!isLoggedIn) {
socketSendString(sCmd, "530 Not logged in.\r\n");
} else {
char* newVirtual = resolveRelative(currentVirtual, pszParam);
// Check if IsDriveLetterPath
if (IsDriveLetterPath(newVirtual)) {
utils::swapString(&currentVirtual, _strdup("/")); // Flush currentVirtual
socketSendString(sCmd, "550 Invalid directory. Try reconnecting.\r\n");
free(newVirtual);
continue;
}
bool isFolder = false;
if (stringUtility::equals(newVirtual, "/", false)) {
isFolder = true;
} else {
char* ftpPath = driveManager::mapFtpPath(newVirtual);
if (stringUtility::endsWith(ftpPath, ":", false)) {
isFolder = true;
} else {
fileSystem::directoryExists(ftpPath, isFolder);
}
free(ftpPath);
}
if (isFolder == true) {
utils::swapString(&currentVirtual, _strdup(newVirtual));
socketSendString(sCmd, "250 \"%s\" is now current directory.\r\n", currentVirtual);
} else {
socketSendString(sCmd, "550 %s failed. \"%s\": directory not found.\r\n", szCmd, newVirtual);
}
free(newVirtual);
}
}
else if (!_stricmp(szCmd, "CDUP") || !_stricmp(szCmd, "XCUP")) {
if (!isLoggedIn) {
socketSendString(sCmd, "530 Not logged in.\r\n");
} else {
utils::swapString(&currentVirtual, resolveRelative(currentVirtual, ".."));
// Check if IsDriveLetterPath
if (IsDriveLetterPath(currentVirtual)) {
utils::swapString(&currentVirtual, _strdup("/")); // Flush currentVirtual
socketSendString(sCmd, "550 Invalid directory. Try reconnecting.\r\n");
continue;
}
socketSendString(sCmd, "250 \"%s\" is now current directory.\r\n", currentVirtual);
}
}
else if (!_stricmp(szCmd, "TYPE")) {
if (!*pszParam) {
socketSendString(sCmd, "501 Syntax error in parameters or arguments.\r\n");
} else if (!isLoggedIn) {
socketSendString(sCmd, "530 Not logged in.\r\n");
} else {
socketSendString(sCmd, "200 TYPE command successful.\r\n");
}
}
else if (!_stricmp(szCmd, "REST")) {
if (!*pszParam || (!(dw = stringUtility::toInt(pszParam)) && (*pszParam != '0'))) {
socketSendString(sCmd, "501 Syntax error in parameters or arguments.\r\n");
} else if (!isLoggedIn) {
socketSendString(sCmd, "530 Not logged in.\r\n");
} else {
dwRestOffset = dw;
socketSendString(sCmd, "350 Ready to resume transfer at %u bytes.\r\n", dwRestOffset);
}
}
else if (!_stricmp(szCmd, "PORT")) {
if (!*pszParam) {
socketSendString(sCmd, "501 Syntax error in parameters or arguments.\r\n");
} else if (!isLoggedIn) {
socketSendString(sCmd, "530 Not logged in.\r\n");
} else {
memset(&saiData, 0, sizeof(SOCKADDR_IN));
saiData.sin_family = AF_INET;
unsigned short h1 = 0;
unsigned short h2 = 0;
unsigned short h3 = 0;
unsigned short h4 = 0;
unsigned short p1 = 0;
unsigned short p2 = 0;
dw = sscanf(pszParam, "%hu,%hu,%hu,%hu,%hu,%hu", &h1, &h2, &h3, &h4, &p1, &p2);
saiData.sin_addr.S_un.S_un_b.s_b1 = (u_char)h1;
saiData.sin_addr.S_un.S_un_b.s_b2 = (u_char)h2;
saiData.sin_addr.S_un.S_un_b.s_b3 = (u_char)h3;
saiData.sin_addr.S_un.S_un_b.s_b4 = (u_char)h4;
saiData.sin_port = (p2 << 8) + p1;
if (dw == 6) {
socketUtility::closeSocket(sPasv);
socketSendString(sCmd, "200 PORT command successful.\r\n");
} else {
socketSendString(sCmd, "501 Syntax error in parameters or arguments.\r\n");
memset(&saiData, 0, sizeof(SOCKADDR_IN));
}
}
}
else if (!_stricmp(szCmd, "PASV")) {
if (!isLoggedIn) {
socketSendString(sCmd, "530 Not logged in.\r\n");
} else {
if (sPasv) {
socketUtility::closeSocket(sPasv);
}
static u_short portTest = 6000;
XNADDR addr;
XNetGetTitleXnAddr(&addr);
bool found = false;
while (!found) {
memset(&saiPasv, 0, sizeof(SOCKADDR_IN));
saiPasv.sin_family = AF_INET;
saiPasv.sin_addr.s_addr = INADDR_ANY;
saiPasv.sin_port = htons(portTest);
if (socketUtility::createSocket(AF_INET, SOCK_STREAM, IPPROTO_TCP, sPasv)) {
found = true;
} else {
utils::debugPrint(
"Error: Could not create socket on port %u, trying to find next available.\n", portTest);
}
portTest++;
if (portTest > 6999) {
portTest = 6000;
}
}
socketUtility::bindSocket(sPasv, &saiPasv);
socketUtility::listenSocket(sPasv, 1);
socketUtility::getSocketName(sPasv, &saiPasv);
socketSendString(sCmd, "227 Entering Passive Mode (%u,%u,%u,%u,%u,%u)\r\n", addr.ina.S_un.S_un_b.s_b1,
addr.ina.S_un.S_un_b.s_b2, addr.ina.S_un.S_un_b.s_b3, addr.ina.S_un.S_un_b.s_b4,
((unsigned char*)&saiPasv.sin_port)[0], ((unsigned char*)&saiPasv.sin_port)[1]);
}
}
else if (!_stricmp(szCmd, "LIST") || !_stricmp(szCmd, "NLST")) {
if (!isLoggedIn) {
socketSendString(sCmd, "530 Not logged in.\r\n");
} else {
if (*pszParam == '-') {
if (pszParam = strchr(pszParam, ' ')) {
pszParam++;
}
}
char* newVirtual =
pszParam && *pszParam ? resolveRelative(currentVirtual, pszParam) : _strdup(currentVirtual);
// Check if IsDriveLetterPath
if (IsDriveLetterPath(newVirtual)) {
utils::swapString(&currentVirtual, _strdup("/")); // Flush currentVirtual
socketSendString(sCmd, "550 Invalid directory. Try reconnecting.\r\n");
free(newVirtual);
continue;
}
pointerVector<fileSystem::FileInfoDetail*>* fileInfoDetails = getDirectoryListing(newVirtual);
if (fileInfoDetails != NULL) {
sData = establishDataConnection(&saiData, &sPasv);
if (sData) {
socketSendString(sCmd, "150 Opened %s mode data connection for \"%s\".\r\n",
sPasv ? "passive" : "active", newVirtual);
const char* months[12] = {
"Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"};
for (size_t i = 0; i < fileInfoDetails->count(); i++) {
char szLine[512];
fileSystem::FileInfoDetail* fileInfoDetail = fileInfoDetails->get(i);
if (_stricmp(szCmd, "NLST") == 0) {
strcpy(szLine, fileInfoDetail->path);
} else {
sprintf(szLine, "%c--------- 1 ftp ftp %llu ", fileInfoDetail->isDirectory ? 'd' : '-',
fileInfoDetail->size);
strcat(szLine, months[fileInfoDetail->writeTime.month - 1]);
sprintf(szLine + strlen(szLine), " %2i %.2i:%.2i ", fileInfoDetail->writeTime.day,
fileInfoDetail->writeTime.hour, fileInfoDetail->writeTime.minute);
strcat(szLine, fileInfoDetail->path);
}
// if (fileInfoDetail->fileInfoDetail->isDirectory == true)
//{
// strcat(szLine, "/");
// }
strcat(szLine, "\r\n");
socketSendString(sData, szLine);
}
socketUtility::closeSocket(sData);
socketSendString(sCmd, "226 %s command successful.\r\n", _stricmp(szCmd, "NLST") ? "LIST" : "NLST");
} else {
socketSendString(sCmd, "425 Can't open data connection.\r\n");
}
} else {
socketSendString(sCmd, "550 \"%s\": Path not found.\r\n", newVirtual);
}
free(newVirtual);
delete (fileInfoDetails);
}
}
else if (!_stricmp(szCmd, "STAT")) {
if (!isLoggedIn) {
socketSendString(sCmd, "530 Not logged in.\r\n");
} else {
if (*pszParam == '-') {
if (pszParam = strchr(pszParam, ' ')) {
pszParam++;
}
}
char* newVirtual =
pszParam && *pszParam ? resolveRelative(currentVirtual, pszParam) : _strdup(currentVirtual);
// Check if IsDriveLetterPath
if (IsDriveLetterPath(newVirtual)) {
utils::swapString(&currentVirtual, _strdup("/")); // Flush currentVirtual
socketSendString(sCmd, "550 Invalid directory. Try reconnecting.\r\n");
free(newVirtual);
continue;
}
pointerVector<fileSystem::FileInfoDetail*>* fileInfoDetails = getDirectoryListing(newVirtual);
if (fileInfoDetails != NULL) {
socketSendString(sCmd, "212-Sending directory listing of \"%s\".\r\n", newVirtual);
const char* months[12] = {
"Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"};
for (size_t i = 0; i < fileInfoDetails->count(); i++) {
char szLine[512];
fileSystem::FileInfoDetail* fileInfoDetail = fileInfoDetails->get(i);
sprintf(szLine, "%c--------- 1 ftp ftp %llu ", fileInfoDetail->isDirectory ? 'd' : '-',
fileInfoDetail->size);
strcat(szLine, months[fileInfoDetail->writeTime.month - 1]);
sprintf(szLine + strlen(szLine), " %2i %.2i:%.2i ", fileInfoDetail->writeTime.day,
fileInfoDetail->writeTime.hour, fileInfoDetail->writeTime.minute);
strcat(szLine, fileInfoDetail->path);
if (fileInfoDetail->isDirectory == true) {
strcat(szLine, "/");
}
strcat(szLine, "\r\n");
socketSendString(sCmd, szLine);
}
} else {
socketSendString(sCmd, "550 \"%s\": Path not found.\r\n", newVirtual);
}
free(newVirtual);
delete (fileInfoDetails);
}
}
else if (!_stricmp(szCmd, "RETR")) {
if (!*pszParam) {
socketSendString(sCmd, "501 Syntax error in parameters or arguments.\r\n");
} else if (!isLoggedIn) {
socketSendString(sCmd, "530 Not logged in.\r\n");
} else {
char* newVirtual = resolveRelative(currentVirtual, pszParam);
// Check if IsDriveLetterPath
if (IsDriveLetterPath(newVirtual)) {
utils::swapString(&currentVirtual, _strdup("/")); // Flush currentVirtual
socketSendString(sCmd, "550 Invalid directory. Try reconnecting.\r\n");
free(newVirtual);
continue;
}
char* ftpPath = driveManager::mapFtpPath(newVirtual);
uint32_t fileHandle;
if (fileSystem::fileOpen(ftpPath, fileSystem::FileModeRead, fileHandle) == true) {
if (dwRestOffset) {
fileSystem::fileSeek(fileHandle, fileSystem::FileSeekModeStart, dwRestOffset);
dwRestOffset = 0;
}
sData = establishDataConnection(&saiData, &sPasv);
if (sData) {
socketSendString(sCmd, "150 Opened %s mode data connection for \"%s\".\r\n",
sPasv ? "passive" : "active", newVirtual);
if (sendSocketFile(sCmd, sData, fileHandle, &dw)) {
socketSendString(sCmd, "226 \"%s\" transferred successfully.\r\n", newVirtual);
} else {
socketSendString(sCmd, "426 Connection closed; transfer aborted.\r\n");
if (dw) {
socketSendString(sCmd, "226 ABOR command successful.\r\n");
}
}
socketUtility::closeSocket(sData);
}
else {
socketSendString(sCmd, "425 Can't open data connection.\r\n");
}
fileSystem::fileClose(fileHandle);
} else {
socketSendString(sCmd, "550 \"%s\": Unable to open file.\r\n", newVirtual);
}
free(ftpPath);
free(newVirtual);
}
}
else if (!_stricmp(szCmd, "STOR") || !_stricmp(szCmd, "APPE")) {
if (!*pszParam) {
socketSendString(sCmd, "501 Syntax error in parameters or arguments.\r\n");
} else if (!isLoggedIn) {
socketSendString(sCmd, "530 Not logged in.\r\n");
} else {
char* newVirtual = resolveRelative(currentVirtual, pszParam);
// Check if IsDriveLetterPath
if (IsDriveLetterPath(newVirtual)) {
utils::swapString(&currentVirtual, _strdup("/")); // Flush currentVirtual
socketSendString(sCmd, "550 Invalid directory. Try reconnecting.\r\n");
free(newVirtual);
continue;
}
char* ftpPath = driveManager::mapFtpPath(newVirtual);
uint32_t fileHandle;
if (fileSystem::fileOpen(ftpPath, fileSystem::FileModeWriteUpdate, fileHandle) == true) {
if (_stricmp(szCmd, "APPE") == 0) {
fileSystem::fileSeek(fileHandle, fileSystem::FileSeekModeEnd, 0);
} else {
fileSystem::fileSeek(fileHandle, fileSystem::FileSeekModeStart, dwRestOffset);
fileSystem::fileTruncate(fileHandle, dwRestOffset);
}
dwRestOffset = 0;
sData = establishDataConnection(&saiData, &sPasv);
if (sData) {
socketSendString(sCmd, "150 Opened %s mode data connection for \"%s\".\r\n",
sPasv ? "passive" : "active", newVirtual);
if (receiveSocketFile(sCmd, sData, fileHandle)) {
socketSendString(sCmd, "226 \"%s\" transferred successfully.\r\n", newVirtual);
} else {
socketSendString(sCmd, "426 Connection closed; transfer aborted.\r\n");
}
socketUtility::closeSocket(sData);
} else {
socketSendString(sCmd, "425 Can't open data connection.\r\n");
}
fileSystem::fileClose(fileHandle);
} else {
socketSendString(sCmd, "550 \"%s\": Unable to open file.\r\n", newVirtual);
}
free(newVirtual);
free(ftpPath);
}
}
else if (!_stricmp(szCmd, "ABOR")) {
socketUtility::closeSocket(sPasv);
dwRestOffset = 0;
socketSendString(sCmd, "200 ABOR command successful.\r\n");
}
else if (!_stricmp(szCmd, "SIZE")) {
if (!*pszParam) {
socketSendString(sCmd, "501 Syntax error in parameters or arguments.\r\n");
} else if (!isLoggedIn) {
socketSendString(sCmd, "530 Not logged in.\r\n");
} else {
char* newVirtual = resolveRelative(currentVirtual, pszParam);
// Check if IsDriveLetterPath
if (IsDriveLetterPath(newVirtual)) {
utils::swapString(&currentVirtual, _strdup("/")); // Flush currentVirtual
socketSendString(sCmd, "550 Invalid directory. Try reconnecting.\r\n");
free(newVirtual);
continue;
}
char* ftpPath = driveManager::mapFtpPath(newVirtual);
uint32_t fileHandle;
if (fileSystem::fileOpen(ftpPath, fileSystem::FileModeRead, fileHandle) == true) {
uint32_t fileSize = 0;
fileSystem::fileSize(fileHandle, fileSize);
socketSendString(sCmd, "213 %llu\r\n", fileSize);
fileSystem::fileClose(fileHandle);
} else {
socketSendString(sCmd, "550 \"%s\": File not found.\r\n", newVirtual);
}
free(newVirtual);
free(ftpPath);
}
}
else if (!_stricmp(szCmd, "MDTM")) {
if (!*pszParam) {
socketSendString(sCmd, "501 Syntax error in parameters or arguments.\r\n");
} else if (!isLoggedIn) {
socketSendString(sCmd, "530 Not logged in.\r\n");
} else {
for (i = 0; i < 14; i++) {
if ((pszParam[i] < '0') || (pszParam[i] > '9')) {
break;
}
}
if ((i == 14) && (pszParam[14] == ' ')) {
strncpy(outputBuffer, pszParam, 4);
outputBuffer[4] = 0;
fileTime.year = (WORD)stringUtility::toInt(outputBuffer);
strncpy(outputBuffer, pszParam + 4, 2);
outputBuffer[2] = 0;
fileTime.month = (WORD)stringUtility::toInt(outputBuffer);
strncpy(outputBuffer, pszParam + 6, 2);
fileTime.day = (WORD)stringUtility::toInt(outputBuffer);
strncpy(outputBuffer, pszParam + 8, 2);
fileTime.hour = (WORD)stringUtility::toInt(outputBuffer);
strncpy(outputBuffer, pszParam + 10, 2);
fileTime.minute = (WORD)stringUtility::toInt(outputBuffer);
strncpy(outputBuffer, pszParam + 12, 2);
fileTime.second = (WORD)stringUtility::toInt(outputBuffer);
pszParam += 15;
dw = 1;
} else {
dw = 0;
}
char* newVirtual = resolveRelative(currentVirtual, pszParam);
// Check if IsDriveLetterPath
if (IsDriveLetterPath(newVirtual)) {
utils::swapString(&currentVirtual, _strdup("/")); // Flush currentVirtual
socketSendString(sCmd, "550 Invalid directory. Try reconnecting.\r\n");
free(newVirtual);
continue;
}
if (dw == 1) {
char* ftpPath = driveManager::mapFtpPath(newVirtual);
if (fileSystem::setFileTime(ftpPath, fileTime) == true) {
socketSendString(sCmd, "250 MDTM command successful.\r\n");
} else {
socketSendString(sCmd, "550 \"%s\": File not found.\r\n", newVirtual);
}
free(ftpPath);
} else {
char* ftpPath = driveManager::mapFtpPath(newVirtual);
fileSystem::FileInfoDetail* fileInfoDetail = fileSystem::fileGetFileInfoDetail(ftpPath);
if (fileInfoDetail != NULL) {
socketSendString(sCmd, "213 %04u%02u%02u%02u%02u%02u\r\n", fileInfoDetail->writeTime.year,
fileInfoDetail->writeTime.month, fileInfoDetail->writeTime.day,
fileInfoDetail->writeTime.hour, fileInfoDetail->writeTime.minute,
fileInfoDetail->writeTime.second);
} else {
socketSendString(sCmd, "550 \"%s\": File not found.\r\n", newVirtual);
}
delete (fileInfoDetail);
free(ftpPath);
}
free(newVirtual);
}
}
else if (!_stricmp(szCmd, "DELE")) {
if (!*pszParam) {
socketSendString(sCmd, "501 Syntax error in parameters or arguments.\r\n");
} else if (!isLoggedIn) {
socketSendString(sCmd, "530 Not logged in.\r\n");
} else {
char* newVirtual = resolveRelative(currentVirtual, pszParam);
// Check if IsDriveLetterPath
if (IsDriveLetterPath(newVirtual)) {
utils::swapString(&currentVirtual, _strdup("/")); // Flush currentVirtual
socketSendString(sCmd, "550 Invalid directory. Try reconnecting.\r\n");
free(newVirtual);
continue;
}
char* ftpPath = driveManager::mapFtpPath(newVirtual);
if (fileSystem::fileDelete(ftpPath) == true) {
socketSendString(sCmd, "250 \"%s\" deleted successfully.\r\n", newVirtual);
} else {
socketSendString(sCmd, "550 \"%s\": File not found.\r\n", newVirtual);
}
free(ftpPath);
free(newVirtual);
}
}
else if (!_stricmp(szCmd, "RNFR")) {
if (!*pszParam) {
socketSendString(sCmd, "501 Syntax error in parameters or arguments.\r\n");
} else if (!isLoggedIn) {
socketSendString(sCmd, "530 Not logged in.\r\n");
} else {
char* newVirtual = resolveRelative(currentVirtual, pszParam);
// Check if IsDriveLetterPath
if (IsDriveLetterPath(newVirtual)) {
utils::swapString(&currentVirtual, _strdup("/")); // Flush currentVirtual
socketSendString(sCmd, "550 Invalid directory. Try reconnecting.\r\n");
free(newVirtual);
continue;
}
char* ftpPath = driveManager::mapFtpPath(newVirtual);
bool exists;
if ((fileSystem::fileExists(ftpPath, exists) == true && exists == true) ||
(fileSystem::directoryExists(ftpPath, exists) == true && exists == true)) {
utils::swapString(&rnfr, _strdup(ftpPath));
socketSendString(sCmd, "350 \"%s\": File/Directory exists; proceed with RNTO.\r\n", newVirtual);
} else {
socketSendString(sCmd, "550 \"%s\": File/Directory not found.\r\n", newVirtual);
}
free(ftpPath);
free(newVirtual);
}
}
else if (!_stricmp(szCmd, "RNTO")) {
if (!*pszParam) {
socketSendString(sCmd, "501 Syntax error in parameters or arguments.\r\n");
} else if (!isLoggedIn) {
socketSendString(sCmd, "530 Not logged in.\r\n");
} else if (rnfr == NULL) {
socketSendString(sCmd, "503 Bad sequence of commands. Send RNFR first.\r\n");
} else {
char* newVirtual = resolveRelative(currentVirtual, pszParam);
// Check if IsDriveLetterPath
if (IsDriveLetterPath(newVirtual)) {
utils::swapString(&currentVirtual, _strdup("/")); // Flush currentVirtual
socketSendString(sCmd, "550 Invalid directory. Try reconnecting.\r\n");
free(newVirtual);
continue;
}
char* ftpPath = driveManager::mapFtpPath(newVirtual);
if (fileSystem::fileMove(rnfr, ftpPath) == true) {
socketSendString(sCmd, "250 RNTO command successful.\r\n");
free(rnfr);
rnfr = NULL;
} else {
socketSendString(sCmd, "553 \"%s\": Unable to rename file.\r\n", newVirtual);
}
free(newVirtual);
free(ftpPath);
}
}
else if (!_stricmp(szCmd, "MKD") || !_stricmp(szCmd, "XMKD")) {
if (!*pszParam) {
socketSendString(sCmd, "501 Syntax error in parameters or arguments.\r\n");
} else if (!isLoggedIn) {
socketSendString(sCmd, "530 Not logged in.\r\n");
} else {
char* newVirtual = resolveRelative(currentVirtual, pszParam);
// Check if IsDriveLetterPath
if (IsDriveLetterPath(newVirtual)) {
utils::swapString(&currentVirtual, _strdup("/")); // Flush currentVirtual
socketSendString(sCmd, "550 Invalid directory. Try reconnecting.\r\n");
free(newVirtual);
continue;
}
char* ftpPath = driveManager::mapFtpPath(newVirtual);
if (fileSystem::directoryCreate(ftpPath) == true) {
socketSendString(sCmd, "250 \"%s\" created successfully.\r\n", newVirtual);
} else {
socketSendString(sCmd, "550 \"%s\": Unable to create directory.\r\n", newVirtual);
}
free(ftpPath);
free(newVirtual);
}
}
else if (!_stricmp(szCmd, "RMD") || !_stricmp(szCmd, "XRMD")) {
if (!*pszParam) {
socketSendString(sCmd, "501 Syntax error in parameters or arguments.\r\n");
} else if (!isLoggedIn) {
socketSendString(sCmd, "530 Not logged in.\r\n");
} else {
char* newVirtual = resolveRelative(currentVirtual, pszParam);
// Check if IsDriveLetterPath
if (IsDriveLetterPath(newVirtual)) {
utils::swapString(&currentVirtual, _strdup("/")); // Flush currentVirtual
socketSendString(sCmd, "550 Invalid directory. Try reconnecting.\r\n");
free(newVirtual);
continue;
}
char* ftpPath = driveManager::mapFtpPath(newVirtual);
if (fileSystem::directoryDelete(ftpPath, true) == true) {
socketSendString(sCmd, "250 \"%s\" removed successfully.\r\n", newVirtual);
} else {
socketSendString(sCmd, "550 \"%s\": Unable to remove directory.\r\n", newVirtual);
}
free(ftpPath);
free(newVirtual);
}
}
else if (!_stricmp(szCmd, "XCRC")) {
if (!*pszParam) {
socketSendString(sCmd, "501 Syntax error in parameters or arguments.\r\n");
} else if (!isLoggedIn) {
socketSendString(sCmd, "530 Not logged in.\r\n");
} else {
char* newVirtual = resolveRelative(currentVirtual, pszParam);
// Check if IsDriveLetterPath
if (IsDriveLetterPath(newVirtual)) {
utils::swapString(&currentVirtual, _strdup("/")); // Flush currentVirtual
socketSendString(sCmd, "550 Invalid directory. Try reconnecting.\r\n");
free(newVirtual);
continue;
}
char* ftpPath = driveManager::mapFtpPath(newVirtual);
uint32_t fileHandle;
if (fileSystem::fileOpen(ftpPath, fileSystem::FileModeRead, fileHandle) == true) {
unsigned long checksum = crc32::calculate(fileHandle);
socketSendString(sCmd, "250 %08X\r\n", checksum);
fileSystem::fileClose(fileHandle);
} else {
socketSendString(sCmd, "550 \"%s\": File not found.\r\n", newVirtual);
}
free(ftpPath);
free(newVirtual);
}
}
else if (!_stricmp(szCmd, "OPTS")) {
if (!*pszParam) {
socketSendString(sCmd, "501 Syntax error in parameters or arguments.\r\n");
} else if (!_stricmp(pszParam, "UTF8 On")) {
socketSendString(sCmd, "200 Always in UTF8 mode.\r\n");
} else {
socketSendString(sCmd, "501 Option not understood.\r\n");
}
}
else if (!_stricmp(szCmd, "AVBL")) {
if (!*pszParam) {
socketSendString(sCmd, "550 Syntax error in parameters or arguments.\r\n");
} else {
char* newVirtual = resolveRelative(currentVirtual, pszParam);
// Check if IsDriveLetterPath
if (IsDriveLetterPath(newVirtual)) {
utils::swapString(&currentVirtual, _strdup("/")); // Flush currentVirtual
socketSendString(sCmd, "550 Invalid directory. Try reconnecting.\r\n");
free(newVirtual);
continue;
}
char* ftpPath = driveManager::mapFtpPath(newVirtual);
uint64_t totalFree;
if (driveManager::getTotalFreeNumberOfBytes(ftpPath, totalFree) == true) {
socketSendString(sCmd, "213 %llu\r\n", totalFree);
} else {
socketSendString(sCmd, "550 \"%s\": not found.\r\n", pszParam);
}
free(ftpPath);
free(newVirtual);
}
}
else {
socketSendString(sCmd, "500 Syntax error, command \"%s\" unrecognized.\r\n", szCmd);
}
}
socketUtility::closeSocket(sPasv);
socketUtility::closeSocket(sCmd);
if (isLoggedIn) {
decrementConnections();
}
free(user);
free(currentVirtual);
free(rnfr);
return false;
}
bool WINAPI ftpServer::listenThread(LPVOID lParam) {
uint64_t sIncoming = 0;
while (mStopRequested == false) {
const int result = socketUtility::getReadStatus(sListen);
if (result == SOCKET_ERROR) {
utils::debugPrint("Error: Socket status failed: %i\n", WSAGetLastError());
break;
}
if (result == 1 && socketUtility::acceptSocket(sListen, sIncoming)) {
HANDLE connectionThreadHandle =
CreateThread(0, 0, (LPTHREAD_START_ROUTINE)connectionThread, (void*)sIncoming, 0, NULL);
if (connectionThreadHandle != NULL) {
SetThreadPriority(connectionThreadHandle, 2);
CloseHandle(connectionThreadHandle);
}
} else {
Sleep(50);
}
}
socketUtility::closeSocket(sListen);
socketUtility::closeSocket(sIncoming);
return false;
}
bool ftpServer::init() {
mStopRequested = false;
mListenThreadHandle = NULL;
XSetFileCacheSize(4 * 1024 * 1024);
memset(&saiListen, 0, sizeof(SOCKADDR_IN));
saiListen.sin_family = AF_INET;
saiListen.sin_addr.S_un.S_addr = INADDR_ANY;
saiListen.sin_port = htons(21);
mMaxConnections = 20;
mCommandTimeout = 300;
mConnectTimeout = 60;
socketUtility::createSocket(AF_INET, SOCK_STREAM, IPPROTO_TCP, sListen);
if (!socketUtility::bindSocket(sListen, &saiListen)) {
socketUtility::closeSocket(sListen);
return false;
}
socketUtility::listenSocket(sListen, SOMAXCONN);
mListenThreadHandle = CreateThread(0, 0, (LPTHREAD_START_ROUTINE)listenThread, 0, 0, NULL);
if (mListenThreadHandle != NULL) {
SetThreadPriority(mListenThreadHandle, 2);
}
return true;
}
void ftpServer::close() {
mStopRequested = true;
WaitForSingleObject(mListenThreadHandle, INFINITE);
CloseHandle(mListenThreadHandle);
}
bool ftpServer::socketSendString(uint64_t s, const char* format, ...) {
va_list args;
va_start(args, format);
uint32_t length = _vsnprintf(NULL, 0, format, args);
char* result = (char*)malloc(length + 1);
_vsnprintf(result, length, format, args);
result[length] = 0;
va_end(args);
bool bSuccess = send((SOCKET)s, result, strlen(result), 0) != SOCKET_ERROR;
free(result);
return bSuccess;
}
ftpServer::ReceiveStatus ftpServer::socketReceiveString(
uint64_t s, char* psz, uint32_t dwMaxChars, uint32_t* pdwCharsReceived) {
uint32_t dwChars = 0;
ReceiveStatus status, statusError;
char buf[2];
uint32_t dw;
for (;;) {
if (dwChars == dwMaxChars) {
statusError = ReceiveStatus_Insufficient_Buffer;
break;
}
status = socketReceiveLetter(s, psz, dwMaxChars - dwChars, &dw);
if (status == ReceiveStatus_OK) {
dwChars += dw;
if (*psz == '\r')
*psz = 0;
else if (*psz == '\n') {
*psz = 0;
*pdwCharsReceived = dwChars;
return ReceiveStatus_OK;
}
psz += dw;
} else if (status == ReceiveStatus_Invalid_Data || status == ReceiveStatus_Insufficient_Buffer) {
statusError = status;
break;
} else {
return status;
}
}
// A non-critical error occurred, read until end of line
for (;;) {
status = socketReceiveLetter(s, buf, sizeof(buf) / sizeof(char), &dw);
if (status == ReceiveStatus_OK) {
if (*buf == '\n') {
return statusError;
}
} else if (status == ReceiveStatus_Invalid_Data || status == ReceiveStatus_Insufficient_Buffer) {
// Go on...
} else {
return status;
}
}
}
ftpServer::ReceiveStatus ftpServer::socketReceiveLetter(
uint64_t s, char* pch, uint32_t dwMaxChars, uint32_t* pdwCharsReceived) {
char buf[4];
uint32_t dw;
TIMEVAL tv;
fd_set fds;
tv.tv_sec = mCommandTimeout;
tv.tv_usec = 0;
FD_ZERO(&fds);
FD_SET((SOCKET)s, &fds);
dw = select(0, &fds, 0, 0, &tv);
if (dw == SOCKET_ERROR || dw == 0)
return ReceiveStatus_Timeout;
dw = recv((SOCKET)s, &buf[0], 1, 0);
if (dw == SOCKET_ERROR || dw == 0)
return ReceiveStatus_Network_Error;
if (dwMaxChars == 0) {
return ReceiveStatus_Insufficient_Buffer;
}
pch[0] = buf[0];
*pdwCharsReceived = 1;
return ReceiveStatus_OK;
}
ftpServer::ReceiveStatus ftpServer::socketReceiveData(
uint64_t s, char* psz, uint32_t dwBytesToRead, uint32_t* pdwBytesRead) {
uint32_t dw;
TIMEVAL tv;
fd_set fds;
tv.tv_sec = mConnectTimeout;
tv.tv_usec = 0;
memset(&fds, 0, sizeof(fd_set));
FD_SET((SOCKET)s, &fds);
dw = select(0, &fds, 0, 0, &tv);
if (dw == SOCKET_ERROR || dw == 0) {
utils::debugPrint("Error: SocketReceiveData failed with Timeout.\n");
return ReceiveStatus_Timeout;
}
dw = recv((SOCKET)s, psz, dwBytesToRead, 0);
if (dw == SOCKET_ERROR) {
utils::debugPrint("Error: SocketReceiveData failed with Network Error.\n");
return ReceiveStatus_Network_Error;
}
*pdwBytesRead = dw;
return ReceiveStatus_OK;
}
uint64_t ftpServer::establishDataConnection(sockaddr_in* psaiData, uint64_t* psPasv) {
uint64_t sData;
uint32_t dw;
TIMEVAL tv;
fd_set fds;
if (psPasv && *psPasv) {
tv.tv_sec = mConnectTimeout;
tv.tv_usec = 0;
memset(&fds, 0, sizeof(fd_set));
FD_SET((SOCKET)*psPasv, &fds);
dw = select(0, &fds, 0, 0, &tv);
if (dw && dw != SOCKET_ERROR) {
socketUtility::acceptSocket(*psPasv, psaiData, sData);
} else {
sData = 0;
}
socketUtility::closeSocket(*psPasv);
return sData;
} else {
socketUtility::createSocket(AF_INET, SOCK_STREAM, IPPROTO_TCP, sData);
if (!socketUtility::connectSocket(sData, psaiData)) {
socketUtility::closeSocket(sData);
return INVALID_SOCKET;
} else {
return sData;
}
}
}
bool ftpServer::receiveSocketFile(uint64_t sCmd, uint64_t sData, uint32_t fileHandle) {
uint32_t socketBufferSize = RECV_SOCKET_BUFFER_SIZE;
socketUtility::setSocketRecvSize(sData, socketBufferSize);
int driveSectorBufferSize = 128 * 1024;
int combinedBufferSize = socketBufferSize + driveSectorBufferSize;
char* combinedBuffer = (char*)malloc(combinedBufferSize);
bool fileComplete = false;
uint32_t totalBytesToWrite = 0;
uint32_t totalWritten = 0;
while (true) {
while (!fileComplete && totalBytesToWrite < (uint32_t)driveSectorBufferSize) {
uint32_t bytesRead = 0;
if (socketReceiveData(sData, combinedBuffer + totalBytesToWrite, socketBufferSize, &bytesRead) !=
ReceiveStatus_OK) {
free(combinedBuffer);
return false;
}
totalBytesToWrite += bytesRead;
if (bytesRead == 0) {
fileComplete = true;
break;
}
}
if (fileComplete && totalBytesToWrite == 0) {
free(combinedBuffer);
return true;
}
uint32_t bytesWritten = 0;
uint32_t bytesToWrite = min((uint32_t)driveSectorBufferSize, totalBytesToWrite);
if (!fileSystem::fileWrite(fileHandle, combinedBuffer, bytesToWrite, bytesWritten)) {
utils::debugPrint("Error: WriteFile failed: %i", GetLastError());
free(combinedBuffer);
return false;
}
totalBytesToWrite -= bytesWritten;
totalWritten += bytesWritten;
memmove(combinedBuffer, combinedBuffer + bytesWritten, totalBytesToWrite);
}
}
bool ftpServer::sendSocketFile(uint64_t sCmd, uint64_t sData, uint32_t fileHandle, uint32_t* pdwAbortFlag) {
uint32_t bufferSize = SEND_SOCKET_BUFFER_SIZE;
socketUtility::setSocketSendSize(sData, bufferSize);
char* szBuffer = (char*)malloc(bufferSize);
while (true) {
uint32_t bytesRead;
if (fileSystem::fileRead(fileHandle, szBuffer, bufferSize, bytesRead) == false) {
utils::debugPrint("Error: ReadFile failed: %i", GetLastError());
free(szBuffer);
return false;
}
if (bytesRead == 0) {
free(szBuffer);
return true;
}
int bytesToSend = bytesRead;
int bufferOffset = 0;
while (bytesToSend > 0) {
int sent = send((SOCKET)sData, szBuffer + bufferOffset, bytesToSend, 0);
int queueLength = 0;
socketUtility::getReadQueueLength(sCmd, queueLength);
if (queueLength > 0) {
char szCmd[512];
uint32_t bytes_received = 0;
if (socketReceiveString(sCmd, szCmd, 512, &bytes_received) == ReceiveStatus_OK) {
if (!_stricmp(szCmd, "ABOR")) {
*pdwAbortFlag = 1;
free(szBuffer);
return false;
} else {
socketSendString(sCmd, "500 Only command allowed at this time is ABOR.\r\n");
}
}
}
bytesToSend -= sent;
bufferOffset += (int)sent;
if (sent < 1) {
free(szBuffer);
return false;
}
}
}
return true;
}