2528 lines
63 KiB
C++
2528 lines
63 KiB
C++
/****************************************************************************************************
|
|
|
|
RepRapFirmware - Webserver
|
|
|
|
This class serves a single-page web applications to the attached network. This page forms the user's
|
|
interface with the RepRap machine. This software interprests returned values from the page and uses it
|
|
to generate G Codes, which it sends to the RepRap. It also collects values from the RepRap like
|
|
temperature and uses those to construct the web page.
|
|
|
|
The page itself - reprap.htm - uses Jquery.js to perform AJAX. See:
|
|
|
|
http://jquery.com/
|
|
|
|
-----------------------------------------------------------------------------------------------------
|
|
|
|
Version 0.2
|
|
|
|
10 May 2013
|
|
|
|
Adrian Bowyer
|
|
RepRap Professional Ltd
|
|
http://reprappro.com
|
|
|
|
Licence: GPL
|
|
|
|
-----------------------------------------------------------------------------------------------------
|
|
|
|
The supported requests are GET requests for files (for which the root is the www directory on the
|
|
SD card), and the following. These all start with "/rr_". Ordinary files used for the web interface
|
|
must not have names starting "/rr_" or they will not be found.
|
|
|
|
rr_connect?password=xxx
|
|
Sent by the web interface software to establish an initial connection, indicating that
|
|
any state variables relating to the web interface (e.g. file upload in progress) should
|
|
be reset. This only happens if the password could be verified.
|
|
|
|
rr_fileinfo Returns file information about the file being printed.
|
|
|
|
rr_fileinfo?name=xxx
|
|
Returns file information about a file on the SD card or a JSON-encapsulated response
|
|
with err = 1 if the passed filename was invalid.
|
|
|
|
rr_status New-style status response, in which temperatures, axis positions and extruder positions
|
|
are returned in separate variables. Another difference is that extruder positions are
|
|
returned as absolute positions instead of relative to the previous gcode. A client
|
|
may also request different status responses by specifying the "type" keyword, followed
|
|
by a custom status response type. Also see "M105 S1".
|
|
|
|
rr_files?dir=xxx
|
|
Returns a listing of the filenames in the /gcode directory of the SD card. 'dir' is a
|
|
directory path relative to the root of the SD card. If the 'dir' variable is not present,
|
|
it defaults to the /gcode directory.
|
|
|
|
rr_reply Returns the last-known G-code reply as plain text (not encapsulated as JSON).
|
|
|
|
rr_upload?name=xxx
|
|
Upload a specified file using a POST request. The payload of this request has to be
|
|
the file content. Only one file may be uploaded at once. When the upload has finished,
|
|
a JSON response with the variable "err" will be returned, which will be 0 if the job
|
|
has finished without problems, it will be set to 1 otherwise.
|
|
|
|
rr_upload_begin?name=xxx
|
|
Indicates that we wish to upload the specified file. xxx is the filename relative
|
|
to the root of the SD card. The directory component of the filename must already
|
|
exist. Returns variables ubuff (= max upload data we can accept in the next message)
|
|
and err (= 0 if the file was created successfully, nonzero if there was an error).
|
|
|
|
rr_upload_data?data=xxx
|
|
Provides a data block for the file upload. Returns the samwe variables as rr_upload_begin,
|
|
except that err is only zero if the file was successfully created and there has not been
|
|
a file write error yet. This response is returned before attempting to write this data block.
|
|
|
|
rr_upload_end
|
|
Indicates that we have finished sending upload data. The server closes the file and reports
|
|
the overall status in err. It may also return ubuff again.
|
|
|
|
rr_upload_cancel
|
|
Indicates that the user wishes to cancel the current upload. Returns err and ubuff.
|
|
|
|
rr_delete?name=xxx
|
|
Delete file xxx. Returns err (zero if successful).
|
|
|
|
rr_mkdir?dir=xxx
|
|
Create a new directory xxx. Return err (zero if successful).
|
|
|
|
rr_move?old=xxx&new=yyy
|
|
Rename an old file xxx to yyy. May also be used to move a file to another directory.
|
|
|
|
****************************************************************************************************/
|
|
|
|
#include "RepRapFirmware.h"
|
|
|
|
//***************************************************************************************************
|
|
|
|
static const char* overflowResponse = "overflow";
|
|
static const char* badEscapeResponse = "bad escape";
|
|
|
|
const float pasvPortTimeout = 10.0; // seconds to wait for the FTP data port
|
|
|
|
|
|
//********************************************************************************************
|
|
//
|
|
//**************************** Generic Webserver implementation ******************************
|
|
//
|
|
//********************************************************************************************
|
|
|
|
|
|
|
|
// Constructor and initialisation
|
|
Webserver::Webserver(Platform* p, Network *n) : platform(p), network(n),
|
|
webserverActive(false), readingConnection(NULL)
|
|
{
|
|
httpInterpreter = new HttpInterpreter(p, this, n);
|
|
ftpInterpreter = new FtpInterpreter(p, this, n);
|
|
telnetInterpreter = new TelnetInterpreter(p, this, n);
|
|
}
|
|
|
|
void Webserver::Init()
|
|
{
|
|
// initialise the webserver class
|
|
gcodeReadIndex = gcodeWriteIndex = 0;
|
|
lastTime = platform->Time();
|
|
longWait = lastTime;
|
|
webserverActive = true;
|
|
|
|
// initialise all protocol handlers
|
|
httpInterpreter->ResetState();
|
|
httpInterpreter->ResetSessions();
|
|
ftpInterpreter->ResetState();
|
|
telnetInterpreter->ResetState();
|
|
}
|
|
|
|
|
|
// Deal with input/output from/to the client (if any)
|
|
void Webserver::Spin()
|
|
{
|
|
// Before we process an incoming Request, we must ensure that the webserver
|
|
// is active and that all upload buffers are empty.
|
|
|
|
if (webserverActive && httpInterpreter->FlushUploadData() && ftpInterpreter->FlushUploadData())
|
|
{
|
|
// Check if we can purge any HTTP sessions
|
|
|
|
httpInterpreter->CheckSessions();
|
|
|
|
// We must ensure that we have exclusive access to LWIP
|
|
|
|
if (!network->Lock())
|
|
{
|
|
platform->ClassReport(longWait);
|
|
return;
|
|
}
|
|
|
|
// See if we have new data to process
|
|
|
|
NetworkTransaction *req = network->GetTransaction(readingConnection);
|
|
if (req != NULL)
|
|
{
|
|
// Process incoming request
|
|
if (!req->LostConnection())
|
|
{
|
|
// Take care of different protocol types here
|
|
ProtocolInterpreter *interpreter;
|
|
uint16_t localPort = req->GetLocalPort();
|
|
switch (localPort)
|
|
{
|
|
case ftpPort: /* FTP */
|
|
interpreter = ftpInterpreter;
|
|
break;
|
|
|
|
case telnetPort: /* Telnet */
|
|
interpreter = telnetInterpreter;
|
|
break;
|
|
|
|
default: /* FTP data */
|
|
if (localPort == network->GetHttpPort())
|
|
{
|
|
interpreter = httpInterpreter;
|
|
}
|
|
else
|
|
{
|
|
interpreter = ftpInterpreter;
|
|
}
|
|
break;
|
|
}
|
|
|
|
// For protocols other than HTTP it is important to send a HELO message
|
|
TransactionStatus status = req->GetStatus();
|
|
if (status == connected)
|
|
{
|
|
interpreter->ConnectionEstablished();
|
|
|
|
// Close this request unless ConnectionEstablished() has already used it for sending
|
|
if (req == network->GetTransaction())
|
|
{
|
|
network->CloseTransaction();
|
|
}
|
|
}
|
|
// Graceful disconnects are handled here, because prior NetworkTransactions might still contain valid
|
|
// data. That's why it's a bad idea to close these connections immediately in the Network class.
|
|
else if (status == disconnected)
|
|
{
|
|
// CloseRequest() will call the disconnect events and close the connection
|
|
network->CloseTransaction();
|
|
}
|
|
// Check for fast uploads
|
|
else if (interpreter->DoingFastUpload())
|
|
{
|
|
if (!interpreter->DoFastUpload())
|
|
{
|
|
// Ensure this connection won't block everything if anything goes wrong
|
|
readingConnection = NULL;
|
|
}
|
|
}
|
|
// Check if we need to send data to a Telnet client
|
|
else if (interpreter == telnetInterpreter && telnetInterpreter->HasRemainingData())
|
|
{
|
|
network->SendAndClose(NULL, true);
|
|
telnetInterpreter->RemainingDataSent();
|
|
}
|
|
// Process other messages
|
|
else
|
|
{
|
|
char c;
|
|
for (uint16_t i=0; i<500; i++)
|
|
{
|
|
if (req->Read(c))
|
|
{
|
|
// Each ProtocolInterpreter must take care of the current NetworkTransaction and remove
|
|
// it from the ready transactions by either calling SendAndClose() or CloseRequest().
|
|
if (interpreter->CharFromClient(c))
|
|
{
|
|
readingConnection = NULL;
|
|
break;
|
|
}
|
|
}
|
|
else
|
|
{
|
|
// We ran out of data before finding a complete request.
|
|
// This happens when the incoming message length exceeds the TCP MSS.
|
|
// Check if we need to process another packet on the same connection.
|
|
readingConnection = (interpreter->NeedMoreData()) ? req->GetConnection() : NULL;
|
|
network->CloseTransaction();
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
else
|
|
{
|
|
platform->Message(HOST_MESSAGE, "Webserver: Skipping zombie request with status %d\n", req->GetStatus());
|
|
network->CloseTransaction();
|
|
}
|
|
}
|
|
|
|
network->Unlock();
|
|
platform->ClassReport(longWait);
|
|
}
|
|
}
|
|
|
|
void Webserver::Exit()
|
|
{
|
|
httpInterpreter->CancelUpload();
|
|
ftpInterpreter->CancelUpload();
|
|
|
|
platform->Message(BOTH_MESSAGE, "Webserver class exited.\n");
|
|
webserverActive = false;
|
|
}
|
|
|
|
void Webserver::Diagnostics()
|
|
{
|
|
platform->AppendMessage(BOTH_MESSAGE, "Webserver Diagnostics:\n");
|
|
}
|
|
|
|
// Process a null-terminated gcode
|
|
// We intercept four G/M Codes so we can deal with file manipulation and emergencies. That
|
|
// way things don't get out of sync, and - as a file name can contain
|
|
// a valid G code (!) - confusion is avoided.
|
|
void Webserver::ProcessGcode(const char* gc)
|
|
{
|
|
if (StringStartsWith(gc, "M112") && !isdigit(gc[4])) // emergency stop
|
|
{
|
|
reprap.EmergencyStop();
|
|
gcodeReadIndex = gcodeWriteIndex; // clear the buffer
|
|
reprap.GetGCodes()->Reset();
|
|
}
|
|
else if (StringStartsWith(gc, "M503") && !isdigit(gc[4])) // echo config.g file
|
|
{
|
|
FileStore *configFile = platform->GetFileStore(platform->GetSysDir(), platform->GetConfigFile(), false);
|
|
if (configFile == NULL)
|
|
{
|
|
ResponseToWebInterface("Configuration file not found", true);
|
|
}
|
|
else
|
|
{
|
|
reprap.MessageToGCodeReply("");
|
|
|
|
char c;
|
|
bool reading_whitespace = false;
|
|
while (configFile->Read(c))
|
|
{
|
|
if (!reading_whitespace || (c != ' ' && c != '\t'))
|
|
{
|
|
reprap.AppendCharToStatusResponse(c);
|
|
}
|
|
reading_whitespace = (c == ' ' || c == '\t');
|
|
}
|
|
configFile->Close();
|
|
|
|
telnetInterpreter->HandleGcodeReply(reprap.GetGcodeReply().Pointer());
|
|
}
|
|
}
|
|
else
|
|
{
|
|
StoreGcodeData(gc, strlen(gc) + 1);
|
|
}
|
|
}
|
|
|
|
// Feeding G Codes to the GCodes class
|
|
bool Webserver::GCodeAvailable()
|
|
{
|
|
return gcodeReadIndex != gcodeWriteIndex;
|
|
}
|
|
|
|
char Webserver::ReadGCode()
|
|
{
|
|
char c;
|
|
if (gcodeReadIndex == gcodeWriteIndex)
|
|
{
|
|
c = 0;
|
|
}
|
|
else
|
|
{
|
|
c = gcodeBuffer[gcodeReadIndex];
|
|
gcodeReadIndex = (gcodeReadIndex + 1u) % gcodeBufferLength;
|
|
}
|
|
return c;
|
|
}
|
|
|
|
// Process a received string of gcodes
|
|
void Webserver::LoadGcodeBuffer(const char* gc)
|
|
{
|
|
char gcodeTempBuf[GcodeLength];
|
|
uint16_t gtp = 0;
|
|
bool inComment = false;
|
|
for (;;)
|
|
{
|
|
char c = *gc++;
|
|
if (c == 0)
|
|
{
|
|
gcodeTempBuf[gtp] = 0;
|
|
ProcessGcode(gcodeTempBuf);
|
|
return;
|
|
}
|
|
|
|
if (c == '\n')
|
|
{
|
|
gcodeTempBuf[gtp] = 0;
|
|
ProcessGcode(gcodeTempBuf);
|
|
gtp = 0;
|
|
inComment = false;
|
|
}
|
|
else
|
|
{
|
|
if (c == ';')
|
|
{
|
|
inComment = true;
|
|
}
|
|
|
|
if (gtp == ARRAY_UPB(gcodeTempBuf))
|
|
{
|
|
// gcode is too long, we haven't room for another character and a null
|
|
if (c != ' ' && !inComment)
|
|
{
|
|
platform->Message(BOTH_ERROR_MESSAGE, "Webserver: GCode local buffer overflow.\n");
|
|
return;
|
|
}
|
|
// else we're either in a comment or the current character is a space.
|
|
// If we're in a comment, we'll silently truncate it.
|
|
// If the current character is a space, we'll wait until we see a non-comment character before reporting an error,
|
|
// in case the next character is end-of-line or the start of a comment.
|
|
}
|
|
else
|
|
{
|
|
gcodeTempBuf[gtp++] = c;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// Process a received string of gcodes
|
|
void Webserver::StoreGcodeData(const char* data, size_t len)
|
|
{
|
|
if (len > GetGcodeBufferSpace())
|
|
{
|
|
platform->Message(BOTH_ERROR_MESSAGE, "GCode buffer overflow in Webserver!\n");
|
|
}
|
|
else
|
|
{
|
|
size_t remaining = gcodeBufferLength - gcodeWriteIndex;
|
|
if (len <= remaining)
|
|
{
|
|
memcpy(gcodeBuffer + gcodeWriteIndex, data, len);
|
|
}
|
|
else
|
|
{
|
|
memcpy(gcodeBuffer + gcodeWriteIndex, data, remaining);
|
|
memcpy(gcodeBuffer, data + remaining, len - remaining);
|
|
}
|
|
gcodeWriteIndex = (gcodeWriteIndex + len) % gcodeBufferLength;
|
|
}
|
|
}
|
|
|
|
// Handle immediate disconnects here (cs will be freed after this call)
|
|
void Webserver::ConnectionLost(const ConnectionState *cs)
|
|
{
|
|
// See which connection caused this event
|
|
uint32_t remoteIP = cs->GetRemoteIP();
|
|
uint16_t remotePort = cs->GetRemotePort();
|
|
uint16_t localPort = cs->GetLocalPort();
|
|
|
|
// Inform protocol handlers that this connection has been lost
|
|
ProtocolInterpreter *interpreter;
|
|
switch (localPort)
|
|
{
|
|
case ftpPort: /* FTP */
|
|
interpreter = ftpInterpreter;
|
|
break;
|
|
|
|
case telnetPort: /* Telnet */
|
|
interpreter = telnetInterpreter;
|
|
break;
|
|
|
|
default: /* FTP data */
|
|
if (localPort == network->GetHttpPort())
|
|
{
|
|
interpreter = httpInterpreter;
|
|
break;
|
|
}
|
|
else if (localPort == network->GetDataPort())
|
|
{
|
|
interpreter = ftpInterpreter;
|
|
break;
|
|
}
|
|
|
|
platform->Message(BOTH_ERROR_MESSAGE, "Webserver: Connection closed at local port %d, but no handler found!\n", localPort);
|
|
return;
|
|
}
|
|
if (reprap.Debug(moduleWebserver))
|
|
{
|
|
platform->Message(HOST_MESSAGE, "Webserver: ConnectionLost called with port %d\n", localPort);
|
|
}
|
|
interpreter->ConnectionLost(remoteIP, remotePort, localPort);
|
|
|
|
// If our reading connection is lost, it will be no longer important which connection is read from first.
|
|
if (cs == readingConnection)
|
|
{
|
|
readingConnection = NULL;
|
|
}
|
|
}
|
|
|
|
void Webserver::ResponseToWebInterface(const char *s, bool error)
|
|
{
|
|
if (!webserverActive)
|
|
{
|
|
return;
|
|
}
|
|
|
|
if (strlen(s) == 0 && !error)
|
|
{
|
|
reprap.MessageToGCodeReply(s);
|
|
telnetInterpreter->HandleGcodeReply("ok\r\n");
|
|
}
|
|
else
|
|
{
|
|
if (error)
|
|
{
|
|
reprap.MessageToGCodeReply("Error: ");
|
|
reprap.AppendMessageToGCodeReply(s);
|
|
|
|
telnetInterpreter->HandleGcodeReply("Error: ");
|
|
telnetInterpreter->HandleGcodeReply(s);
|
|
}
|
|
else
|
|
{
|
|
reprap.MessageToGCodeReply(s);
|
|
telnetInterpreter->HandleGcodeReply(s);
|
|
}
|
|
}
|
|
}
|
|
|
|
void Webserver::AppendResponseToWebInterface(const char *s)
|
|
{
|
|
if (!webserverActive)
|
|
{
|
|
return;
|
|
}
|
|
|
|
reprap.AppendMessageToGCodeReply(s);
|
|
telnetInterpreter->HandleGcodeReply(s);
|
|
}
|
|
|
|
//********************************************************************************************
|
|
//
|
|
//********************** Generic Procotol Interpreter implementation *************************
|
|
//
|
|
//********************************************************************************************
|
|
|
|
ProtocolInterpreter::ProtocolInterpreter(Platform *p, Webserver *ws, Network *n)
|
|
: platform(p), webserver(ws), network(n)
|
|
{
|
|
uploadState = notUploading;
|
|
uploadPointer = NULL;
|
|
uploadLength = 0;
|
|
filenameBeingUploaded[0] = 0;
|
|
}
|
|
|
|
// Start writing to a new file
|
|
bool ProtocolInterpreter::StartUpload(FileStore *file)
|
|
{
|
|
CancelUpload();
|
|
|
|
if (file != NULL)
|
|
{
|
|
fileBeingUploaded.Set(file);
|
|
uploadState = uploadOK;
|
|
return true;
|
|
}
|
|
|
|
uploadState = uploadError;
|
|
platform->Message(HOST_MESSAGE, "Could not open file while starting upload!\n");
|
|
return false;
|
|
}
|
|
|
|
// Process a received buffer of upload data
|
|
bool ProtocolInterpreter::StoreUploadData(const char* data, unsigned int len)
|
|
{
|
|
if (uploadState == uploadOK)
|
|
{
|
|
uploadPointer = data;
|
|
uploadLength = len;
|
|
return true;
|
|
}
|
|
return false;
|
|
}
|
|
|
|
// Try to flush upload buffer and return true if all data has been flushed
|
|
bool ProtocolInterpreter::FlushUploadData()
|
|
{
|
|
if (uploadState == uploadOK && uploadLength != 0)
|
|
{
|
|
// Write some uploaded data to file (never write more than 256 bytes at once)
|
|
unsigned int len = min<unsigned int>(uploadLength, 256);
|
|
if (!fileBeingUploaded.Write(uploadPointer, len))
|
|
{
|
|
platform->Message(HOST_MESSAGE, "Could not flush upload data!\n");
|
|
uploadState = uploadError;
|
|
}
|
|
|
|
uploadPointer += len;
|
|
uploadLength -= len;
|
|
|
|
return (uploadLength == 0);
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
void ProtocolInterpreter::CancelUpload()
|
|
{
|
|
if (fileBeingUploaded.IsLive())
|
|
{
|
|
fileBeingUploaded.Close(); // cancel any pending file upload
|
|
if (strlen(filenameBeingUploaded) != 0)
|
|
{
|
|
platform->GetMassStorage()->Delete("0:/", filenameBeingUploaded);
|
|
}
|
|
}
|
|
filenameBeingUploaded[0] = 0;
|
|
uploadPointer = NULL;
|
|
uploadLength = 0;
|
|
uploadState = notUploading;
|
|
}
|
|
|
|
bool ProtocolInterpreter::DoFastUpload()
|
|
{
|
|
NetworkTransaction *req = network->GetTransaction();
|
|
if (IsUploading())
|
|
{
|
|
char *buffer;
|
|
unsigned int len;
|
|
if (req->ReadBuffer(buffer, len))
|
|
{
|
|
StoreUploadData(buffer, len);
|
|
}
|
|
else
|
|
{
|
|
network->CloseTransaction();
|
|
}
|
|
}
|
|
else if (req->DataLength() > 0)
|
|
{
|
|
platform->Message(HOST_MESSAGE, "Webserver: Closing invalid data connection\n");
|
|
network->SendAndClose(NULL);
|
|
return false;
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
void ProtocolInterpreter::FinishUpload(uint32_t fileLength)
|
|
{
|
|
// Write the remaining data
|
|
if (uploadState == uploadOK)
|
|
{
|
|
while (uploadLength > 0)
|
|
{
|
|
unsigned int len = min<unsigned int>(uploadLength, 256);
|
|
if (!fileBeingUploaded.Write(uploadPointer, len))
|
|
{
|
|
uploadState = uploadError;
|
|
platform->Message(HOST_MESSAGE, "Could not write remaining data while finishing upload!\n");
|
|
break;
|
|
}
|
|
|
|
uploadLength -= len;
|
|
uploadPointer += len;
|
|
}
|
|
}
|
|
|
|
uploadPointer = NULL;
|
|
uploadLength = 0;
|
|
|
|
if (uploadState == uploadOK && !fileBeingUploaded.Flush())
|
|
{
|
|
uploadState = uploadError;
|
|
platform->Message(HOST_MESSAGE, "Could not flush remaining data while finishing upload!\n");
|
|
}
|
|
|
|
// Check the file length is as expected
|
|
if (uploadState == uploadOK && fileLength != 0 && fileBeingUploaded.Length() != fileLength)
|
|
{
|
|
uploadState = uploadError;
|
|
platform->Message(HOST_MESSAGE, "Uploaded file size is different (%u vs. expected %u Bytes)!\n", fileBeingUploaded.Length(), fileLength);
|
|
}
|
|
|
|
// Close the file
|
|
if (!fileBeingUploaded.Close())
|
|
{
|
|
uploadState = uploadError;
|
|
platform->Message(HOST_MESSAGE, "Could not close the upload file while finishing upload!\n");
|
|
}
|
|
|
|
// Delete file if an error has occurred
|
|
if (uploadState == uploadError && strlen(filenameBeingUploaded) != 0)
|
|
{
|
|
platform->GetMassStorage()->Delete("0:/", filenameBeingUploaded);
|
|
}
|
|
filenameBeingUploaded[0] = 0;
|
|
}
|
|
|
|
|
|
|
|
//********************************************************************************************
|
|
//
|
|
// *********************** HTTP interpreter for the Webserver class **************************
|
|
//
|
|
//********************************************************************************************
|
|
|
|
Webserver::HttpInterpreter::HttpInterpreter(Platform *p, Webserver *ws, Network *n)
|
|
: ProtocolInterpreter(p, ws, n), state(doingCommandWord)
|
|
{
|
|
uploadingTextData = false;
|
|
numContinuationBytes = 0;
|
|
}
|
|
|
|
// File Uploads
|
|
|
|
bool Webserver::HttpInterpreter::DoFastUpload()
|
|
{
|
|
bool success = ProtocolInterpreter::DoFastUpload();
|
|
uploadedBytes += uploadLength;
|
|
|
|
// See if we can finish it this time
|
|
if (uploadedBytes >= postFileLength)
|
|
{
|
|
// Reset POST upload state for this client
|
|
uint32_t remoteIP = network->GetTransaction()->GetRemoteIP();
|
|
for(size_t i=0; i<numActiveSessions; i++)
|
|
{
|
|
if (sessions[i].ip == remoteIP && sessions[i].isPostUploading)
|
|
{
|
|
sessions[i].isPostUploading = false;
|
|
break;
|
|
}
|
|
}
|
|
|
|
// We're done, flush the remaining upload data and send the JSON response
|
|
FinishUpload(postFileLength);
|
|
SendJsonResponse("upload");
|
|
uploadState = notUploading;
|
|
}
|
|
|
|
return success;
|
|
}
|
|
|
|
bool Webserver::HttpInterpreter::DoingFastUpload() const
|
|
{
|
|
if (state == doingPost)
|
|
{
|
|
// Always finish the current request before checking for fast POST uploads
|
|
return false;
|
|
}
|
|
|
|
uint32_t remoteIP = network->GetTransaction()->GetRemoteIP();
|
|
uint16_t remotePort = network->GetTransaction()->GetRemotePort();
|
|
for(size_t i=0; i<numActiveSessions; i++)
|
|
{
|
|
if (sessions[i].ip == remoteIP && sessions[i].isPostUploading)
|
|
{
|
|
return (remotePort == sessions[i].postPort);
|
|
}
|
|
}
|
|
return false;
|
|
}
|
|
|
|
bool Webserver::HttpInterpreter::StartUpload(FileStore *file)
|
|
{
|
|
numContinuationBytes = 0;
|
|
return ProtocolInterpreter::StartUpload(file);
|
|
}
|
|
|
|
bool Webserver::HttpInterpreter::StoreUploadData(const char* data, unsigned int len)
|
|
{
|
|
if (uploadState == uploadOK)
|
|
{
|
|
uploadPointer = data;
|
|
uploadLength = len;
|
|
|
|
// Count the number of UTF8 continuation bytes. We may need it to adjust the expected file length.
|
|
if (uploadingTextData)
|
|
{
|
|
while (len != 0)
|
|
{
|
|
if ((*data & 0xC0) == 0x80)
|
|
{
|
|
++numContinuationBytes;
|
|
}
|
|
++data;
|
|
--len;
|
|
}
|
|
}
|
|
|
|
return true;
|
|
}
|
|
return false;
|
|
}
|
|
|
|
void Webserver::HttpInterpreter::CancelUpload()
|
|
{
|
|
CancelUpload(network->GetTransaction()->GetRemoteIP());
|
|
}
|
|
|
|
void Webserver::HttpInterpreter::CancelUpload(uint32_t remoteIP)
|
|
{
|
|
for(size_t i=0; i<numActiveSessions; i++)
|
|
{
|
|
if (sessions[i].ip == remoteIP && sessions[i].isPostUploading)
|
|
{
|
|
sessions[i].isPostUploading = false;
|
|
sessions[i].lastQueryTime = platform->Time();
|
|
break;
|
|
}
|
|
}
|
|
|
|
ProtocolInterpreter::CancelUpload();
|
|
}
|
|
|
|
// Output to the client
|
|
|
|
// Start sending a file or a JSON response.
|
|
void Webserver::HttpInterpreter::SendFile(const char* nameOfFileToSend)
|
|
{
|
|
if (StringEquals(nameOfFileToSend, "/"))
|
|
{
|
|
nameOfFileToSend = INDEX_PAGE;
|
|
}
|
|
FileStore *fileToSend = platform->GetFileStore(platform->GetWebDir(), nameOfFileToSend, false);
|
|
if (fileToSend == NULL)
|
|
{
|
|
nameOfFileToSend = FOUR04_FILE;
|
|
fileToSend = platform->GetFileStore(platform->GetWebDir(), nameOfFileToSend, false);
|
|
if (fileToSend == NULL)
|
|
{
|
|
RejectMessage("not found", 404);
|
|
return;
|
|
}
|
|
}
|
|
|
|
NetworkTransaction *req = network->GetTransaction();
|
|
req->Write("HTTP/1.1 200 OK\n");
|
|
|
|
const char* contentType;
|
|
bool zip = false;
|
|
if (StringEndsWith(nameOfFileToSend, ".png"))
|
|
{
|
|
contentType = "image/png";
|
|
}
|
|
else if (StringEndsWith(nameOfFileToSend, ".ico"))
|
|
{
|
|
contentType = "image/x-icon";
|
|
}
|
|
else if (StringEndsWith(nameOfFileToSend, ".js"))
|
|
{
|
|
contentType = "application/javascript";
|
|
}
|
|
else if (StringEndsWith(nameOfFileToSend, ".css"))
|
|
{
|
|
contentType = "text/css";
|
|
}
|
|
else if (StringEndsWith(nameOfFileToSend, ".htm") || StringEndsWith(nameOfFileToSend, ".html"))
|
|
{
|
|
contentType = "text/html";
|
|
}
|
|
else if (StringEndsWith(nameOfFileToSend, ".zip"))
|
|
{
|
|
contentType = "application/zip";
|
|
zip = true;
|
|
}
|
|
else
|
|
{
|
|
contentType = "application/octet-stream";
|
|
}
|
|
req->Printf("Content-Type: %s\n", contentType);
|
|
|
|
if (zip && fileToSend != NULL)
|
|
{
|
|
req->Write("Content-Encoding: gzip\n");
|
|
req->Printf("Content-Length: %lu", fileToSend->Length());
|
|
}
|
|
|
|
req->Write("Connection: close\n\n");
|
|
network->SendAndClose(fileToSend);
|
|
}
|
|
|
|
void Webserver::HttpInterpreter::SendGCodeReply()
|
|
{
|
|
NetworkTransaction *req = network->GetTransaction();
|
|
req->Write("HTTP/1.1 200 OK\n");
|
|
req->Write("Content-Type: text/plain\n");
|
|
req->Printf("Content-Length: %u\n", reprap.GetGcodeReply().strlen());
|
|
req->Write("Connection: close\n\n");
|
|
req->Write(reprap.GetGcodeReply());
|
|
network->SendAndClose(NULL);
|
|
}
|
|
|
|
void Webserver::HttpInterpreter::SendJsonResponse(const char* command)
|
|
{
|
|
// rr_reply is treated differently, because it (currently) responds as "text/plain"
|
|
if (IsAuthenticated() && StringEquals(command, "reply"))
|
|
{
|
|
SendGCodeReply();
|
|
return;
|
|
}
|
|
|
|
// See if we can find a suitable JSON response
|
|
NetworkTransaction *req = network->GetTransaction();
|
|
bool keepOpen = false;
|
|
bool mayKeepOpen;
|
|
bool found;
|
|
char jsonResponseBuffer[jsonReplyLength];
|
|
StringRef jsonResponse(jsonResponseBuffer, ARRAY_SIZE(jsonResponseBuffer));
|
|
if (numQualKeys == 0)
|
|
{
|
|
found = GetJsonResponse(command, jsonResponse, "", "", 0, mayKeepOpen);
|
|
}
|
|
else
|
|
{
|
|
found = GetJsonResponse(command, jsonResponse, qualifiers[0].key, qualifiers[0].value, qualifiers[1].key - qualifiers[0].value - 1, mayKeepOpen);
|
|
}
|
|
|
|
if (found)
|
|
{
|
|
jsonResponseBuffer[ARRAY_UPB(jsonResponseBuffer)] = 0;
|
|
if (reprap.Debug(moduleWebserver))
|
|
{
|
|
platform->Message(HOST_MESSAGE, "JSON response: %s queued\n", jsonResponseBuffer);
|
|
}
|
|
}
|
|
else
|
|
{
|
|
jsonResponseBuffer[0] = 0;
|
|
platform->Message(HOST_MESSAGE, "KnockOut request: %s not recognised\n", command);
|
|
}
|
|
|
|
if (mayKeepOpen)
|
|
{
|
|
// Check that the browser wants to persist the connection too
|
|
for (size_t i = 0; i < numHeaderKeys; ++i)
|
|
{
|
|
if (StringEquals(headers[i].key, "Connection"))
|
|
{
|
|
// Comment out the following line to disable persistent connections
|
|
keepOpen = StringEquals(headers[i].value, "keep-alive");
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
req->Write("HTTP/1.1 200 OK\n");
|
|
req->Write("Content-Type: application/json\n");
|
|
req->Printf("Content-Length: %u\n", jsonResponse.strlen());
|
|
req->Printf("Connection: %s\n\n", keepOpen ? "keep-alive" : "close");
|
|
req->Write(jsonResponse);
|
|
|
|
network->SendAndClose(NULL, keepOpen);
|
|
}
|
|
|
|
//----------------------------------------------------------------------------------------------------
|
|
|
|
// Input from the client
|
|
|
|
// Get the Json response for this command.
|
|
// 'value' is null-terminated, but we also pass its length in case it contains embedded nulls, which matters when uploading files.
|
|
bool Webserver::HttpInterpreter::GetJsonResponse(const char* request, StringRef& response, const char* key, const char* value, size_t valueLength, bool& keepOpen)
|
|
{
|
|
keepOpen = false; // assume we don't want to persist the connection
|
|
bool found = true; // assume success
|
|
|
|
if (!IsAuthenticated() && reprap.NoPasswordSet())
|
|
{
|
|
// Try to allocate a new HTTP session even if this is no connect request,
|
|
// because we may need to know if a POST upload is in progress.
|
|
Authenticate();
|
|
}
|
|
|
|
if (StringEquals(request, "connect") && StringEquals(key, "password"))
|
|
{
|
|
if (IsAuthenticated())
|
|
{
|
|
// This IP is already authenticated, no need to check the password again
|
|
response.copy("{\"err\":0}");
|
|
}
|
|
else if (reprap.CheckPassword(value))
|
|
{
|
|
if (Authenticate())
|
|
{
|
|
// This is only possible if we have at least one HTTP session left
|
|
response.copy("{\"err\":0}");
|
|
}
|
|
else
|
|
{
|
|
// Otherwise report an error
|
|
response.copy("{\"err\":2}");
|
|
}
|
|
}
|
|
else
|
|
{
|
|
// Wrong password
|
|
response.copy("{\"err\":1}");
|
|
}
|
|
}
|
|
else if (!IsAuthenticated())
|
|
{
|
|
// Don't respond if this IP is not authenticated
|
|
found = false;
|
|
}
|
|
else
|
|
{
|
|
UpdateAuthentication();
|
|
|
|
if (StringEquals(request, "disconnect"))
|
|
{
|
|
RemoveAuthentication();
|
|
response.copy("{\"err\":0}");
|
|
}
|
|
else if (StringEquals(request, "status"))
|
|
{
|
|
int type = 0;
|
|
if (StringEquals(key, "type"))
|
|
{
|
|
// New-style JSON status responses
|
|
type = atoi(value);
|
|
if (type < 1 || type > 3)
|
|
{
|
|
type = 1;
|
|
}
|
|
|
|
reprap.GetStatusResponse(response, type, true);
|
|
}
|
|
else
|
|
{
|
|
// Deprecated
|
|
reprap.GetLegacyStatusResponse(response, 1, 0);
|
|
}
|
|
}
|
|
else if (StringEquals(request, "gcode") && StringEquals(key, "gcode"))
|
|
{
|
|
webserver->LoadGcodeBuffer(value);
|
|
response.printf("{\"buff\":%u}", webserver->GetGcodeBufferSpace());
|
|
}
|
|
else if (StringEquals(request, "upload"))
|
|
{
|
|
response.printf("{\"err\":%d}", (uploadState == uploadOK && uploadedBytes == postFileLength) ? 0 : 1);
|
|
}
|
|
else if (StringEquals(request, "upload_begin") && StringEquals(key, "name"))
|
|
{
|
|
FileStore *file = platform->GetFileStore("0:/", value, true);
|
|
if (StartUpload(file))
|
|
{
|
|
strncpy(filenameBeingUploaded, value, ARRAY_SIZE(filenameBeingUploaded));
|
|
filenameBeingUploaded[ARRAY_UPB(filenameBeingUploaded)] = 0;
|
|
uploadingTextData = (numQualKeys < 2 || !StringEquals(qualifiers[1].key, "type") ||
|
|
!StringEquals(qualifiers[1].value, "binary"));
|
|
}
|
|
|
|
GetJsonUploadResponse(response);
|
|
}
|
|
else if (StringEquals(request, "upload_data") && StringEquals(key, "data"))
|
|
{
|
|
StoreUploadData(value, valueLength);
|
|
|
|
GetJsonUploadResponse(response);
|
|
keepOpen = true;
|
|
}
|
|
else if (StringEquals(request, "upload_end") && StringEquals(key, "size"))
|
|
{
|
|
uint32_t fileLength = strtoul(value, NULL, 10);
|
|
FinishUpload(fileLength);
|
|
|
|
GetJsonUploadResponse(response);
|
|
uploadState = notUploading;
|
|
}
|
|
else if (StringEquals(request, "upload_cancel"))
|
|
{
|
|
CancelUpload();
|
|
response.copy("{\"err\":0}");
|
|
}
|
|
else if (StringEquals(request, "delete") && StringEquals(key, "name"))
|
|
{
|
|
bool ok = platform->GetMassStorage()->Delete("0:/", value);
|
|
response.printf("{\"err\":%d}", (ok) ? 0 : 1);
|
|
}
|
|
else if (StringEquals(request, "files"))
|
|
{
|
|
// TODO: get rid of GetFilesResponse and write directly to NetworkTransaction!
|
|
const char* dir = (StringEquals(key, "dir")) ? value : platform->GetGCodeDir();
|
|
reprap.GetFilesResponse(response, dir);
|
|
}
|
|
else if (StringEquals(request, "fileinfo"))
|
|
{
|
|
reprap.GetPrintMonitor()->GetFileInfoResponse(response, (StringEquals(key, "name")) ? value : NULL);
|
|
}
|
|
else if (StringEquals(request, "move"))
|
|
{
|
|
if (numQualKeys >= 2)
|
|
{
|
|
if (StringEquals(key, "old") && StringEquals(qualifiers[1].key, "new"))
|
|
{
|
|
response.printf("{\"err\":%d}", platform->GetMassStorage()->Rename(value, qualifiers[1].value) ? 1 : 0);
|
|
}
|
|
else
|
|
{
|
|
response.printf("{\"err\":1}");
|
|
}
|
|
}
|
|
else
|
|
{
|
|
response.printf("{\"err\":1}");
|
|
}
|
|
}
|
|
else if (StringEquals(request, "name"))
|
|
{
|
|
reprap.GetNameResponse(response);
|
|
}
|
|
else if (StringEquals(request, "connect"))
|
|
{
|
|
CancelUpload();
|
|
reprap.GetStatusResponse(response, 1, -1);
|
|
}
|
|
else if (StringEquals(request, "mkdir") && StringEquals(key, "dir"))
|
|
{
|
|
bool ok = (platform->GetMassStorage()->MakeDirectory(value));
|
|
response.printf("{\"err\":%d}", (ok) ? 0 : 1);
|
|
}
|
|
else if (StringEquals(request, "config"))
|
|
{
|
|
if (StringEquals(key, "type"))
|
|
{
|
|
int type = max<int>(2, min<int>(0, atoi(value)));
|
|
// TODO: implement this
|
|
}
|
|
}
|
|
else
|
|
{
|
|
found = false;
|
|
}
|
|
}
|
|
|
|
return found;
|
|
}
|
|
|
|
void Webserver::HttpInterpreter::GetJsonUploadResponse(StringRef& response)
|
|
{
|
|
response.printf("{\"ubuff\":%u,\"err\":%d}", webUploadBufferSize, (uploadState == uploadOK) ? 0 : 1);
|
|
}
|
|
|
|
void Webserver::HttpInterpreter::ResetState()
|
|
{
|
|
clientPointer = 0;
|
|
state = doingCommandWord;
|
|
numCommandWords = 0;
|
|
numQualKeys = 0;
|
|
numHeaderKeys = 0;
|
|
commandWords[0] = clientMessage;
|
|
}
|
|
|
|
bool Webserver::HttpInterpreter::NeedMoreData()
|
|
{
|
|
if (state == doingPost)
|
|
{
|
|
StoreUploadData(clientMessage + (clientPointer - uploadedBytes), uploadedBytes);
|
|
ResetState();
|
|
return false;
|
|
}
|
|
else if (!IsAuthenticated())
|
|
{
|
|
// It makes very little sense to allow unknown connections to block our HTTP reader.
|
|
// Especially because rr_connect requests don't take up more than one TCP MSS.
|
|
ResetState();
|
|
return false;
|
|
}
|
|
return true;
|
|
}
|
|
|
|
void Webserver::HttpInterpreter::ResetSessions()
|
|
{
|
|
numActiveSessions = 0;
|
|
}
|
|
|
|
void Webserver::HttpInterpreter::ConnectionLost(uint32_t remoteIP, uint16_t remotePort, uint16_t localPort)
|
|
{
|
|
// Deal with aborted POST uploads. Note that we also check the remote port here,
|
|
// because the client *might* have two instances of the web interface running.
|
|
if (uploadState == uploadOK)
|
|
{
|
|
for(size_t i=0; i<numActiveSessions; i++)
|
|
{
|
|
if (sessions[i].ip == remoteIP && sessions[i].isPostUploading && sessions[i].postPort == remotePort)
|
|
{
|
|
if (reprap.Debug(moduleWebserver))
|
|
{
|
|
platform->Message(HOST_MESSAGE, "POST upload for '%s' has been cancelled!\n", filenameBeingUploaded);
|
|
}
|
|
CancelUpload(remoteIP);
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// Process a character from the client
|
|
// Rewritten as a state machine by dc42 to increase capability and speed, and reduce RAM requirement.
|
|
// On entry:
|
|
// There is space for at least 1 character in clientMessage.
|
|
// On return:
|
|
// If we return false:
|
|
// We want more characters. There is space for at least 1 character in clientMessage.
|
|
// If we return true:
|
|
// We have processed the message and sent the reply. No more characters may be read from this message.
|
|
// Whenever this calls ProcessMessage:
|
|
// The first line has been split up into words. Variables numCommandWords and commandWords give the number of words we found
|
|
// and the pointers to each word. The second word is treated specially. It is assumed to be a filename followed by an optional
|
|
// qualifier comprising key/value pairs. Both may include %xx escapes, and the qualifier may include + to mean space. We store
|
|
// a pointer to the filename without qualifier in commandWords[1]. We store the qualifier key/value pointers in array 'qualifiers'
|
|
// and the number of them in numQualKeys.
|
|
// The remaining lines have been parsed as header name/value pairs. Pointers to them are stored in array 'headers' and the number
|
|
// of them in numHeaders.
|
|
// If one of our arrays is about to overflow, or the message is not in a format we expect, then we call RejectMessage with an
|
|
// appropriate error code and string.
|
|
bool Webserver::HttpInterpreter::CharFromClient(char c)
|
|
{
|
|
switch(state)
|
|
{
|
|
case doingCommandWord:
|
|
switch(c)
|
|
{
|
|
case '\n':
|
|
clientMessage[clientPointer++] = 0;
|
|
++numCommandWords;
|
|
numHeaderKeys = 0;
|
|
headers[0].key = clientMessage + clientPointer;
|
|
state = doingHeaderKey;
|
|
break;
|
|
case '\r':
|
|
break;
|
|
case ' ':
|
|
case '\t':
|
|
clientMessage[clientPointer++] = 0;
|
|
if (numCommandWords < maxCommandWords)
|
|
{
|
|
++numCommandWords;
|
|
commandWords[numCommandWords] = clientMessage + clientPointer;
|
|
if (numCommandWords == 1)
|
|
{
|
|
state = doingFilename;
|
|
}
|
|
}
|
|
else
|
|
{
|
|
return RejectMessage("too many command words");
|
|
}
|
|
break;
|
|
default:
|
|
clientMessage[clientPointer++] = c;
|
|
break;
|
|
}
|
|
break;
|
|
|
|
case doingFilename:
|
|
switch(c)
|
|
{
|
|
case '\n':
|
|
clientMessage[clientPointer++] = 0;
|
|
++numCommandWords;
|
|
numQualKeys = 0;
|
|
numHeaderKeys = 0;
|
|
headers[0].key = clientMessage + clientPointer;
|
|
state = doingHeaderKey;
|
|
break;
|
|
case '?':
|
|
clientMessage[clientPointer++] = 0;
|
|
++numCommandWords;
|
|
numQualKeys = 0;
|
|
qualifiers[0].key = clientMessage + clientPointer;
|
|
state = doingQualifierKey;
|
|
break;
|
|
case '%':
|
|
state = doingFilenameEsc1;
|
|
break;
|
|
case '\r':
|
|
break;
|
|
case ' ':
|
|
case '\t':
|
|
clientMessage[clientPointer++] = 0;
|
|
if (numCommandWords < maxCommandWords)
|
|
{
|
|
++numCommandWords;
|
|
commandWords[numCommandWords] = clientMessage + clientPointer;
|
|
state = doingCommandWord;
|
|
}
|
|
else
|
|
{
|
|
return RejectMessage("too many command words");
|
|
}
|
|
break;
|
|
default:
|
|
clientMessage[clientPointer++] = c;
|
|
break;
|
|
}
|
|
break;
|
|
|
|
case doingQualifierKey:
|
|
switch(c)
|
|
{
|
|
case '=':
|
|
clientMessage[clientPointer++] = 0;
|
|
qualifiers[numQualKeys].value = clientMessage + clientPointer;
|
|
++numQualKeys;
|
|
state = doingQualifierValue;
|
|
break;
|
|
case '\n': // key with no value
|
|
case ' ':
|
|
case '\t':
|
|
case '\r':
|
|
case '%': // none of our keys needs escaping, so treat an escape within a key as an error
|
|
case '&': // key with no value
|
|
return RejectMessage("bad qualifier key");
|
|
default:
|
|
clientMessage[clientPointer++] = c;
|
|
break;
|
|
}
|
|
break;
|
|
|
|
case doingQualifierValue:
|
|
switch(c)
|
|
{
|
|
case '\n':
|
|
clientMessage[clientPointer++] = 0;
|
|
qualifiers[numQualKeys].key = clientMessage + clientPointer; // so that we can read the whole value even if it contains a null
|
|
numHeaderKeys = 0;
|
|
headers[0].key = clientMessage + clientPointer;
|
|
state = doingHeaderKey;
|
|
break;
|
|
case ' ':
|
|
case '\t':
|
|
clientMessage[clientPointer++] = 0;
|
|
qualifiers[numQualKeys].key = clientMessage + clientPointer; // so that we can read the whole value even if it contains a null
|
|
++numCommandWords;
|
|
commandWords[numCommandWords] = clientMessage + clientPointer;
|
|
state = doingCommandWord;
|
|
break;
|
|
case '\r':
|
|
break;
|
|
case '%':
|
|
state = doingQualifierValueEsc1;
|
|
break;
|
|
case '&':
|
|
// Another variable is coming
|
|
clientMessage[clientPointer++] = 0;
|
|
qualifiers[numQualKeys].key = clientMessage + clientPointer; // so that we can read the whole value even if it contains a null
|
|
if (numQualKeys < maxQualKeys)
|
|
{
|
|
state = doingQualifierKey;
|
|
}
|
|
else
|
|
{
|
|
return RejectMessage("too many keys in qualifier");
|
|
}
|
|
break;
|
|
case '+':
|
|
clientMessage[clientPointer++] = ' ';
|
|
break;
|
|
default:
|
|
clientMessage[clientPointer++] = c;
|
|
break;
|
|
}
|
|
break;
|
|
|
|
case doingFilenameEsc1:
|
|
case doingQualifierValueEsc1:
|
|
if (c >= '0' && c <= '9')
|
|
{
|
|
decodeChar = (c - '0') << 4;
|
|
state = (HttpState)(state + 1);
|
|
}
|
|
else if (c >= 'A' && c <= 'F')
|
|
{
|
|
decodeChar = (c - ('A' - 10)) << 4;
|
|
state = (HttpState)(state + 1);
|
|
}
|
|
else
|
|
{
|
|
return RejectMessage(badEscapeResponse);
|
|
}
|
|
break;
|
|
|
|
case doingFilenameEsc2:
|
|
case doingQualifierValueEsc2:
|
|
if (c >= '0' && c <= '9')
|
|
{
|
|
clientMessage[clientPointer++] = decodeChar | (c - '0');
|
|
state = (HttpState)(state - 2);
|
|
}
|
|
else if (c >= 'A' && c <= 'F')
|
|
{
|
|
clientMessage[clientPointer++] = decodeChar | c - ('A' - 10);
|
|
state = (HttpState)(state - 2);
|
|
}
|
|
else
|
|
{
|
|
return RejectMessage(badEscapeResponse);
|
|
}
|
|
break;
|
|
|
|
case doingHeaderKey:
|
|
switch(c)
|
|
{
|
|
case '\n':
|
|
if (clientMessage + clientPointer == headers[numHeaderKeys].key) // if the key hasn't started yet, then this is the blank line at the end
|
|
{
|
|
if (ProcessMessage())
|
|
{
|
|
ResetState();
|
|
return true;
|
|
}
|
|
}
|
|
else
|
|
{
|
|
return RejectMessage("unexpected newline");
|
|
}
|
|
break;
|
|
case '\r':
|
|
break;
|
|
case ':':
|
|
if (numHeaderKeys == maxHeaders - 1)
|
|
{
|
|
return RejectMessage("too many header key-value pairs");
|
|
}
|
|
clientMessage[clientPointer++] = 0;
|
|
headers[numHeaderKeys].value = clientMessage + clientPointer;
|
|
++numHeaderKeys;
|
|
state = expectingHeaderValue;
|
|
break;
|
|
default:
|
|
clientMessage[clientPointer++] = c;
|
|
break;
|
|
}
|
|
break;
|
|
|
|
case expectingHeaderValue:
|
|
if (c == ' ' || c == '\t')
|
|
{
|
|
break; // ignore spaces between header key and value
|
|
}
|
|
state = doingHeaderValue;
|
|
// no break
|
|
|
|
case doingHeaderValue:
|
|
if (c == '\n')
|
|
{
|
|
state = doingHeaderContinuation;
|
|
}
|
|
else if (c != '\r')
|
|
{
|
|
clientMessage[clientPointer++] = c;
|
|
}
|
|
break;
|
|
|
|
case doingHeaderContinuation:
|
|
switch(c)
|
|
{
|
|
case ' ':
|
|
case '\t':
|
|
// It's a continuation of the previous value
|
|
clientMessage[clientPointer++] = c;
|
|
state = doingHeaderValue;
|
|
break;
|
|
case '\n':
|
|
// It's the blank line
|
|
clientMessage[clientPointer] = 0;
|
|
if (ProcessMessage())
|
|
{
|
|
ResetState();
|
|
return true;
|
|
}
|
|
// no break
|
|
case '\r':
|
|
break;
|
|
default:
|
|
// It's a new key
|
|
if (clientPointer + 3 <= ARRAY_SIZE(clientMessage))
|
|
{
|
|
clientMessage[clientPointer++] = 0;
|
|
headers[numHeaderKeys].key = clientMessage + clientPointer;
|
|
clientMessage[clientPointer++] = c;
|
|
state = doingHeaderKey;
|
|
}
|
|
else
|
|
{
|
|
return RejectMessage(overflowResponse);
|
|
}
|
|
break;
|
|
}
|
|
break;
|
|
|
|
case doingPost:
|
|
clientMessage[clientPointer++] = c;
|
|
uploadedBytes++;
|
|
|
|
if (uploadedBytes == postFileLength)
|
|
{
|
|
StoreUploadData(clientMessage + (clientPointer - uploadedBytes), uploadedBytes);
|
|
FinishUpload(postFileLength);
|
|
|
|
SendJsonResponse("upload");
|
|
|
|
// Reset state
|
|
uint32_t remoteIP = network->GetTransaction()->GetRemoteIP();
|
|
for(unsigned int i=0; i<numActiveSessions; i++)
|
|
{
|
|
if (sessions[i].ip == remoteIP && sessions[i].isPostUploading)
|
|
{
|
|
sessions[i].isPostUploading = false;
|
|
sessions[i].lastQueryTime = platform->Time();
|
|
break;
|
|
}
|
|
}
|
|
uploadState = notUploading;
|
|
ResetState();
|
|
|
|
return true;
|
|
}
|
|
|
|
break;
|
|
|
|
default:
|
|
break;
|
|
}
|
|
|
|
if (clientPointer == ARRAY_SIZE(clientMessage))
|
|
{
|
|
return RejectMessage(overflowResponse);
|
|
}
|
|
return false;
|
|
}
|
|
|
|
// Process the message received so far. We have reached the end of the headers.
|
|
// Return true if the message is complete, false if we want to continue receiving data (i.e. postdata)
|
|
bool Webserver::HttpInterpreter::ProcessMessage()
|
|
{
|
|
if (reprap.Debug(moduleWebserver))
|
|
{
|
|
platform->Message(HOST_MESSAGE, "HTTP requests with %d command words:", numCommandWords);
|
|
for (unsigned int i = 0; i < numCommandWords; ++i)
|
|
{
|
|
platform->AppendMessage(HOST_MESSAGE, " %s", commandWords[i]);
|
|
}
|
|
platform->AppendMessage(HOST_MESSAGE, "\n");
|
|
}
|
|
|
|
if (numCommandWords < 2)
|
|
{
|
|
return RejectMessage("too few command words");
|
|
}
|
|
|
|
if (StringEquals(commandWords[0], "GET"))
|
|
{
|
|
if (StringStartsWith(commandWords[1], KO_START))
|
|
{
|
|
SendJsonResponse(commandWords[1] + KO_FIRST);
|
|
}
|
|
else if (commandWords[1][0] == '/' && StringStartsWith(commandWords[1] + 1, KO_START))
|
|
{
|
|
SendJsonResponse(commandWords[1] + 1 + KO_FIRST);
|
|
}
|
|
else
|
|
{
|
|
SendFile(commandWords[1]);
|
|
}
|
|
return true;
|
|
}
|
|
else if (StringEquals(commandWords[0], "POST"))
|
|
{
|
|
bool isUploadRequest = (StringEquals(commandWords[1], KO_START "upload"));
|
|
isUploadRequest |= (commandWords[1][0] == '/' && StringEquals(commandWords[1] + 1, KO_START "upload"));
|
|
if (isUploadRequest)
|
|
{
|
|
if (numQualKeys > 0 && StringEquals(qualifiers[0].key, "name"))
|
|
{
|
|
// We cannot upload more than one file at once
|
|
if (uploadState == uploadOK)
|
|
{
|
|
return RejectMessage("cannot upload more than one file at once");
|
|
}
|
|
|
|
// See how many bytes we expect to read
|
|
bool contentLengthFound = false;
|
|
for(size_t i=0; i<numHeaderKeys; i++)
|
|
{
|
|
if (StringEquals(headers[i].key, "Content-Length"))
|
|
{
|
|
postFileLength = atoi(headers[i].value);
|
|
contentLengthFound = true;
|
|
break;
|
|
}
|
|
}
|
|
|
|
// Start POST file upload
|
|
if (contentLengthFound)
|
|
{
|
|
FileStore *file = platform->GetFileStore("0:/", qualifiers[0].value, true);
|
|
if (StartUpload(file))
|
|
{
|
|
// Start new file upload
|
|
uploadingTextData = false;
|
|
uploadedBytes = numContinuationBytes = 0;
|
|
|
|
strncpy(filenameBeingUploaded, qualifiers[0].value, ARRAY_SIZE(filenameBeingUploaded));
|
|
filenameBeingUploaded[ARRAY_UPB(filenameBeingUploaded)] = 0;
|
|
|
|
// Set POST variables only if we actually need to store data
|
|
if (postFileLength > 0)
|
|
{
|
|
uint32_t remoteIP = network->GetTransaction()->GetRemoteIP();
|
|
uint16_t remotePort = network->GetTransaction()->GetRemotePort();
|
|
for(size_t i=0; i<numActiveSessions; i++)
|
|
{
|
|
if (sessions[i].ip == remoteIP)
|
|
{
|
|
sessions[i].postPort = remotePort;
|
|
sessions[i].isPostUploading = true;
|
|
break;
|
|
}
|
|
}
|
|
state = doingPost;
|
|
|
|
return false;
|
|
}
|
|
|
|
// User has uploaded an empty file - we should have finished here already
|
|
FinishUpload(0);
|
|
SendJsonResponse("upload");
|
|
return true;
|
|
}
|
|
return RejectMessage("could not start file upload");
|
|
}
|
|
}
|
|
return RejectMessage("invalid POST upload request");
|
|
}
|
|
return RejectMessage("only rr_upload is supported for POST requests");
|
|
}
|
|
else
|
|
{
|
|
return RejectMessage("Unknown message type");
|
|
}
|
|
}
|
|
|
|
// Reject the current message. Always returns true to indicate that we should stop reading the message.
|
|
bool Webserver::HttpInterpreter::RejectMessage(const char* response, unsigned int code)
|
|
{
|
|
platform->Message(HOST_MESSAGE, "Webserver: rejecting message with: %s\n", response);
|
|
|
|
NetworkTransaction *req = network->GetTransaction();
|
|
req->Printf("HTTP/1.1 %u %s\nConnection: close\n\n", code, response);
|
|
network->SendAndClose(NULL);
|
|
|
|
ResetState();
|
|
|
|
return true;
|
|
}
|
|
|
|
// Authenticate current IP and return true on success
|
|
bool Webserver::HttpInterpreter::Authenticate()
|
|
{
|
|
if (numActiveSessions < maxSessions)
|
|
{
|
|
sessions[numActiveSessions].isPostUploading = false;
|
|
sessions[numActiveSessions].ip = network->GetTransaction()->GetRemoteIP();
|
|
sessions[numActiveSessions].lastQueryTime = platform->Time();
|
|
numActiveSessions++;
|
|
return true;
|
|
}
|
|
return false;
|
|
}
|
|
|
|
bool Webserver::HttpInterpreter::IsAuthenticated() const
|
|
{
|
|
uint32_t remoteIP = network->GetTransaction()->GetRemoteIP();
|
|
for(size_t i=0; i<numActiveSessions; i++)
|
|
{
|
|
if (sessions[i].ip == remoteIP)
|
|
{
|
|
return true;
|
|
}
|
|
}
|
|
return false;
|
|
}
|
|
|
|
void Webserver::HttpInterpreter::UpdateAuthentication()
|
|
{
|
|
uint32_t remoteIP = network->GetTransaction()->GetRemoteIP();
|
|
for(size_t i=0; i<numActiveSessions; i++)
|
|
{
|
|
if (sessions[i].ip == remoteIP)
|
|
{
|
|
sessions[i].lastQueryTime = platform->Time();
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
void Webserver::HttpInterpreter::RemoveAuthentication()
|
|
{
|
|
uint32_t remoteIP = network->GetTransaction()->GetRemoteIP();
|
|
for(size_t i=numActiveSessions - 1; i>=0; i--)
|
|
{
|
|
if (sessions[i].ip == remoteIP)
|
|
{
|
|
for(int k=numActiveSessions - 1; k > i; k--)
|
|
{
|
|
memcpy(&sessions[k - 1], &sessions[k], sizeof(HttpSession));
|
|
}
|
|
numActiveSessions--;
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
|
|
void Webserver::HttpInterpreter::CheckSessions()
|
|
{
|
|
const float time = platform->Time();
|
|
for(int i=numActiveSessions - 1; i>=0; i--)
|
|
{
|
|
if (!sessions[i].isPostUploading && (time - sessions[i].lastQueryTime) > httpSessionTimeout)
|
|
{
|
|
for(int k=numActiveSessions - 1; k > i; k--)
|
|
{
|
|
memcpy(&sessions[k - 1], &sessions[k], sizeof(HttpSession));
|
|
}
|
|
numActiveSessions--;
|
|
}
|
|
}
|
|
}
|
|
|
|
|
|
//********************************************************************************************
|
|
//
|
|
//************************* FTP interpreter for the Webserver class **************************
|
|
//
|
|
//********************************************************************************************
|
|
|
|
Webserver::FtpInterpreter::FtpInterpreter(Platform *p, Webserver *ws, Network *n)
|
|
: ProtocolInterpreter(p, ws, n), state(authenticating), clientPointer(0)
|
|
{
|
|
strcpy(currentDir, "/");
|
|
}
|
|
|
|
void Webserver::FtpInterpreter::ConnectionEstablished()
|
|
{
|
|
if (reprap.Debug(moduleWebserver))
|
|
{
|
|
platform->Message(HOST_MESSAGE, "Webserver: FTP connection established!\n");
|
|
}
|
|
|
|
NetworkTransaction *req = network->GetTransaction();
|
|
|
|
switch (state)
|
|
{
|
|
case waitingForPasvPort:
|
|
if (req->GetLocalPort() == ftpPort)
|
|
{
|
|
network->SendAndClose(NULL);
|
|
return;
|
|
}
|
|
|
|
network->SaveDataConnection();
|
|
state = pasvPortConnected;
|
|
|
|
break;
|
|
|
|
default:
|
|
// I (zpl) wanted to allow only one active FTP session, but some FTP programs
|
|
// like FileZilla open a second connection for transfers for some reason.
|
|
if (req->GetLocalPort() == ftpPort)
|
|
{
|
|
req->Write("220 RepRapPro Ormerod\r\n");
|
|
network->SendAndClose(NULL, true);
|
|
|
|
ResetState();
|
|
}
|
|
|
|
break;
|
|
}
|
|
}
|
|
|
|
void Webserver::FtpInterpreter::ConnectionLost(uint32_t remoteIP, uint16_t remotePort, uint16_t localPort)
|
|
{
|
|
if (localPort != ftpPort)
|
|
{
|
|
// Close the data port
|
|
network->CloseDataPort();
|
|
|
|
// Send response
|
|
if (network->AcquireFTPTransaction())
|
|
{
|
|
if (state == doingPasvIO)
|
|
{
|
|
if (uploadState != uploadError)
|
|
{
|
|
SendReply(226, "Transfer complete.");
|
|
}
|
|
else
|
|
{
|
|
SendReply(526, "Upload failed!");
|
|
}
|
|
}
|
|
else
|
|
{
|
|
SendReply(550, "Lost data connection!");
|
|
}
|
|
}
|
|
|
|
// Do file handling
|
|
if (IsUploading())
|
|
{
|
|
FinishUpload(0U);
|
|
uploadState = notUploading;
|
|
}
|
|
state = authenticated;
|
|
}
|
|
}
|
|
|
|
bool Webserver::FtpInterpreter::CharFromClient(char c)
|
|
{
|
|
if (clientPointer == ARRAY_UPB(clientMessage))
|
|
{
|
|
clientPointer = 0;
|
|
platform->Message(HOST_MESSAGE, "Webserver: Buffer overflow in FTP server!\n");
|
|
return true;
|
|
}
|
|
|
|
switch (c)
|
|
{
|
|
case 0:
|
|
break;
|
|
|
|
case '\r':
|
|
case '\n':
|
|
clientMessage[clientPointer++] = 0;
|
|
|
|
if (reprap.Debug(moduleWebserver))
|
|
{
|
|
platform->Message(HOST_MESSAGE, "FtpInterpreter::ProcessLine called with state %d:\n%s\n", state, clientMessage);
|
|
}
|
|
|
|
if (clientPointer > 1) // only process a new line if we actually received data
|
|
{
|
|
ProcessLine();
|
|
clientPointer = 0;
|
|
return true;
|
|
}
|
|
|
|
if (reprap.Debug(moduleWebserver))
|
|
{
|
|
platform->Message(HOST_MESSAGE, "FtpInterpreter::ProcessLine call finished.\n");
|
|
}
|
|
|
|
clientPointer = 0;
|
|
break;
|
|
|
|
default:
|
|
clientMessage[clientPointer++] = c;
|
|
break;
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
void Webserver::FtpInterpreter::ResetState()
|
|
{
|
|
clientPointer = 0;
|
|
strcpy(currentDir, "/");
|
|
|
|
network->CloseDataPort();
|
|
CancelUpload();
|
|
|
|
state = authenticating;
|
|
}
|
|
|
|
bool Webserver::FtpInterpreter::DoingFastUpload() const
|
|
{
|
|
return (IsUploading() && network->GetTransaction()->GetLocalPort() == network->GetDataPort());
|
|
}
|
|
|
|
// return true if an error has occurred, false otherwise
|
|
void Webserver::FtpInterpreter::ProcessLine()
|
|
{
|
|
switch (state)
|
|
{
|
|
case authenticating:
|
|
// don't check the user name
|
|
if (StringStartsWith(clientMessage, "USER"))
|
|
{
|
|
SendReply(331, "Please specify the password.");
|
|
}
|
|
// but check the password
|
|
else if (StringStartsWith(clientMessage, "PASS"))
|
|
{
|
|
char pass[SHORT_STRING_LENGTH];
|
|
int pass_length = 0;
|
|
bool reading_pass = false;
|
|
for(int i=4; i<clientPointer && i<SHORT_STRING_LENGTH +3; i++)
|
|
{
|
|
reading_pass |= (clientMessage[i] != ' ' && clientMessage[i] != '\t');
|
|
if (reading_pass)
|
|
{
|
|
pass[pass_length++] = clientMessage[i];
|
|
}
|
|
}
|
|
pass[pass_length] = 0;
|
|
|
|
if (reprap.CheckPassword(pass))
|
|
{
|
|
state = authenticated;
|
|
SendReply(230, "Login successful.");
|
|
}
|
|
else
|
|
{
|
|
SendReply(530, "Login incorrect.", false);
|
|
}
|
|
}
|
|
// if it's different, send response 500 to indicate we don't know the code (might be AUTH or so)
|
|
else
|
|
{
|
|
SendReply(500, "Unknown login command.");
|
|
}
|
|
|
|
break;
|
|
|
|
case authenticated:
|
|
// get system type
|
|
if (StringEquals(clientMessage, "SYST"))
|
|
{
|
|
SendReply(215, "UNIX Type: L8");
|
|
}
|
|
// get features
|
|
else if (StringEquals(clientMessage, "FEAT"))
|
|
{
|
|
SendFeatures();
|
|
}
|
|
// get current dir
|
|
else if (StringEquals(clientMessage, "PWD"))
|
|
{
|
|
snprintf(ftpResponse, ftpResponseLength, "\"%s\"", currentDir);
|
|
SendReply(257, ftpResponse);
|
|
}
|
|
// set current dir
|
|
else if (StringStartsWith(clientMessage, "CWD"))
|
|
{
|
|
ReadFilename(3);
|
|
ChangeDirectory(filename);
|
|
}
|
|
// 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"))
|
|
{
|
|
for(int i=4; i<clientPointer; i++)
|
|
{
|
|
if (clientMessage[i] == 'I')
|
|
{
|
|
SendReply(200, "Switching to Binary mode.");
|
|
return;
|
|
}
|
|
|
|
if (clientMessage[i] == 'A')
|
|
{
|
|
SendReply(200, "Switching to ASCII mode.");
|
|
return;
|
|
}
|
|
}
|
|
|
|
SendReply(500, "Unknown command.");
|
|
}
|
|
// enter passive mode mode
|
|
else if (StringEquals(clientMessage, "PASV"))
|
|
{
|
|
/* get local IP address */
|
|
const byte *ip_address = platform->IPAddress();
|
|
|
|
/* open random port > 1023 */
|
|
rand();
|
|
uint16_t pasv_port = random(1024, 65535);
|
|
network->OpenDataPort(pasv_port);
|
|
portOpenTime = platform->Time();
|
|
state = waitingForPasvPort;
|
|
|
|
/* send FTP response */
|
|
snprintf(ftpResponse, ftpResponseLength, "Entering Passive Mode (%d,%d,%d,%d,%d,%d)",
|
|
*ip_address++, *ip_address++, *ip_address++, *ip_address++,
|
|
pasv_port / 256, pasv_port % 256);
|
|
SendReply(227, ftpResponse);
|
|
}
|
|
// PASV commands are not supported in this state
|
|
else if (StringEquals(clientMessage, "LIST") || StringStartsWith(clientMessage, "RETR") || StringStartsWith(clientMessage, "STOR"))
|
|
{
|
|
SendReply(425, "Use PASV first.");
|
|
}
|
|
// delete file
|
|
else if (StringStartsWith(clientMessage, "DELE"))
|
|
{
|
|
ReadFilename(4);
|
|
|
|
bool ok;
|
|
if (filename[0] == '/')
|
|
{
|
|
ok = platform->GetMassStorage()->Delete(NULL, filename);
|
|
}
|
|
else
|
|
{
|
|
ok = platform->GetMassStorage()->Delete(currentDir, filename);
|
|
}
|
|
|
|
if (ok)
|
|
{
|
|
SendReply(250, "Delete operation successful.");
|
|
}
|
|
else
|
|
{
|
|
SendReply(550, "Delete operation failed.");
|
|
}
|
|
}
|
|
// delete directory
|
|
else if (StringStartsWith(clientMessage, "RMD"))
|
|
{
|
|
ReadFilename(3);
|
|
|
|
bool ok;
|
|
if (filename[0] == '/')
|
|
{
|
|
ok = platform->GetMassStorage()->Delete(NULL, filename);
|
|
}
|
|
else
|
|
{
|
|
ok = platform->GetMassStorage()->Delete(currentDir, filename);
|
|
}
|
|
|
|
if (ok)
|
|
{
|
|
SendReply(250, "Remove directory operation successful.");
|
|
}
|
|
else
|
|
{
|
|
SendReply(550, "Remove directory operation failed.");
|
|
}
|
|
}
|
|
// make new directory
|
|
else if (StringStartsWith(clientMessage, "MKD"))
|
|
{
|
|
ReadFilename(3);
|
|
const char *location = (filename[0] == '/')
|
|
? filename
|
|
: platform->GetMassStorage()->CombineName(currentDir, filename);
|
|
|
|
if (platform->GetMassStorage()->MakeDirectory(location))
|
|
{
|
|
snprintf(ftpResponse, ftpResponseLength, "\"%s\" created", location);
|
|
SendReply(257, ftpResponse);
|
|
}
|
|
else
|
|
{
|
|
SendReply(550, "Create directory operation failed.");
|
|
}
|
|
}
|
|
// rename file or directory
|
|
else if (StringStartsWith(clientMessage, "RNFR"))
|
|
{
|
|
ReadFilename(4);
|
|
if (filename[0] != '/')
|
|
{
|
|
const char *temp = platform->GetMassStorage()->CombineName(currentDir, filename);
|
|
strncpy(filename, temp, MaxFilenameLength);
|
|
filename[MaxFilenameLength - 1] = 0;
|
|
}
|
|
|
|
if (platform->GetMassStorage()->FileExists(filename))
|
|
{
|
|
SendReply(350, "Ready to RNTO.");
|
|
}
|
|
else
|
|
{
|
|
SendReply(550, "Invalid file or directory.");
|
|
}
|
|
}
|
|
else if (StringStartsWith(clientMessage, "RNTO"))
|
|
{
|
|
// Copy origin path to temp oldFilename and read new path
|
|
char oldFilename[MaxFilenameLength];
|
|
strncpy(oldFilename, filename, MaxFilenameLength);
|
|
oldFilename[MaxFilenameLength - 1] = 0;
|
|
ReadFilename(4);
|
|
|
|
// See where this file needs to be moved to
|
|
if (filename[0] == '/')
|
|
{
|
|
if (platform->GetMassStorage()->Rename(oldFilename, filename))
|
|
{
|
|
SendReply(250, "Rename successful.");
|
|
}
|
|
else
|
|
{
|
|
SendReply(550, "Could not rename file or directory.");
|
|
}
|
|
}
|
|
else
|
|
{
|
|
const char *newFilename = platform->GetMassStorage()->CombineName(currentDir, filename);
|
|
if (platform->GetMassStorage()->Rename(oldFilename, newFilename))
|
|
{
|
|
SendReply(250, "Rename successful.");
|
|
}
|
|
else
|
|
{
|
|
SendReply(500, "Could not rename file or directory.");
|
|
}
|
|
}
|
|
}
|
|
// no op
|
|
else if (StringEquals(clientMessage, "NOOP"))
|
|
{
|
|
SendReply(200, "NOOP okay.");
|
|
}
|
|
// end connection
|
|
else if (StringEquals(clientMessage, "QUIT"))
|
|
{
|
|
SendReply(221, "Goodbye.", false);
|
|
ResetState();
|
|
}
|
|
// unknown
|
|
else
|
|
{
|
|
SendReply(500, "Unknown command.");
|
|
}
|
|
|
|
break;
|
|
|
|
case waitingForPasvPort:
|
|
if (!reprap.Debug(moduleWebserver) && platform->Time() - portOpenTime > pasvPortTimeout)
|
|
{
|
|
SendReply(425, "Failed to establish connection.");
|
|
|
|
network->CloseDataPort();
|
|
state = authenticated;
|
|
}
|
|
else
|
|
{
|
|
network->WaitForDataConection();
|
|
}
|
|
|
|
break;
|
|
|
|
case pasvPortConnected:
|
|
// save current connection state so we can send '226 Transfer complete.' when ConnectionLost() is called
|
|
network->SaveFTPConnection();
|
|
|
|
// list directory entries
|
|
if (StringEquals(clientMessage, "LIST"))
|
|
{
|
|
// send response via main port
|
|
strncpy(ftpResponse, "150 Here comes the directory listing.\r\n", ftpResponseLength);
|
|
NetworkTransaction *ftp_req = network->GetTransaction();
|
|
ftp_req->Write(ftpResponse);
|
|
network->SendAndClose(NULL, true);
|
|
|
|
// send file list via data port
|
|
if (network->AcquireDataTransaction())
|
|
{
|
|
FileInfo file_info;
|
|
if (platform->GetMassStorage()->FindFirst(currentDir, file_info))
|
|
{
|
|
NetworkTransaction *data_req = network->GetTransaction();
|
|
char line[300];
|
|
|
|
do {
|
|
// Example for a typical UNIX-like file list:
|
|
// "drwxr-xr-x 2 ftp ftp 0 Apr 11 2013 bin\r\n"
|
|
char dirChar = (file_info.isDirectory) ? 'd' : '-';
|
|
snprintf(line, ARRAY_SIZE(line), "%crw-rw-rw- 1 ftp ftp %13d %s %02d %04d %s\r\n",
|
|
dirChar, file_info.size, platform->GetMassStorage()->GetMonthName(file_info.month),
|
|
file_info.day, file_info.year, file_info.fileName);
|
|
|
|
// Fortunately we don't need to bother with output buffer chunks any more...
|
|
data_req->Write(line);
|
|
} while (platform->GetMassStorage()->FindNext(file_info));
|
|
}
|
|
|
|
network->SendAndClose(NULL);
|
|
state = doingPasvIO;
|
|
}
|
|
else
|
|
{
|
|
SendReply(500, "Unknown error.");
|
|
network->CloseDataPort();
|
|
state = authenticated;
|
|
}
|
|
}
|
|
// upload a file
|
|
else if (StringStartsWith(clientMessage, "STOR"))
|
|
{
|
|
FileStore *file;
|
|
|
|
ReadFilename(4);
|
|
if (filename[0] == '/')
|
|
{
|
|
file = platform->GetFileStore(NULL, filename, true);
|
|
}
|
|
else
|
|
{
|
|
file = platform->GetFileStore(currentDir, filename, true);
|
|
}
|
|
|
|
if (StartUpload(file))
|
|
{
|
|
strncpy(filenameBeingUploaded, filename, ARRAY_SIZE(filenameBeingUploaded));
|
|
filenameBeingUploaded[ARRAY_UPB(filenameBeingUploaded)] = 0;
|
|
|
|
SendReply(150, "OK to send data.");
|
|
state = doingPasvIO;
|
|
}
|
|
else
|
|
{
|
|
SendReply(550, "Failed to open file.");
|
|
network->CloseDataPort();
|
|
state = authenticated;
|
|
}
|
|
}
|
|
// download a file
|
|
else if (StringStartsWith(clientMessage, "RETR"))
|
|
{
|
|
FileStore *fs;
|
|
|
|
ReadFilename(4);
|
|
if (filename[0] == '/')
|
|
{
|
|
fs = platform->GetFileStore(NULL, filename, false);
|
|
}
|
|
else
|
|
{
|
|
fs = platform->GetFileStore(currentDir, filename, false);
|
|
}
|
|
|
|
if (fs == NULL)
|
|
{
|
|
SendReply(550, "Failed to open file.");
|
|
}
|
|
else
|
|
{
|
|
snprintf(ftpResponse, ftpResponseLength, "Opening data connection for %s (%lu bytes).", filename, fs->Length());
|
|
SendReply(150, ftpResponse);
|
|
|
|
if (network->AcquireDataTransaction())
|
|
{
|
|
// send the file via data port
|
|
network->SendAndClose(fs, false);
|
|
state = doingPasvIO;
|
|
}
|
|
else
|
|
{
|
|
SendReply(500, "Unknown error.");
|
|
network->CloseDataPort();
|
|
state = authenticated;
|
|
}
|
|
}
|
|
}
|
|
// unknown command
|
|
else
|
|
{
|
|
SendReply(500, "Unknown command.");
|
|
network->CloseDataPort();
|
|
state = authenticated;
|
|
}
|
|
|
|
break;
|
|
|
|
case doingPasvIO:
|
|
// abort current transfer
|
|
if (StringEquals(clientMessage, "ABOR"))
|
|
{
|
|
if (IsUploading())
|
|
{
|
|
CancelUpload();
|
|
SendReply(226, "ABOR successful.");
|
|
}
|
|
else
|
|
{
|
|
network->CloseDataPort();
|
|
SendReply(226, "ABOR successful.");
|
|
}
|
|
}
|
|
// unknown command
|
|
else
|
|
{
|
|
SendReply(500, "Unknown command.");
|
|
network->CloseDataPort();
|
|
state = authenticated;
|
|
}
|
|
|
|
break;
|
|
}
|
|
}
|
|
|
|
void Webserver::FtpInterpreter::SendReply(int code, const char *message, bool keepConnection)
|
|
{
|
|
NetworkTransaction *req = network->GetTransaction();
|
|
req->Printf("%d %s\r\n", code, message);
|
|
network->SendAndClose(NULL, keepConnection);
|
|
}
|
|
|
|
void Webserver::FtpInterpreter::SendFeatures()
|
|
{
|
|
NetworkTransaction *req = network->GetTransaction();
|
|
req->Write("211-Features:\r\n");
|
|
req->Write("PASV\r\n"); // support PASV mode
|
|
req->Write("211 End\r\n");
|
|
network->SendAndClose(NULL, true);
|
|
}
|
|
|
|
void Webserver::FtpInterpreter::ReadFilename(int start)
|
|
{
|
|
int filenameLength = 0;
|
|
bool readingPath = false;
|
|
for(int i=start; i<clientPointer && filenameLength < MaxFilenameLength; i++)
|
|
{
|
|
switch (clientMessage[i])
|
|
{
|
|
// ignore quotes
|
|
case '"':
|
|
case '\'':
|
|
break;
|
|
|
|
// skip whitespaces unless the actual filename is being read
|
|
case ' ':
|
|
case '\t':
|
|
if (readingPath)
|
|
{
|
|
filename[filenameLength++] = clientMessage[i];
|
|
}
|
|
break;
|
|
|
|
// read path name
|
|
default:
|
|
readingPath = true;
|
|
filename[filenameLength++] = clientMessage[i];
|
|
break;
|
|
}
|
|
}
|
|
filename[filenameLength] = 0;
|
|
}
|
|
|
|
void Webserver::FtpInterpreter::ChangeDirectory(const char *newDirectory)
|
|
{
|
|
char combinedPath[MaxFilenameLength];
|
|
|
|
if (newDirectory[0] != 0)
|
|
{
|
|
/* Prepare the new directory path */
|
|
if (newDirectory[0] == '/') // absolute path
|
|
{
|
|
strncpy(combinedPath, newDirectory, MaxFilenameLength);
|
|
combinedPath[MaxFilenameLength - 1] = 0;
|
|
}
|
|
else // relative path
|
|
{
|
|
if (StringEquals(newDirectory, "..")) // go up
|
|
{
|
|
if (StringEquals(currentDir, "/"))
|
|
{
|
|
// we're already at the root, so we can't go up any more
|
|
SendReply(550, "Failed to change directory.");
|
|
return;
|
|
}
|
|
else
|
|
{
|
|
strncpy(combinedPath, currentDir, MaxFilenameLength);
|
|
for(int i=strlen(combinedPath) -2; i>=0; i--)
|
|
{
|
|
if (combinedPath[i] == '/')
|
|
{
|
|
combinedPath[i +1] = 0;
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
else // go to child directory
|
|
{
|
|
strncpy(combinedPath, currentDir, MaxFilenameLength);
|
|
if (strlen(currentDir) > 1)
|
|
{
|
|
strncat(combinedPath, "/", MaxFilenameLength - strlen(combinedPath) - 1);
|
|
}
|
|
strncat(combinedPath, newDirectory, MaxFilenameLength - strlen(combinedPath) - 1);
|
|
}
|
|
}
|
|
|
|
/* Make sure the new path does not end with a '/', because FatFs won't see the directory otherwise */
|
|
if (StringEndsWith(combinedPath, "/") && strlen(combinedPath) > 1)
|
|
{
|
|
combinedPath[strlen(combinedPath) -1] = 0;
|
|
}
|
|
|
|
/* Verify path and change it */
|
|
if (platform->GetMassStorage()->PathExists(combinedPath))
|
|
{
|
|
strncpy(currentDir, combinedPath, MaxFilenameLength);
|
|
SendReply(250, "Directory successfully changed.");
|
|
}
|
|
else
|
|
{
|
|
SendReply(550, "Failed to change directory.");
|
|
}
|
|
}
|
|
else
|
|
{
|
|
SendReply(550, "Failed to change directory.");
|
|
}
|
|
}
|
|
|
|
|
|
//********************************************************************************************
|
|
//
|
|
//*********************** Telnet interpreter for the Webserver class *************************
|
|
//
|
|
//********************************************************************************************
|
|
|
|
Webserver::TelnetInterpreter::TelnetInterpreter(Platform *p, Webserver *ws, Network *n)
|
|
: ProtocolInterpreter(p, ws, n)
|
|
{
|
|
ResetState();
|
|
}
|
|
|
|
void Webserver::TelnetInterpreter::ConnectionEstablished()
|
|
{
|
|
NetworkTransaction *req = network->GetTransaction();
|
|
req->Write("RepRapPro Ormerod Telnet Interface\r\n\r\n");
|
|
req->Write("Please enter your password:\r\n");
|
|
req->Write("> ");
|
|
network->SendAndClose(NULL, true);
|
|
}
|
|
|
|
void Webserver::TelnetInterpreter::ConnectionLost(uint32_t remoteIP, uint16_t remotePort, uint16_t localPort)
|
|
{
|
|
ResetState();
|
|
}
|
|
|
|
bool Webserver::TelnetInterpreter::CharFromClient(char c)
|
|
{
|
|
if (clientPointer == ARRAY_UPB(clientMessage))
|
|
{
|
|
clientPointer = 0;
|
|
platform->Message(HOST_MESSAGE, "Webserver: Buffer overflow in Telnet server!\n");
|
|
return true;
|
|
}
|
|
|
|
switch (c)
|
|
{
|
|
case 0:
|
|
break;
|
|
|
|
case '\r':
|
|
case '\n':
|
|
if (clientPointer != 0)
|
|
{
|
|
clientMessage[clientPointer] = 0;
|
|
ProcessLine();
|
|
clientPointer = 0;
|
|
}
|
|
return true;
|
|
|
|
case '#':
|
|
// On Linux, the last character of the HELO message sent by Telnet is '#',
|
|
// so clear the buffer if the user hasn't logged in yet.
|
|
if (state == authenticating)
|
|
{
|
|
clientPointer = 0;
|
|
break;
|
|
}
|
|
// no break
|
|
|
|
default:
|
|
clientMessage[clientPointer++] = c;
|
|
break;
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
void Webserver::TelnetInterpreter::ResetState()
|
|
{
|
|
state = authenticating;
|
|
clientPointer = 0;
|
|
sendPending = false;
|
|
}
|
|
|
|
void Webserver::TelnetInterpreter::ProcessLine()
|
|
{
|
|
NetworkTransaction *req = network->GetTransaction();
|
|
|
|
switch (state)
|
|
{
|
|
case authenticating:
|
|
if (reprap.CheckPassword(clientMessage))
|
|
{
|
|
network->SaveTelnetConnection();
|
|
state = authenticated;
|
|
|
|
req->Write("Log in successful!\r\n");
|
|
network->SendAndClose(NULL, true);
|
|
}
|
|
else
|
|
{
|
|
req->Write("Invalid password.\r\n> ");
|
|
network->SendAndClose(NULL, true);
|
|
}
|
|
break;
|
|
|
|
case authenticated:
|
|
// Special commands for Telnet
|
|
if (StringEquals(clientMessage, "exit") || StringEquals(clientMessage, "quit"))
|
|
{
|
|
req->Write("Goodbye.\r\n");
|
|
network->SendAndClose(NULL);
|
|
}
|
|
// All other commands are processed by the Webserver
|
|
else
|
|
{
|
|
webserver->ProcessGcode(clientMessage);
|
|
if (sendPending)
|
|
{
|
|
sendPending = false;
|
|
network->SendAndClose(NULL, true);
|
|
}
|
|
else
|
|
{
|
|
network->CloseTransaction();
|
|
}
|
|
}
|
|
break;
|
|
}
|
|
}
|
|
|
|
void Webserver::TelnetInterpreter::HandleGcodeReply(const char *reply)
|
|
{
|
|
if (state >= authenticated && network->AcquireTelnetTransaction())
|
|
{
|
|
NetworkTransaction *req = network->GetTransaction();
|
|
|
|
// Whenever a new line is read, we also need to send a carriage return
|
|
while (*reply != 0)
|
|
{
|
|
if (*reply == '\n')
|
|
{
|
|
req->Write('\r');
|
|
}
|
|
req->Write(*reply);
|
|
reply++;
|
|
}
|
|
|
|
// We must not send the message here, because then we'd have to deal with LWIP internals
|
|
sendPending = true;
|
|
}
|
|
}
|
|
|
|
// End
|