This repository has been archived on 2025-02-01. You can view files and clone it, but cannot push or open issues or pull requests.
reprapfirmware-dc42/src/DuetNG/FtpResponder.cpp
David Crocker 384d1d3ad4 Release 1.19alpha
First working release of new WiFi code
Axes moving other than X (possibly mapped), Y and Z are not taken to be
part of the feed rate
Introduced SafeStrncpy and SafeStrncat
2017-05-04 21:39:43 +01:00

890 lines
21 KiB
C++

/*
* FtpResponder.cpp
*
* Created on: 15 Apr 2017
* Authors: David and Christian
*/
#include "FtpResponder.h"
#include "Network.h"
#include "Platform.h"
FtpResponder::FtpResponder(NetworkResponder *n) : NetworkResponder(n), dataSocket(nullptr),
passivePort(0), passivePortOpenTime(0), dataBuf(nullptr)
{
strcpy(fileToMove, "");
}
// Ask the responder to accept this connection, returns true if it did
bool FtpResponder::Accept(Socket *s, Protocol protocol)
{
if (responderState == ResponderState::free && protocol == FtpProtocol)
{
// Make sure we can get an output buffer before we accept the connection, or we won't be able to reply
if (outBuf != nullptr || OutputBuffer::Allocate(outBuf))
{
skt = s;
if (reprap.Debug(moduleWebserver))
{
debugPrintf("FTP connection accepted\n");
}
outBuf->copy("220 RepRapFirmware FTP server\r\n");
Commit(ResponderState::authenticating);
return true;
}
}
else if (responderState == ResponderState::waitingForPasvPort && protocol == FtpDataProtocol && s->GetLocalPort() == passivePort)
{
dataSocket = s;
responderState = ResponderState::pasvPortOpened;
if (reprap.Debug(moduleWebserver))
{
debugPrintf("FTP data connection accepted\n");
}
return true;
}
return false;
}
// This is called to force termination if we implement the specified protocol
void FtpResponder::Terminate(Protocol protocol)
{
if (protocol == FtpProtocol || protocol == AnyProtocol)
{
ConnectionLost();
}
}
// Do some work, returning true if we did anything significant
bool FtpResponder::Spin()
{
switch (responderState)
{
case ResponderState::free:
return false;
case ResponderState::authenticating:
case ResponderState::reading:
return ReadData();
case ResponderState::sending:
SendData();
return true;
case ResponderState::waitingForPasvPort:
if (millis() - passivePortOpenTime > ftpPasvPortTimeout && (outBuf != nullptr || OutputBuffer::Allocate(outBuf)))
{
outBuf->copy("425 Failed to establish connection.\r\n");
Commit(ResponderState::reading);
CloseDataPort();
return true;
}
return false;
case ResponderState::pasvPortOpened:
if (dataBuf != nullptr || OutputBuffer::Allocate(dataBuf))
{
return ReadData();
}
return false;
case ResponderState::uploading:
DoUpload();
if (!uploadError && skt->CanRead())
{
ReadData(); // check for incoming ABOR requests
}
return true;
case ResponderState::sendingPasvData:
SendPassiveData();
if (!sendError && skt->CanRead())
{
ReadData(); // check for incoming ABOR requests
}
return true;
case ResponderState::pasvTransferComplete:
if (outBuf != nullptr || OutputBuffer::Allocate(outBuf))
{
// Is the main FTP connection still available?
if (skt->CanSend())
{
// Yes - send a response
if (uploadError || sendError)
{
outBuf->copy("526 Transfer failed!\r\n");
}
else
{
outBuf->copy("226 Transfer complete.\r\n");
}
Commit(ResponderState::reading);
// Close the data port again
CloseDataPort();
}
else
{
// No - reset our state
ConnectionLost();
}
return true;
}
return false;
default: // should not happen
return false;
}
}
void FtpResponder::Diagnostics(MessageType mt)
{
GetPlatform()->MessageF(mt, "FTP state %d\n", (int)responderState);
}
// This must be called only for the main FTP port
void FtpResponder::ConnectionLost()
{
CloseDataPort();
NetworkResponder::ConnectionLost();
}
// Send our data over the main FTP port.
// We send outBuf first and then outStack. fileBeingSent is reserved for the data port.
void FtpResponder::SendData()
{
// Send our output buffer and output stack
for(;;)
{
if (outBuf == nullptr)
{
outBuf = outStack->Pop();
if (outBuf == nullptr)
{
break;
}
}
const size_t bytesLeft = outBuf->BytesLeft();
if (bytesLeft == 0)
{
outBuf = OutputBuffer::Release(outBuf);
}
else
{
const size_t sent = skt->Send(reinterpret_cast<const uint8_t *>(outBuf->UnreadData()), bytesLeft);
if (sent == 0)
{
// Check whether the connection has been closed
if (!skt->CanSend())
{
// The connection has been lost or the other end has closed it
if (reprap.Debug(moduleWebserver))
{
debugPrintf("Can't send anymore\n");
}
ConnectionLost();
}
return;
}
outBuf->Taken(sent); // tell the output buffer how much data we have taken
if (sent < bytesLeft)
{
return;
}
outBuf = OutputBuffer::Release(outBuf);
}
}
// If we get here then there is nothing left to send
skt->Send(); // tell the socket there is no more data
// If we are going to free up this responder after sending, then we must close the connection
if (stateAfterSending == ResponderState::free)
{
skt->Close();
skt = nullptr;
}
responderState = stateAfterSending;
}
// Send our data over the passive FTP data port.
// We send dataBuf first and then fileBeingSent.
void FtpResponder::SendPassiveData()
{
// Send our output buffers
while (dataBuf != nullptr)
{
const size_t bytesLeft = dataBuf->BytesLeft();
if (bytesLeft == 0)
{
dataBuf = OutputBuffer::Release(dataBuf);
}
else
{
const size_t sent = dataSocket->Send(reinterpret_cast<const uint8_t *>(dataBuf->UnreadData()), bytesLeft);
if (sent == 0)
{
// Check whether the connection has been closed
if (!dataSocket->CanSend())
{
if (reprap.Debug(moduleWebserver))
{
debugPrintf("Can't send anymore over the data port\n");
}
sendError = true;
dataSocket = nullptr;
if (fileBeingSent != nullptr)
{
fileBeingSent->Close();
fileBeingSent = nullptr;
}
responderState = ResponderState::pasvTransferComplete;
}
return;
}
// Tell the output buffer how much data we have taken
dataBuf->Taken(sent);
if (sent < bytesLeft)
{
return;
}
dataBuf = OutputBuffer::Release(dataBuf);
}
}
// If we get here then there are no output buffers left to send
// If we have a file to send, send it
if (fileBeingSent != nullptr && fileBuffer == nullptr)
{
fileBuffer = NetworkBuffer::Allocate();
if (fileBuffer == nullptr)
{
return; // no buffer available, try again later
}
}
// If we have a file buffer here, we must be in the process of sending a file
while (fileBuffer != nullptr)
{
if (fileBuffer->IsEmpty() && fileBeingSent != nullptr)
{
const int bytesRead = fileBuffer->ReadFromFile(fileBeingSent);
if (bytesRead != (int)NetworkBuffer::bufferSize)
{
// We had a read error or we reached the end of the file
fileBeingSent->Close();
fileBeingSent = nullptr;
}
}
if (fileBuffer->IsEmpty())
{
// Must have sent the whole file
fileBuffer->Release();
fileBuffer = nullptr;
}
else
{
const size_t remaining = fileBuffer->Remaining();
const size_t sent = dataSocket->Send(fileBuffer->UnreadData(), remaining);
if (sent == 0)
{
// Check whether the connection has been closed
if (!dataSocket->CanSend())
{
if (reprap.Debug(moduleWebserver))
{
debugPrintf("Can't send anymore\n");
}
sendError = true;
dataSocket = nullptr;
if (fileBeingSent != nullptr)
{
fileBeingSent->Close();
fileBeingSent = nullptr;
}
fileBuffer->Release();
fileBuffer = nullptr;
responderState = ResponderState::pasvTransferComplete;
}
return;
}
fileBuffer->Taken(sent);
if (sent < remaining)
{
return;
}
}
}
// If we get here then there is nothing left to send. Close it as well
dataSocket->Send(); // tell the socket there is no more data
dataSocket->Close();
dataSocket = nullptr;
responderState = ResponderState::pasvTransferComplete;
}
// Write some more upload data
void FtpResponder::DoUpload()
{
// Write incoming data to the file
const uint8_t *buffer;
size_t len;
if (dataSocket->ReadBuffer(buffer, len))
{
if (reprap.Debug(moduleWebserver))
{
GetPlatform()->MessageF(HOST_MESSAGE, "Writing %u bytes of upload data\n", len);
}
const bool success = fileBeingUploaded.Write(reinterpret_cast<const char*>(buffer), len);
dataSocket->Taken(len);
if (!success)
{
uploadError = true;
GetPlatform()->Message(GENERIC_MESSAGE, "Error: Could not write upload data!\n");
CancelUpload();
responderState = ResponderState::pasvTransferComplete;
return;
}
}
// Upload has finished if the connection is closed
if (!dataSocket->CanRead())
{
dataSocket = nullptr;
responderState = ResponderState::pasvTransferComplete;
FinishUpload(0, 0);
}
}
// Try to read some data from the main FTP port and return true
// if anything significant could be done
bool FtpResponder::ReadData()
{
bool readSomething = false;
char c;
while (!haveCompleteLine && skt->ReadChar(c))
{
CharFromClient(c);
readSomething = true;
}
if (!readSomething && !skt->CanRead())
{
ConnectionLost();
return true;
}
if (haveCompleteLine && (outBuf != nullptr || OutputBuffer::Allocate(outBuf)))
{
ProcessLine();
return true;
}
return readSomething;
}
// Keep track of incoming characters
void FtpResponder::CharFromClient(char c)
{
switch (c)
{
case 0:
break;
case '\r':
case '\n':
if (clientPointer != 0)
{
// This line is complete
clientMessage[clientPointer] = 0;
haveCompleteLine = true;
}
break;
default:
clientMessage[clientPointer++] = c;
// Make sure we don't overflow the line buffer
if (clientPointer > ARRAY_UPB(clientMessage))
{
clientPointer = 0;
GetPlatform()->Message(HOST_MESSAGE, "Webserver: Buffer overflow in FTP server!\n");
}
break;
}
}
// Process the next FTP command
void FtpResponder::ProcessLine()
{
if (reprap.Debug(moduleWebserver))
{
debugPrintf("FTP request '%s' (state %d)\n", clientMessage, (int)responderState);
}
haveCompleteLine = false;
clientPointer = 0;
switch (responderState)
{
case ResponderState::authenticating:
// don't check the user name
if (StringStartsWith(clientMessage, "USER"))
{
outBuf->copy("331 Please specify the password.\r\n");
Commit(ResponderState::authenticating);
}
// but check the password (if set)
else if (StringStartsWith(clientMessage, "PASS"))
{
const char *password = GetParameter("PASS");
if (reprap.NoPasswordSet() || reprap.CheckPassword(password))
{
strcpy(currentDirectory, "/");
outBuf->copy("230 Login successful.\r\n");
Commit(ResponderState::reading);
}
else
{
outBuf->copy("530 Login incorrect.\r\n");
Commit();
}
}
// end connection
else if (StringEquals(clientMessage, "QUIT"))
{
outBuf->copy("221 Goodbye.\r\n");
Commit();
}
// if it's different, send response 500 to indicate we don't know the code (might be AUTH or so)
else
{
outBuf->copy("500 Unknown login command.\r\n");
Commit(ResponderState::authenticating);
}
break;
case ResponderState::reading:
// get system type
if (StringEquals(clientMessage, "SYST"))
{
outBuf->copy("215 UNIX Type: L8\r\n");
Commit(ResponderState::reading);
}
// get features
else if (StringEquals(clientMessage, "FEAT"))
{
outBuf->copy( "211-Features:\r\n"
"PASV\r\n" // support PASV mode
"211 End\r\n"
);
Commit(ResponderState::reading);
}
// get current dir
else if (StringEquals(clientMessage, "PWD"))
{
outBuf->printf("257 \"%s\"\r\n", currentDirectory);
Commit(ResponderState::reading);
}
// set current dir
else if (StringStartsWith(clientMessage, "CWD"))
{
const char *directory = GetParameter("CWD");
ChangeDirectory(directory);
}
// change to parent of current directory
else if (StringEquals(clientMessage, "CDUP"))
{
ChangeDirectory("..");
}
// switch transfer mode (sends response, but doesn't have any effects)
else if (StringStartsWith(clientMessage, "TYPE"))
{
const char *type = GetParameter("TYPE");
if (StringEquals(type, "I"))
{
outBuf->copy("200 Switching to Binary mode.\r\n");
Commit(ResponderState::reading);
}
else if (StringEquals(type, "A"))
{
outBuf->copy("200 Switching to ASCII mode.\r\n");
Commit(ResponderState::reading);
}
else
{
outBuf->copy("500 Unknown command.\r\n");
Commit(ResponderState::reading);
}
}
// enter passive mode mode
else if (StringEquals(clientMessage, "PASV"))
{
// reset error conditions
uploadError = sendError = false;
// open random port > 1023
passivePort = random(1024, 65535);
passivePortOpenTime = millis();
GetNetwork()->OpenDataPort(passivePort);
if (reprap.Debug(moduleWebserver))
{
debugPrintf("FTP data port open at port %u\n", passivePort);
}
// send FTP response
const uint8_t * const ipAddress = GetNetwork()->GetIPAddress();
outBuf->printf("227 Entering Passive Mode (%d,%d,%d,%d,%d,%d)\r\n",
ipAddress[0], ipAddress[1], ipAddress[2], ipAddress[3],
passivePort / 256, passivePort % 256);
Commit(ResponderState::waitingForPasvPort);
}
// PASV commands are not supported in this state
else if (StringEquals(clientMessage, "LIST") || StringStartsWith(clientMessage, "RETR") || StringStartsWith(clientMessage, "STOR"))
{
outBuf->copy("425 Use PASV first.\r\n");
Commit(ResponderState::reading);
}
// delete file
else if (StringStartsWith(clientMessage, "DELE"))
{
const char *filename = GetParameter("DELE");
if (GetPlatform()->GetMassStorage()->Delete(currentDirectory, filename))
{
outBuf->copy("250 Delete operation successful.\r\n");
}
else
{
outBuf->copy("550 Delete operation failed.\r\n");
}
Commit(ResponderState::reading);
}
// delete directory
else if (StringStartsWith(clientMessage, "RMD"))
{
const char *filename = GetParameter("RMD");
if (GetPlatform()->GetMassStorage()->Delete(currentDirectory, filename))
{
outBuf->copy("250 Remove directory operation successful.\r\n");
}
else
{
outBuf->copy("550 Remove directory operation failed.\r\n");
}
Commit(ResponderState::reading);
}
// make new directory
else if (StringStartsWith(clientMessage, "MKD"))
{
const char *filename = GetParameter("MKD");
const char *location = (filename[0] == '/')
? filename
: GetPlatform()->GetMassStorage()->CombineName(currentDirectory, filename);
if (GetPlatform()->GetMassStorage()->MakeDirectory(location))
{
outBuf->printf("257 \"%s\" created\r\n", location);
}
else
{
outBuf->copy("550 Create directory operation failed.\r\n");
}
Commit(ResponderState::reading);
}
// rename file or directory
else if (StringStartsWith(clientMessage, "RNFR"))
{
const char *filename = GetParameter("RNFR");
if (filename[0] != '/')
{
filename = GetPlatform()->GetMassStorage()->CombineName(currentDirectory, filename);
}
SafeStrncpy(fileToMove, filename, ARRAY_SIZE(fileToMove));
if (GetPlatform()->GetMassStorage()->FileExists(fileToMove))
{
outBuf->copy("350 Ready to RNTO.\r\n");
}
else
{
outBuf->copy("550 Invalid file or directory.\r\n");
}
Commit(ResponderState::reading);
}
else if (StringStartsWith(clientMessage, "RNTO"))
{
const char *filename = GetParameter("RNTO");
if (filename[0] != '/')
{
filename = GetPlatform()->GetMassStorage()->CombineName(currentDirectory, filename);
}
if (GetPlatform()->GetMassStorage()->Rename(fileToMove, filename))
{
outBuf->copy("250 Rename successful.\r\n");
}
else
{
outBuf->copy("500 Could not rename file or directory\r\n");
}
Commit(ResponderState::reading);
}
// no op
else if (StringEquals(clientMessage, "NOOP"))
{
outBuf->copy("200 NOOP okay.\r\n");
Commit(ResponderState::reading);
}
// end connection
else if (StringEquals(clientMessage, "QUIT"))
{
outBuf->copy("221 Goodbye.\r\n");
Commit();
}
// unknown
else
{
outBuf->copy("500 Unknown command.\r\n");
Commit(ResponderState::reading);
}
break;
case ResponderState::pasvPortOpened:
// list directory entries
if (StringEquals(clientMessage, "LIST"))
{
// send announcement via ftp main port
outBuf->copy("150 Here comes the directory listing.\r\n");
Commit(ResponderState::sendingPasvData);
// build directory listing, dataBuf is sent later in the Spin loop
MassStorage * const massStorage = GetPlatform()->GetMassStorage();
FileInfo fileInfo;
if (massStorage->FindFirst(currentDirectory, fileInfo))
{
do {
// Example for a typical UNIX-like file list:
// "drwxr-xr-x 2 ftp ftp 0 Apr 11 2013 bin\r\n"
const char dirChar = (fileInfo.isDirectory) ? 'd' : '-';
const struct tm * const timeInfo = gmtime(&fileInfo.lastModified);
dataBuf->catf("%crw-rw-rw- 1 ftp ftp %13lu %s %02d %04d %s\r\n",
dirChar, fileInfo.size, massStorage->GetMonthName(timeInfo->tm_mon + 1),
timeInfo->tm_mday, timeInfo->tm_year + 1900, fileInfo.fileName);
} while (massStorage->FindNext(fileInfo));
}
}
// upload a file
else if (StringStartsWith(clientMessage, "STOR"))
{
const char *filename = GetParameter("STOR");
FileStore *file = GetPlatform()->GetFileStore(currentDirectory, filename, true);
if (file != nullptr)
{
StartUpload(file, filename);
outBuf->copy("150 OK to send data.\r\n");
Commit(ResponderState::uploading);
}
else
{
outBuf->copy("550 Failed to open file.\r\n");
Commit(ResponderState::reading);
}
}
// download a file
else if (StringStartsWith(clientMessage, "RETR"))
{
const char *filename = GetParameter("RETR");
fileBeingSent = GetPlatform()->GetFileStore(currentDirectory, filename, false);
if (fileBeingSent != nullptr)
{
outBuf->printf("150 Opening data connection for %s (%lu bytes).\r\n", filename, fileBeingSent->Length());
Commit(ResponderState::sendingPasvData);
}
else
{
outBuf->copy("550 Failed to open file.\r\n");
Commit(ResponderState::reading);
}
}
// abort current operation
else if (StringEquals(clientMessage, "ABOR"))
{
CloseDataPort();
outBuf->copy("226 ABOR successful.\r\n");
Commit(ResponderState::reading);
}
// end connection
else if (StringEquals(clientMessage, "QUIT"))
{
CloseDataPort();
outBuf->copy("221 Goodbye.\r\n");
Commit();
}
// unknown command
else
{
outBuf->copy("500 Unknown command.\r\n");
Commit(ResponderState::pasvPortOpened);
}
break;
case ResponderState::uploading:
case ResponderState::sendingPasvData:
// abort current transfer
if (StringEquals(clientMessage, "ABOR"))
{
CancelUpload();
CloseDataPort();
outBuf->copy("226 ABOR successful.\r\n");
Commit(ResponderState::reading);
}
// in theory we could add support for QUIT here but it's not worth the effort
// unknown command
else
{
outBuf->copy("500 Unknown command.\r\n");
Commit(responderState);
}
break;
default:
// do nothing
break;
}
}
const char *FtpResponder::GetParameter(const char *after) const
{
const size_t commandLength = strlen(after);
if (commandLength >= ftpMessageLength)
{
return "";
}
const char *result = clientMessage + strlen(after) + 1;
while ((*result == '\t' || *result == ' ') && *result != 0)
{
++result;
}
return result;
}
void FtpResponder::ChangeDirectory(const char *newDirectory)
{
char combinedPath[FILENAME_LENGTH];
if (newDirectory[0] != 0)
{
// Prepare the new directory path
if (newDirectory[0] == '/') // Absolute path
{
SafeStrncpy(combinedPath, newDirectory, ARRAY_SIZE(combinedPath));
}
else // Relative path
{
if (StringEquals(newDirectory, "..")) // Go up
{
// Check if we're already at the root directory
if (StringEquals(currentDirectory, "/"))
{
outBuf->copy("550 Failed to change directory.\r\n");
Commit(responderState);
return;
}
// No - find the parent directory
SafeStrncpy(combinedPath, currentDirectory, ARRAY_SIZE(combinedPath));
for(int i = strlen(combinedPath) - 2; i >= 0; i--)
{
if (combinedPath[i] == '/')
{
combinedPath[i + 1] = 0;
break;
}
}
}
else // Go to child directory
{
SafeStrncpy(combinedPath, currentDirectory, ARRAY_SIZE(combinedPath));
if (!StringEndsWith(combinedPath, "/") && strlen(combinedPath) > 1)
{
SafeStrncat(combinedPath, "/", ARRAY_SIZE(combinedPath));
}
SafeStrncat(combinedPath, newDirectory, ARRAY_SIZE(combinedPath));
}
}
// Make sure the new path does not end with a slash, else FatFs won't be able to see the directory
if (StringEndsWith(combinedPath, "/") && strlen(combinedPath) > 1)
{
combinedPath[strlen(combinedPath) - 1] = 0;
}
// Verify the final path and change it if possible
if (GetPlatform()->GetMassStorage()->DirectoryExists(combinedPath))
{
SafeStrncpy(currentDirectory, combinedPath, ARRAY_SIZE(currentDirectory));
outBuf->copy("250 Directory successfully changed.\r\n");
Commit(responderState);
}
else
{
outBuf->copy("550 Failed to change directory.\r\n");
Commit(responderState);
}
}
else
{
outBuf->copy("550 Failed to change directory.\r\n");
Commit(responderState);
}
}
void FtpResponder::CloseDataPort()
{
if (reprap.Debug(moduleWebserver))
{
debugPrintf("FTP data port is being closed\n");
}
if (dataSocket != nullptr)
{
dataSocket->Close();
dataSocket = nullptr;
}
GetNetwork()->CloseDataPort();
OutputBuffer::ReleaseAll(dataBuf);
dataBuf = nullptr;
if (fileBeingSent != nullptr)
{
fileBeingSent->Close();
fileBeingSent = nullptr;
}
}
// End