Welcome to mirror list, hosted at ThFree Co, Russian Federation.

github.com/Duet3D/RepRapFirmware.git - Unnamed repository; edit this file 'description' to name the repository.
summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
Diffstat (limited to 'src/DuetNG/DuetEthernet/Webserver.cpp')
-rw-r--r--src/DuetNG/DuetEthernet/Webserver.cpp2918
1 files changed, 2907 insertions, 11 deletions
diff --git a/src/DuetNG/DuetEthernet/Webserver.cpp b/src/DuetNG/DuetEthernet/Webserver.cpp
index 2a1740e8..293b77e0 100644
--- a/src/DuetNG/DuetEthernet/Webserver.cpp
+++ b/src/DuetNG/DuetEthernet/Webserver.cpp
@@ -1,56 +1,2952 @@
-/*
- * Webserver.cpp
- *
- * Created on: 13 Dec 2016
- * Author: David
- */
+/****************************************************************************************************
+
+ 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. Times should be generally specified
+ in the format YYYY-MM-DDTHH:MM:SS so the firmware can parse them.
+
+ rr_connect?password=xxx&time=yyy
+ 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_filelist?dir=xxx
+ Returns a JSON-formatted list of all the files in xxx including the type and size in the
+ following format: "files":[{"type":'f/d',"name":"xxx",size:yyy},...]
+
+ rr_files?dir=xxx&flagDirs={1/0} [DEPRECATED]
+ 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. If flagDirs is set to 1, all directories will be
+ prefixed by an asterisk.
+
+ rr_reply Returns the last-known G-code reply as plain text (not encapsulated as JSON).
+
+ rr_configfile [DEPRECATED]
+ Sends the config file as plain text (not encapsulated as JSON either).
+
+ rr_download?name=xxx
+ Download a specified file from the SD card
+
+ rr_upload?name=xxx&time=yyy
+ 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_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"
-Webserver::Webserver(Platform* p, Network *n)
- : seq(0)
-{
+//***************************************************************************************************
+
+const char* overflowResponse = "overflow";
+const char* badEscapeResponse = "bad escape";
+
+//********************************************************************************************
+//
+//**************************** Generic Webserver implementation ******************************
+//
+//********************************************************************************************
+
+
+// Constructor and initialisation
+Webserver::Webserver(Platform* p, Network *n) : platform(p), network(n), webserverActive(false)
+{
+ 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
+ longWait = platform->Time();
+ webserverActive = true;
+ readingConnection = nullptr;
+ // initialise all protocol handlers
+ httpInterpreter->ResetState();
+ ftpInterpreter->ResetState();
+ telnetInterpreter->ResetState();
}
+// Deal with input/output from/to the client (if any)
void Webserver::Spin()
{
+ if (webserverActive)
+ {
+ // Check if we can actually send something back to the client
+ if (OutputBuffer::GetBytesLeft(nullptr) == 0)
+ {
+ platform->ClassReport(longWait);
+ return;
+ }
+
+ // We must ensure that we have exclusive access to LWIP
+ if (!network->Lock())
+ {
+ platform->ClassReport(longWait);
+ return;
+ }
+
+ // Allow each ProtocolInterpreter to do something
+ httpInterpreter->Spin();
+ ftpInterpreter->Spin();
+ telnetInterpreter->Spin();
+
+ // See if we have new data to process
+ currentTransaction = network->GetTransaction(readingConnection);
+ if (currentTransaction != nullptr)
+ {
+ // Take care of different protocol types here
+ ProtocolInterpreter *interpreter;
+ uint16_t localPort = currentTransaction->GetLocalPort();
+ switch (localPort)
+ {
+ case FTP_PORT: /* FTP */
+ interpreter = ftpInterpreter;
+ break;
+ case TELNET_PORT: /* Telnet */
+ interpreter = telnetInterpreter;
+ break;
+
+ default: /* HTTP and FTP data */
+ if (localPort == network->GetHttpPort())
+ {
+ interpreter = httpInterpreter;
+ }
+ else
+ {
+ interpreter = ftpInterpreter;
+ }
+ break;
+ }
+
+ // See if we have to print some debug info
+ if (reprap.Debug(moduleWebserver))
+ {
+ const char *type;
+ switch (currentTransaction->GetStatus())
+ {
+ case released: type = "released"; break;
+ case connected: type = "connected"; break;
+ case receiving: type = "receiving"; break;
+ case sending: type = "sending"; break;
+ case disconnected: type = "disconnected"; break;
+ case deferred: type = "deferred"; break;
+ case acquired: type = "acquired"; break;
+ default: type = "unknown"; break;
+ }
+ platform->MessageF(HOST_MESSAGE, "Incoming transaction: Type %s at local port %d (remote port %d)\n",
+ type, localPort, currentTransaction->GetRemotePort());
+ }
+
+ // For protocols other than HTTP it is important to send a HELO message
+ TransactionStatus status = currentTransaction->GetStatus();
+ if (status == connected)
+ {
+ interpreter->ConnectionEstablished();
+ }
+ // 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)
+ {
+ // This will call the disconnect events and effectively close the connection
+ currentTransaction->Discard();
+ }
+ // Check for fast uploads via this connection
+ else if (interpreter->DoingFastUpload())
+ {
+ interpreter->DoFastUpload();
+ }
+ // Process other messages (if we can)
+ else if (interpreter->CanParseData())
+ {
+ readingConnection = currentTransaction->GetConnection();
+ for(size_t i = 0; i < TCP_MSS / 3; i++)
+ {
+ char c;
+ if (currentTransaction->Read(c))
+ {
+ // Each ProtocolInterpreter must take care of the current NetworkTransaction by
+ // calling either Commit(), Discard() or Defer()
+ if (interpreter->CharFromClient(c))
+ {
+ readingConnection = nullptr;
+ break;
+ }
+ }
+ else
+ {
+ // We ran out of data before finding a complete request. This happens when the incoming
+ // message length exceeds the TCP MSS. Notify the current ProtocolInterpreter about this,
+ // which will remove the current transaction too
+ interpreter->NoMoreDataAvailable();
+ readingConnection = nullptr;
+ break;
+ }
+ }
+ }
+ }
+ else if (readingConnection != nullptr)
+ {
+ // We failed to find a transaction for a reading connection.
+ // This should never happen, but if it does, terminate this connection instantly
+ platform->Message(HOST_MESSAGE, "Error: Transaction for reading connection not found\n");
+ readingConnection->Terminate();
+ }
+ network->Unlock(); // unlock LWIP again
+ }
+ platform->ClassReport(longWait);
}
void Webserver::Exit()
{
+ httpInterpreter->CancelUpload();
+ ftpInterpreter->CancelUpload();
+ //telnetInterpreter->CancelUpload(); // Telnet doesn't support fast file uploads
+ platform->Message(HOST_MESSAGE, "Webserver class exited.\n");
+ webserverActive = false;
}
void Webserver::Diagnostics(MessageType mtype)
{
-
+ platform->Message(mtype, "=== Webserver ===\n");
+ httpInterpreter->Diagnostics(mtype);
+ ftpInterpreter->Diagnostics(mtype);
+ telnetInterpreter->Diagnostics(mtype);
}
bool Webserver::GCodeAvailable(const WebSource source) const
{
+ switch (source)
+ {
+ case WebSource::HTTP:
+ return httpInterpreter->GCodeAvailable();
+
+ case WebSource::Telnet:
+ return telnetInterpreter->GCodeAvailable();
+ }
+
return false;
}
char Webserver::ReadGCode(const WebSource source)
{
+ switch (source)
+ {
+ case WebSource::HTTP:
+ return httpInterpreter->ReadGCode();
+
+ case WebSource::Telnet:
+ return telnetInterpreter->ReadGCode();
+ }
+
return 0;
}
void Webserver::HandleGCodeReply(const WebSource source, OutputBuffer *reply)
{
+ switch (source)
+ {
+ case WebSource::HTTP:
+ httpInterpreter->HandleGCodeReply(reply);
+ break;
+ case WebSource::Telnet:
+ telnetInterpreter->HandleGCodeReply(reply);
+ break;
+ }
}
void Webserver::HandleGCodeReply(const WebSource source, const char *reply)
{
+ switch (source)
+ {
+ case WebSource::HTTP:
+ httpInterpreter->HandleGCodeReply(reply);
+ break;
+
+ case WebSource::Telnet:
+ telnetInterpreter->HandleGCodeReply(reply);
+ break;
+ }
+}
+
+uint16_t Webserver::GetGCodeBufferSpace(const WebSource source) const
+{
+ switch (source)
+ {
+ case WebSource::HTTP:
+ return httpInterpreter->GetGCodeBufferSpace();
+
+ case WebSource::Telnet:
+ return telnetInterpreter->GetGCodeBufferSpace();
+ }
+
+ return 0;
+}
+
+// Handle immediate disconnects here (cs will be freed after this call)
+// May be called by ISR, but not while LwIP is NOT locked
+void Webserver::ConnectionLost(const ConnectionState *cs)
+{
+ // Inform protocol handlers that this connection has been lost
+ uint16_t localPort = cs->GetLocalPort();
+ ProtocolInterpreter *interpreter;
+ switch (localPort)
+ {
+ case FTP_PORT: /* FTP */
+ interpreter = ftpInterpreter;
+ break;
+
+ case TELNET_PORT: /* Telnet */
+ interpreter = telnetInterpreter;
+ break;
+
+ default: /* HTTP and FTP data */
+ if (localPort == network->GetHttpPort())
+ {
+ interpreter = httpInterpreter;
+ break;
+ }
+ else if (localPort == network->GetDataPort())
+ {
+ interpreter = ftpInterpreter;
+ break;
+ }
+
+ platform->MessageF(GENERIC_MESSAGE, "Error: Webserver should handle disconnect event at local port %d, but no handler was found!\n", localPort);
+ return;
+ }
+
+ // Print some debug information and notify the protocol interpreter
+ if (reprap.Debug(moduleWebserver))
+ {
+ platform->MessageF(HOST_MESSAGE, "ConnectionLost called for local port %d (remote port %d)\n", localPort, cs->GetRemotePort());
+ }
+ interpreter->ConnectionLost(cs);
+
+ // Don't process any more data from this connection if has gone down
+ if (readingConnection == cs)
+ {
+ readingConnection = nullptr;
+ }
+}
+
+
+//********************************************************************************************
+//
+//********************** Generic Procotol Interpreter implementation *************************
+//
+//********************************************************************************************
+
+ProtocolInterpreter::ProtocolInterpreter(Platform *p, Webserver *ws, Network *n)
+ : platform(p), webserver(ws), network(n)
+{
+ uploadState = notUploading;
+ filenameBeingUploaded[0] = 0;
+}
+
+void ProtocolInterpreter::Spin()
+{
+ // Check if anything went wrong while writing upload data, and delete the file again if that is the case
+ if (uploadState == uploadError)
+ {
+ if (fileBeingUploaded.IsLive())
+ {
+ fileBeingUploaded.Close();
+ }
+ if (filenameBeingUploaded[0] != 0)
+ {
+ platform->GetMassStorage()->Delete(FS_PREFIX, filenameBeingUploaded);
+ }
+
+ uploadState = notUploading;
+ filenameBeingUploaded[0] = 0;
+ }
+}
+
+void ProtocolInterpreter::ConnectionEstablished()
+{
+ // Don't care about incoming connections by default
+ webserver->currentTransaction->Discard();
+}
+
+void ProtocolInterpreter::NoMoreDataAvailable()
+{
+ // Request is not complete yet, but don't care. Interpreters that do not explicitly
+ // overwrite this method don't support more than one connected client anyway
+ webserver->currentTransaction->Discard();
+}
+
+// Start writing to a new file
+bool ProtocolInterpreter::StartUpload(FileStore *file, const char *fileName)
+{
+ if (file != nullptr)
+ {
+ fileBeingUploaded.Set(file);
+ strncpy(filenameBeingUploaded, fileName, ARRAY_SIZE(filenameBeingUploaded));
+ filenameBeingUploaded[ARRAY_UPB(filenameBeingUploaded)] = 0;
+
+ uploadState = uploadOK;
+ return true;
+ }
+
+ platform->Message(GENERIC_MESSAGE, "Error: Could not open file while starting upload!\n");
+ return false;
+}
+
+void ProtocolInterpreter::CancelUpload()
+{
+ if (uploadState == uploadOK)
+ {
+ // Do the file handling next time when Spin is called
+ uploadState = uploadError;
+ }
+}
+
+void ProtocolInterpreter::DoFastUpload()
+{
+ NetworkTransaction *transaction = webserver->currentTransaction;
+
+ const char *buffer;
+ size_t len;
+ if (transaction->ReadBuffer(buffer, len))
+ {
+ // See if we can output a debug message
+ if (reprap.Debug(moduleWebserver))
+ {
+ platform->MessageF(HOST_MESSAGE, "Writing %u bytes of upload data\n", len);
+ }
+
+ // Writing data usually takes a while, so keep LwIP running while this is being done
+ network->Unlock();
+ if (!fileBeingUploaded.Write(buffer, len))
+ {
+ platform->Message(GENERIC_MESSAGE, "Error: Could not write upload data!\n");
+ CancelUpload();
+
+ while (!network->Lock());
+ transaction->Commit(false);
+ return;
+ }
+ while (!network->Lock());
+ }
+
+ if (uploadState != uploadOK || !transaction->HasMoreDataToRead())
+ {
+ transaction->Discard();
+ }
+}
+
+bool ProtocolInterpreter::FinishUpload(uint32_t fileLength)
+{
+ // Flush remaining data for FSO
+ if (uploadState == uploadOK && !fileBeingUploaded.Flush())
+ {
+ uploadState = uploadError;
+ platform->Message(GENERIC_MESSAGE, "Error: 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->MessageF(GENERIC_MESSAGE, "Error: Uploaded file size is different (%u vs. expected %u bytes)!\n", fileBeingUploaded.Length(), fileLength);
+ }
+
+ // Close the file
+ if (fileBeingUploaded.IsLive())
+ {
+ fileBeingUploaded.Close();
+ }
+
+ // Delete the file again if an error has occurred
+ if (uploadState == uploadError && filenameBeingUploaded[0] != 0)
+ {
+ platform->GetMassStorage()->Delete(FS_PREFIX, filenameBeingUploaded);
+ }
+
+ // Clean up again
+ bool success = (uploadState == uploadOK);
+ uploadState = notUploading;
+ filenameBeingUploaded[0] = 0;
+ return success;
+}
+
+
+//********************************************************************************************
+//
+// *********************** HTTP interpreter for the Webserver class **************************
+//
+//********************************************************************************************
+
+
+
+Webserver::HttpInterpreter::HttpInterpreter(Platform *p, Webserver *ws, Network *n)
+ : ProtocolInterpreter(p, ws, n), state(doingCommandWord), numSessions(0), clientsServed(0)
+{
+ gcodeReadIndex = gcodeWriteIndex = 0;
+ gcodeReply = new OutputStack();
+ deferredRequestConnection = nullptr;
+ seq = 0;
+}
+
+void Webserver::HttpInterpreter::Diagnostics(MessageType mt)
+{
+ platform->MessageF(mt, "HTTP sessions: %d of %d\n", numSessions, maxHttpSessions);
+}
+
+void Webserver::HttpInterpreter::Spin()
+{
+ // Deal with aborted uploads
+ ProtocolInterpreter::Spin();
+
+ // Verify HTTP sessions
+ const uint32_t now = millis();
+ for(int i = numSessions - 1; i >= 0; i--)
+ {
+ if (sessions[i].isPostUploading)
+ {
+ // Check for cancelled POST uploads
+ if (uploadState != uploadOK)
+ {
+ sessions[i].isPostUploading = false;
+ sessions[i].lastQueryTime = millis();
+ }
+ }
+ else if ((now - sessions[i].lastQueryTime) > httpSessionTimeout)
+ {
+ // Check for timed out sessions
+ for(size_t k = i + 1; k < numSessions; k++)
+ {
+ memcpy(&sessions[k - 1], &sessions[k], sizeof(HttpSession));
+ }
+ numSessions--;
+ clientsServed++; // assume the disconnected client hasn't fetched the G-Code reply yet
+ }
+ }
+
+ // If we cannot send the G-Code reply to anyone, we may free up some run-time space by dumping it
+ if (numSessions == 0 || clientsServed >= numSessions)
+ {
+ while (!gcodeReply->IsEmpty())
+ {
+ OutputBuffer::ReleaseAll(gcodeReply->Pop());
+ }
+ clientsServed = 0;
+ }
+
+}
+
+// File Uploads
+
+bool Webserver::HttpInterpreter::DoingFastUpload() const
+{
+ uint32_t remoteIP = webserver->currentTransaction->GetRemoteIP();
+ uint16_t remotePort = webserver->currentTransaction->GetRemotePort();
+ for(size_t i = 0; i < numSessions; i++)
+ {
+ if (sessions[i].ip == remoteIP && sessions[i].isPostUploading)
+ {
+ // There is only one session per IP address...
+ return (sessions[i].postPort == remotePort);
+ }
+ }
+ return false;
+}
+
+void Webserver::HttpInterpreter::DoFastUpload()
+{
+ NetworkTransaction *transaction = webserver->currentTransaction;
+
+ // Write some data on the SD card
+ const char *buffer;
+ size_t len;
+ if (transaction->ReadBuffer(buffer, len))
+ {
+ network->Unlock();
+ // Write data in sector-aligned chunks. This also means that the buffer in fatfs is only used to hold the FAT.
+ static const size_t writeBufLength = 2048; // use a multiple of the 512b sector size
+ static uint32_t writeBufStorage[writeBufLength/4]; // aligned buffer for file writes
+ static size_t writeBufIndex;
+ char* const writeBuf = (char *)writeBufStorage;
+
+ if (uploadedBytes == 0)
+ {
+ writeBufIndex = 0;
+ }
+
+ while (len != 0)
+ {
+ size_t lengthToCopy = min<size_t>(writeBufLength - writeBufIndex, len);
+ memcpy(writeBuf + writeBufIndex, buffer, lengthToCopy);
+ writeBufIndex += lengthToCopy;
+ uploadedBytes += lengthToCopy;
+ buffer += lengthToCopy;
+ len -= lengthToCopy;
+ if (writeBufIndex == writeBufLength || uploadedBytes >= postFileLength)
+ {
+ bool success = fileBeingUploaded.Write(writeBuf, writeBufIndex);
+ writeBufIndex = 0;
+ if (!success)
+ {
+ platform->Message(GENERIC_MESSAGE, "Error: Could not write upload data!\n");
+ CancelUpload();
+
+ while (!network->Lock());
+ SendJsonResponse("upload");
+ return;
+ }
+ }
+ }
+ while (!network->Lock());
+ }
+
+ // See if the upload has finished
+ if (uploadState == uploadOK && uploadedBytes >= postFileLength)
+ {
+ // Reset POST upload state for this client
+ uint32_t remoteIP = transaction->GetRemoteIP();
+ for(size_t i = 0; i < numSessions; i++)
+ {
+ if (sessions[i].ip == remoteIP && sessions[i].isPostUploading)
+ {
+ sessions[i].isPostUploading = false;
+ sessions[i].lastQueryTime = millis();
+ break;
+ }
+ }
+
+ // Grab a copy of the filename and finish this upload
+ char filename[FILENAME_LENGTH];
+ strncpy(filename, filenameBeingUploaded, FILENAME_LENGTH);
+ FinishUpload(postFileLength);
+
+ // Update the file timestamp if it was specified before
+ if (fileLastModified != 0)
+ {
+ (void)platform->GetMassStorage()->SetLastModifiedTime(nullptr, filename, fileLastModified);
+ }
+
+ // Eventually send the JSON response
+ SendJsonResponse("upload");
+ }
+ else if (uploadState != uploadOK || !transaction->HasMoreDataToRead())
+ {
+ // We cannot read any more, discard the transaction again
+ transaction->Discard();
+ }
+}
+
+// Output to the client
+
+// Start sending a file or a JSON response.
+void Webserver::HttpInterpreter::SendFile(const char* nameOfFileToSend, bool isWebFile)
+{
+ NetworkTransaction *transaction = webserver->currentTransaction;
+ FileStore *fileToSend;
+ bool zip = false;
+
+ if (isWebFile)
+ {
+ if (nameOfFileToSend[0] == '/')
+ {
+ ++nameOfFileToSend; // all web files are relative to the /www folder, so remove the leading '/'
+ if (nameOfFileToSend[0] == 0)
+ {
+ nameOfFileToSend = INDEX_PAGE_FILE;
+ }
+ }
+ fileToSend = platform->GetFileStore(platform->GetWebDir(), nameOfFileToSend, false);
+
+ // If we failed to open the file, see if we can open a file with the same name and a ".gz" extension
+ if (fileToSend == nullptr && !StringEndsWith(nameOfFileToSend, ".gz") && strlen(nameOfFileToSend) + 3 <= FILENAME_LENGTH)
+ {
+ char nameBuf[FILENAME_LENGTH + 1];
+ strcpy(nameBuf, nameOfFileToSend);
+ strcat(nameBuf, ".gz");
+ fileToSend = platform->GetFileStore(platform->GetWebDir(), nameBuf, false);
+ if (fileToSend != nullptr)
+ {
+ zip = true;
+ }
+ }
+
+ // If we still couldn't find the file and it was an HTML file, return the 404 error page
+ if (fileToSend == nullptr && (StringEndsWith(nameOfFileToSend, ".html") || StringEndsWith(nameOfFileToSend, ".htm")))
+ {
+ nameOfFileToSend = FOUR04_PAGE_FILE;
+ fileToSend = platform->GetFileStore(platform->GetWebDir(), nameOfFileToSend, false);
+ }
+
+ if (fileToSend == nullptr)
+ {
+ RejectMessage("not found", 404);
+ return;
+ }
+ transaction->SetFileToWrite(fileToSend);
+ }
+ else
+ {
+ fileToSend = platform->GetFileStore(FS_PREFIX, nameOfFileToSend, false);
+ if (fileToSend == nullptr)
+ {
+ nameOfFileToSend = FOUR04_PAGE_FILE;
+ fileToSend = platform->GetFileStore(platform->GetWebDir(), nameOfFileToSend, false);
+ if (fileToSend == nullptr)
+ {
+ RejectMessage("not found", 404);
+ return;
+ }
+ }
+ transaction->SetFileToWrite(fileToSend);
+ }
+
+ transaction->Write("HTTP/1.1 200 OK\n");
+
+ // Don't cache files served by rr_download
+ if (!isWebFile)
+ {
+ transaction->Write("Cache-Control: no-cache, no-store, must-revalidate\n");
+ transaction->Write("Pragma: no-cache\n");
+ transaction->Write("Expires: 0\n");
+ }
+
+ const char* contentType;
+ 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 if (StringEndsWith(nameOfFileToSend, ".g") || StringEndsWith(nameOfFileToSend, ".gc") || StringEndsWith(nameOfFileToSend, ".gcode"))
+ {
+ contentType = "text/plain";
+ }
+ else
+ {
+ contentType = "application/octet-stream";
+ }
+ transaction->Printf("Content-Type: %s\n", contentType);
+
+ if (zip && fileToSend != nullptr)
+ {
+ transaction->Write("Content-Encoding: gzip\n");
+ transaction->Printf("Content-Length: %lu\n", fileToSend->Length());
+ }
+
+ transaction->Write("Connection: close\n\n");
+ transaction->Commit(false);
+}
+
+void Webserver::HttpInterpreter::SendGCodeReply()
+{
+ // Do we need to keep the G-Code reply for other clients?
+ bool clearReply = false;
+ if (!gcodeReply->IsEmpty())
+ {
+ clientsServed++;
+ if (clientsServed < numSessions)
+ {
+ // Yes - make sure the Network class doesn't discard its buffers yet
+ // NB: This must happen here, because NetworkTransaction::Write() might already release OutputBuffers
+ gcodeReply->IncreaseReferences(1);
+ }
+ else
+ {
+ // No - clean up again later
+ clearReply = true;
+ }
+
+ if (reprap.Debug(moduleWebserver))
+ {
+ platform->MessageF(HOST_MESSAGE, "Sending G-Code reply to client %d of %d (length %u)\n", clientsServed, numSessions, gcodeReply->DataLength());
+ }
+ }
+
+ // Send the whole G-Code reply as plain text to the client
+ NetworkTransaction *transaction = webserver->currentTransaction;
+ transaction->Write("HTTP/1.1 200 OK\n");
+ transaction->Write("Cache-Control: no-cache, no-store, must-revalidate\n");
+ transaction->Write("Pragma: no-cache\n");
+ transaction->Write("Expires: 0\n");
+ transaction->Write("Content-Type: text/plain\n");
+ transaction->Printf("Content-Length: %u\n", gcodeReply->DataLength());
+ transaction->Write("Connection: close\n\n");
+ transaction->Write(gcodeReply);
+ transaction->Commit(false);
+
+ // Possibly clean up the G-code reply once again
+ if (clearReply)
+ {
+ gcodeReply->Clear();
+ }
+}
+
+void Webserver::HttpInterpreter::SendJsonResponse(const char* command)
+{
+ // Try to authorize the user automatically to retain compatibility with the old web interface
+ if (!IsAuthenticated() && reprap.NoPasswordSet())
+ {
+ Authenticate();
+ }
+
+ // Update the authentication status and try handle "text/plain" requests here
+ if (IsAuthenticated())
+ {
+ UpdateAuthentication();
+
+ if (StringEquals(command, "reply")) // rr_reply
+ {
+ SendGCodeReply();
+ return;
+ }
+
+ if (StringEquals(command, "configfile")) // rr_configfile [DEPRECATED]
+ {
+ const char *configPath = platform->GetMassStorage()->CombineName(platform->GetSysDir(), platform->GetConfigFile());
+ char fileName[FILENAME_LENGTH];
+ strncpy(fileName, configPath, FILENAME_LENGTH);
+
+ SendFile(fileName, false);
+ return;
+ }
+
+ if (StringEquals(command, "download") && StringEquals(qualifiers[0].key, "name"))
+ {
+ SendFile(qualifiers[0].value, false);
+ return;
+ }
+ }
+
+ // Try to process a request for JSON responses
+ OutputBuffer *jsonResponse;
+ if (!OutputBuffer::Allocate(jsonResponse))
+ {
+ // Reset the connection immediately if we cannot write any data. Should never happen
+ webserver->currentTransaction->GetConnection()->Terminate();
+ return;
+ }
+
+ bool keepOpen = false;
+ bool mayKeepOpen;
+ if (numQualKeys == 0)
+ {
+ GetJsonResponse(command, jsonResponse, "", "", 0, mayKeepOpen);
+ }
+ else
+ {
+ GetJsonResponse(command, jsonResponse, qualifiers[0].key, qualifiers[0].value, qualifiers[1].key - qualifiers[0].value - 1, mayKeepOpen);
+ }
+
+ // Check special cases of deferred requests (rr_fileinfo) and rejected messages
+ NetworkTransaction *transaction = webserver->currentTransaction;
+ if (transaction->GetStatus() == deferred || transaction->GetStatus() == sending)
+ {
+ OutputBuffer::Release(jsonResponse);
+ return;
+ }
+
+ // Send the JSON response
+
+ 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;
+ }
+ }
+ }
+
+ transaction->Write("HTTP/1.1 200 OK\n");
+ transaction->Write("Cache-Control: no-cache, no-store, must-revalidate\n");
+ transaction->Write("Pragma: no-cache\n");
+ transaction->Write("Expires: 0\n");
+ transaction->Write("Content-Type: application/json\n");
+ transaction->Printf("Content-Length: %u\n", (jsonResponse != nullptr) ? jsonResponse->Length() : 0);
+ transaction->Printf("Connection: %s\n\n", keepOpen ? "keep-alive" : "close");
+ transaction->Write(jsonResponse);
+
+ transaction->Commit(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.
+void Webserver::HttpInterpreter::GetJsonResponse(const char* request, OutputBuffer *&response, const char* key, const char* value, size_t valueLength, bool& keepOpen)
+{
+ keepOpen = false; // assume we don't want to persist the connection
+
+ if (StringEquals(request, "connect") && StringEquals(key, "password"))
+ {
+ if (IsAuthenticated() || reprap.CheckPassword(value))
+ {
+ // Password OK
+ if (Authenticate())
+ {
+ // See if we can update the current RTC date and time
+ if (numQualKeys > 1 && StringEquals(qualifiers[1].key, "time") && !platform->IsDateTimeSet())
+ {
+ struct tm timeInfo;
+ memset(&timeInfo, 0, sizeof(timeInfo));
+ if (strptime(qualifiers[1].value, "%Y-%m-%dT%H:%M:%S", &timeInfo) != nullptr)
+ {
+ time_t newTime = mktime(&timeInfo);
+ platform->SetDateTime(newTime);
+ }
+ }
+
+ // Client has been logged in
+ response->printf("{\"err\":0,\"sessionTimeout\":%u,\"boardType\":\"%s\"}", httpSessionTimeout, platform->GetBoardString());
+ }
+ else
+ {
+ // No more HTTP sessions available
+ response->copy("{\"err\":2}");
+ }
+ }
+ else
+ {
+ // Wrong password
+ response->copy("{\"err\":1}");
+ }
+ }
+ else if (!IsAuthenticated())
+ {
+ RejectMessage("Not authorized", 500);
+ }
+ else if (StringEquals(request, "disconnect"))
+ {
+ response->printf("{\"err\":%d}", RemoveAuthentication() ? 0 : 1);
+ }
+ 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;
+ }
+
+ OutputBuffer::Release(response);
+ response = reprap.GetStatusResponse(type, ResponseSource::HTTP);
+ }
+ else
+ {
+ // Deprecated
+ OutputBuffer::Release(response);
+ response = reprap.GetLegacyStatusResponse(1, 0);
+ }
+ }
+ else if (StringEquals(request, "gcode") && StringEquals(key, "gcode"))
+ {
+ LoadGcodeBuffer(value);
+ response->printf("{\"buff\":%u}", GetGCodeBufferSpace());
+ }
+ else if (StringEquals(request, "upload"))
+ {
+ response->printf("{\"err\":%d}", (uploadedBytes == postFileLength) ? 0 : 1);
+ }
+ else if (StringEquals(request, "delete") && StringEquals(key, "name"))
+ {
+ bool ok = platform->GetMassStorage()->Delete(FS_PREFIX, value);
+ response->printf("{\"err\":%d}", (ok) ? 0 : 1);
+ }
+ else if (StringEquals(request, "filelist"))
+ {
+ OutputBuffer::Release(response);
+ response = reprap.GetFilelistResponse(value);
+ }
+ else if (StringEquals(request, "files"))
+ {
+ const char* dir = (StringEquals(key, "dir")) ? value : platform->GetGCodeDir();
+ bool flagDirs = false;
+ if (numQualKeys >= 2)
+ {
+ if (StringEquals(qualifiers[1].key, "flagDirs"))
+ {
+ flagDirs = StringEquals(qualifiers[1].value, "1");
+ }
+ }
+ OutputBuffer::Release(response);
+ response = reprap.GetFilesResponse(dir, flagDirs);
+ }
+ else if (StringEquals(request, "fileinfo"))
+ {
+ if (deferredRequestConnection != nullptr)
+ {
+ // Don't allow multiple deferred requests to be processed at once
+ webserver->currentTransaction->Defer(DeferralMode::ResetData);
+ }
+ else
+ {
+ if (StringEquals(qualifiers[0].key, "name"))
+ {
+ // Regular rr_fileinfo?name=xxx call
+ strncpy(filenameBeingProcessed, value, ARRAY_SIZE(filenameBeingProcessed));
+ filenameBeingProcessed[ARRAY_UPB(filenameBeingProcessed)] = 0;
+ }
+ else
+ {
+ // Simple rr_fileinfo call to get info about the file being printed
+ filenameBeingProcessed[0] = 0;
+ }
+
+ deferredRequestConnection = webserver->currentTransaction->GetConnection();
+ ProcessDeferredRequest();
+ }
+ }
+ 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) ? 0 : 1);
+ }
+ else
+ {
+ response->printf("{\"err\":1}");
+ }
+ }
+ else
+ {
+ response->printf("{\"err\":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"))
+ {
+ OutputBuffer::Release(response);
+ response = reprap.GetConfigResponse();
+ }
+ else
+ {
+ RejectMessage("Unknown request", 500);
+ }
+}
+
+void Webserver::HttpInterpreter::ResetState()
+{
+ clientPointer = 0;
+ state = doingCommandWord;
+ numCommandWords = 0;
+ numQualKeys = 0;
+ numHeaderKeys = 0;
+ commandWords[0] = clientMessage;
+}
+
+void Webserver::HttpInterpreter::NoMoreDataAvailable()
+{
+ RejectMessage("Incomplete or too long HTTP request", 500);
+}
+
+// May be called from ISR!
+void Webserver::HttpInterpreter::ConnectionLost(const ConnectionState *cs)
+{
+ // Make sure deferred requests are cancelled
+ if (deferredRequestConnection == cs)
+ {
+ reprap.GetPrintMonitor()->StopParsing(filenameBeingProcessed);
+ deferredRequestConnection = nullptr;
+ }
+
+ // If we couldn't read an entire request from a connection, reset our state here again
+ if (webserver->readingConnection == cs)
+ {
+ ResetState();
+ }
+
+ // 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)
+ {
+ const uint32_t remoteIP = cs->GetRemoteIP();
+ const uint16_t remotePort = cs->GetRemotePort();
+ for(size_t i = 0; i < numSessions; i++)
+ {
+ if (sessions[i].ip == remoteIP && sessions[i].isPostUploading && sessions[i].postPort == remotePort)
+ {
+ if (reprap.Debug(moduleWebserver))
+ {
+ platform->MessageF(HOST_MESSAGE, "POST upload for '%s' has been cancelled!\n", filenameBeingUploaded);
+ }
+ sessions[i].isPostUploading = false;
+ CancelUpload();
+ break;
+ }
+ }
+ }
+}
+
+bool Webserver::HttpInterpreter::CanParseData()
+{
+ // We want to send a response, but we need memory for that. Check if we have to truncate the G-Code reply
+ while (OutputBuffer::GetBytesLeft(nullptr) < minHttpResponseSize)
+ {
+ if (gcodeReply->IsEmpty())
+ {
+ // We cannot truncate any G-Code reply and don't have enough free space, try again later
+ return false;
+ }
+
+ if (OutputBuffer::Truncate(gcodeReply->GetFirstItem(), minHttpResponseSize) == 0)
+ {
+ // Truncating didn't work out, but see if we can free up a few more bytes by releasing the first reply item
+ OutputBuffer::ReleaseAll(gcodeReply->Pop());
+ }
+ }
+
+ // Are we still processing a deferred request?
+ if (deferredRequestConnection == webserver->currentTransaction->GetConnection())
+ {
+ if (deferredRequestConnection->IsConnected())
+ {
+ // Process more of this request. If it doesn't finish this time, it will be appended to the list
+ // of ready transactions again, which will ensure it can be processed later again
+ ProcessDeferredRequest();
+ }
+ else
+ {
+ // Don't bother with this request if the connection has been closed.
+ // We expect a "disconnected" transaction to report this later, so don't clean up anything here
+ webserver->currentTransaction->Discard();
+ }
+ return false;
+ }
+
+ return true;
+}
+
+// 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
+ 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())
+ {
+ 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())
+ {
+ 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;
+
+ 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->MessageF(HOST_MESSAGE, "HTTP req, command words {", numCommandWords);
+ for (size_t i = 0; i < numCommandWords; ++i)
+ {
+ platform->MessageF(HOST_MESSAGE, " %s", commandWords[i]);
+ }
+ platform->Message(HOST_MESSAGE, " }, parameters {");
+
+ for (size_t i = 0; i < numQualKeys; ++i)
+ {
+ platform->MessageF(HOST_MESSAGE, " %s=%s", qualifiers[i].key, qualifiers[i].value);
+ }
+ platform->Message(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], true);
+ }
+
+ ResetState();
+ return true;
+ }
+ else if (IsAuthenticated() && 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 (IsUploading())
+ {
+ 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)
+ {
+ return RejectMessage("invalid POST upload request");
+ }
+
+ // Start a new file upload
+ FileStore *file = platform->GetFileStore(FS_PREFIX, qualifiers[0].value, true);
+ if (!StartUpload(file, qualifiers[0].value))
+ {
+ return RejectMessage("could not start file upload");
+ }
+
+ // Try to get the last modified file date and time
+ if (numQualKeys > 1 && StringEquals(qualifiers[1].key, "time"))
+ {
+ struct tm timeInfo;
+ memset(&timeInfo, 0, sizeof(timeInfo));
+ if (strptime(qualifiers[1].value, "%Y-%m-%dT%H:%M:%S", &timeInfo) != nullptr)
+ {
+ fileLastModified = mktime(&timeInfo);
+ }
+ else
+ {
+ fileLastModified = 0;
+ }
+ }
+ else
+ {
+ fileLastModified = 0;
+ }
+
+ if (reprap.Debug(moduleWebserver))
+ {
+ platform->MessageF(HOST_MESSAGE, "Start uploading file %s length %lu\n", qualifiers[0].value, postFileLength);
+ }
+ uploadedBytes = 0;
+
+ // Keep track of the connection that is now uploading
+ uint32_t remoteIP = webserver->currentTransaction->GetRemoteIP();
+ uint16_t remotePort = webserver->currentTransaction->GetRemotePort();
+ for(size_t i = 0; i < numSessions; i++)
+ {
+ if (sessions[i].ip == remoteIP)
+ {
+ sessions[i].postPort = remotePort;
+ sessions[i].isPostUploading = true;
+ break;
+ }
+ }
+
+ ResetState();
+ return true;
+ }
+ }
+ return RejectMessage("only rr_upload is supported for POST requests");
+ }
+ else
+ {
+ return RejectMessage("Unknown message type or not authenticated");
+ }
+}
+
+// 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->MessageF(HOST_MESSAGE, "Webserver: rejecting message with: %s\n", response);
+
+ NetworkTransaction *transaction = webserver->currentTransaction;
+ transaction->Printf("HTTP/1.1 %u %s\nConnection: close\n\n", code, response);
+ transaction->Commit(false);
+
+ ResetState();
+
+ return true;
+}
+
+// Authenticate current IP and return true on success
+bool Webserver::HttpInterpreter::Authenticate()
+{
+ if (numSessions < maxHttpSessions)
+ {
+ sessions[numSessions].ip = webserver->currentTransaction->GetRemoteIP();
+ sessions[numSessions].lastQueryTime = millis();
+ sessions[numSessions].isPostUploading = false;
+ numSessions++;
+ return true;
+ }
+ return false;
+}
+
+bool Webserver::HttpInterpreter::IsAuthenticated() const
+{
+ const uint32_t remoteIP = webserver->currentTransaction->GetRemoteIP();
+ for(size_t i = 0; i < numSessions; i++)
+ {
+ if (sessions[i].ip == remoteIP)
+ {
+ return true;
+ }
+ }
+ return false;
+}
+
+void Webserver::HttpInterpreter::UpdateAuthentication()
+{
+ const uint32_t remoteIP = webserver->currentTransaction->GetRemoteIP();
+ for(size_t i = 0; i < numSessions; i++)
+ {
+ if (sessions[i].ip == remoteIP)
+ {
+ sessions[i].lastQueryTime = millis();
+ break;
+ }
+ }
+}
+
+bool Webserver::HttpInterpreter::RemoveAuthentication()
+{
+ const uint32_t remoteIP = webserver->currentTransaction->GetRemoteIP();
+ for(int i=(int)numSessions - 1; i>=0; i--)
+ {
+ if (sessions[i].ip == remoteIP)
+ {
+ if (sessions[i].isPostUploading)
+ {
+ // Don't allow sessions with active POST uploads to be removed
+ return false;
+ }
+
+ for (size_t k = i + 1; k < numSessions; ++k)
+ {
+ memcpy(&sessions[k - 1], &sessions[k], sizeof(HttpSession));
+ }
+ numSessions--;
+ return true;
+ }
+ }
+ return false;
+}
+
+// Process a received string of gcodes
+void Webserver::HttpInterpreter::LoadGcodeBuffer(const char* gc)
+{
+ char gcodeTempBuf[GCODE_LENGTH];
+ 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(HOST_MESSAGE, "Error: GCode local buffer overflow in HTTP webserver.\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 null-terminated gcode
+// We intercept one M Codes so we can deal with 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::HttpInterpreter::ProcessGcode(const char* gc)
+{
+ if (StringStartsWith(gc, "M112") && !isdigit(gc[4])) // emergency stop
+ {
+ reprap.EmergencyStop();
+ gcodeReadIndex = gcodeWriteIndex; // clear the buffer
+ reprap.GetGCodes()->Reset();
+ }
+ else
+ {
+ StoreGcodeData(gc, strlen(gc) + 1);
+ }
+}
+
+// Process a received string of gcodes
+void Webserver::HttpInterpreter::StoreGcodeData(const char* data, uint16_t len)
+{
+ if (len > GetGCodeBufferSpace())
+ {
+ platform->Message(HOST_MESSAGE, "Error: GCode buffer overflow in HTTP Webserver!\n");
+ }
+ else
+ {
+ uint16_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;
+ }
+}
+
+// Feeding G Codes to the GCodes class
+char Webserver::HttpInterpreter::ReadGCode()
+{
+ char c;
+ if (gcodeReadIndex == gcodeWriteIndex)
+ {
+ c = 0;
+ }
+ else
+ {
+ c = gcodeBuffer[gcodeReadIndex];
+ gcodeReadIndex = (gcodeReadIndex + 1u) % gcodeBufferLength;
+ }
+ return c;
+}
+
+// Handle a G Code reply from the GCodes class
+void Webserver::HttpInterpreter::HandleGCodeReply(OutputBuffer *reply)
+{
+ if (reply != nullptr)
+ {
+ if (numSessions > 0)
+ {
+ // FIXME: This might cause G-code responses to be sent twice to fast HTTP clients, but
+ // I (chrishamm) cannot think of a nicer way to deal with slow clients at the moment...
+ gcodeReply->Push(reply);
+ clientsServed = 0;
+ seq++;
+ }
+ else
+ {
+ // Don't use buffers that may never get released...
+ OutputBuffer::ReleaseAll(reply);
+ }
+ }
+}
+
+void Webserver::HttpInterpreter::HandleGCodeReply(const char *reply)
+{
+ if (numSessions > 0)
+ {
+ OutputBuffer *buffer = gcodeReply->GetLastItem();
+ if (buffer == nullptr || buffer->IsReferenced())
+ {
+ if (!OutputBuffer::Allocate(buffer))
+ {
+ // No more space available, stop here
+ return;
+ }
+ gcodeReply->Push(buffer);
+ }
+
+ buffer->cat(reply);
+ clientsServed = 0;
+ seq++;
+ }
+}
+
+// Called to process a deferred request and takes care of the current Webserver transaction
+void Webserver::HttpInterpreter::ProcessDeferredRequest()
+{
+ OutputBuffer *jsonResponse = nullptr;
+ const ConnectionState *lastDeferredConnection = deferredRequestConnection;
+
+ // At the moment only file info requests are deferred.
+ // Parsing the file may take a while, so keep LwIP running while we're waiting
+ network->Unlock();
+ bool gotFileInfo = reprap.GetPrintMonitor()->GetFileInfoResponse(filenameBeingProcessed, jsonResponse);
+ while (!network->Lock());
+
+ // Because LwIP was unlocked before, there is a chance that the ConnectionLost() call has already
+ // stopped the file parsing. Check this special case here
+ if (lastDeferredConnection == deferredRequestConnection)
+ {
+ NetworkTransaction *transaction = webserver->currentTransaction;
+ if (gotFileInfo)
+ {
+ deferredRequestConnection = nullptr;
+
+ // Got it - send the response now
+ transaction->Write("HTTP/1.1 200 OK\n");
+ transaction->Write("Cache-Control: no-cache, no-store, must-revalidate\n");
+ transaction->Write("Pragma: no-cache\n");
+ transaction->Write("Expires: 0\n");
+ transaction->Write("Content-Type: application/json\n");
+ transaction->Printf("Content-Length: %u\n", (jsonResponse != nullptr) ? jsonResponse->Length() : 0);
+ transaction->Printf("Connection: close\n\n");
+ transaction->Write(jsonResponse);
+
+ transaction->Commit(false);
+ }
+ else
+ {
+ // File hasn't been fully parsed yet, try again later
+ transaction->Defer(DeferralMode::DiscardData);
+ }
+ }
+ else
+ {
+ // Clean up again if we cannot send the response at all
+ OutputBuffer::ReleaseAll(jsonResponse);
+ }
+}
+
+//********************************************************************************************
+//
+//************************* FTP interpreter for the Webserver class **************************
+//
+//********************************************************************************************
+
+Webserver::FtpInterpreter::FtpInterpreter(Platform *p, Webserver *ws, Network *n)
+ : ProtocolInterpreter(p, ws, n), state(authenticating), clientPointer(0)
+{
+ connectedClients = 0;
+ strcpy(currentDir, "/");
+}
+
+void Webserver::FtpInterpreter::Diagnostics(MessageType mt)
+{
+ platform->MessageF(mt, "FTP connections: %d, state %d\n", connectedClients, state);
+}
+
+void Webserver::FtpInterpreter::ConnectionEstablished()
+{
+ connectedClients++;
+ if (reprap.Debug(moduleWebserver))
+ {
+ platform->Message(HOST_MESSAGE, "Webserver: FTP connection established!\n");
+ }
+
+ // Is this a new connection on the data port?
+ NetworkTransaction *transaction = webserver->currentTransaction;
+ if (transaction->GetLocalPort() != FTP_PORT)
+ {
+ if (state == waitingForPasvPort)
+ {
+ // Yes - save it for the main request
+ network->SaveDataConnection();
+ state = pasvPortConnected;
+ transaction->Discard();
+ }
+ else
+ {
+ // Should never get here...
+ transaction->Commit(false);
+ }
+ return;
+ }
+
+ // A client is trying to connect to the main FTP port
+ switch (state)
+ {
+ case idle:
+ case authenticated: // added by DC because without it, we can't transfer any files with FileZilla
+ // We can safely deal with one connection on the main FTP port
+ state = authenticating;
+ SendReply(220, "RepRapFirmware FTP server", true);
+ break;
+
+ default:
+ // But don't allow multiple ones, this could mess things up
+ SendReply(421, "Only one client can be connected at a time.", false);
+ return;
+ }
+}
+
+// May be called from ISR!
+void Webserver::FtpInterpreter::ConnectionLost(const ConnectionState *cs)
+{
+ connectedClients--;
+
+ if (cs->GetLocalPort() != FTP_PORT)
+ {
+ // Did everything work out? Usually this is only called for uploads
+ if (network->AcquireFTPTransaction())
+ {
+ webserver->currentTransaction = network->GetTransaction();
+ if (state == doingPasvIO)
+ {
+ if (uploadState != uploadError && !cs->IsTerminated())
+ {
+ SendReply(226, "Transfer complete.");
+ FinishUpload(0);
+ }
+ else
+ {
+ SendReply(526, "Transfer failed!");
+ }
+ }
+ else
+ {
+ SendReply(550, "Lost data connection!");
+ }
+ }
+
+ // Close the data port and reset our state again
+ network->CloseDataPort();
+ CancelUpload();
+ state = authenticated;
+ }
+
+ if (connectedClients == 0)
+ {
+ // Last one gone now...
+ ResetState();
+ }
+}
+
+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->MessageF(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 = idle;
+}
+
+bool Webserver::FtpInterpreter::DoingFastUpload() const
+{
+ return (IsUploading() && webserver->currentTransaction->GetLocalPort() == network->GetDataPort());
+}
+
+// return true if an error has occurred, false otherwise
+void Webserver::FtpInterpreter::ProcessLine()
+{
+ switch (state)
+ {
+ case idle:
+ 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[PASSWORD_LENGTH];
+ int pass_length = 0;
+ bool reading_pass = false;
+ for(size_t i = 4; i < clientPointer && i < PASSWORD_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"))
+ {
+ NetworkTransaction *transaction = webserver->currentTransaction;
+ transaction->Printf("257 \"%s\"\r\n", currentDir);
+ transaction->Commit(true);
+ }
+ // 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(size_t 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 uint8_t * const ip_address = network->GetIPAddress();
+
+ /* open random port > 1023 */
+ //rand(); // TRNG doesn't require this
+ uint16_t pasv_port = random(1024, 65535);
+ network->OpenDataPort(pasv_port);
+ portOpenTime = millis();
+ state = waitingForPasvPort;
+
+ /* send FTP response */
+ NetworkTransaction *transaction = webserver->currentTransaction;
+ transaction->Printf("227 Entering Passive Mode (%d,%d,%d,%d,%d,%d)\r\n",
+ ip_address[0], ip_address[1], ip_address[2], ip_address[3],
+ pasv_port / 256, pasv_port % 256);
+ transaction->Commit(true);
+ }
+ // 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);
+ if (platform->GetMassStorage()->Delete(currentDir, filename))
+ {
+ SendReply(250, "Delete operation successful.");
+ }
+ else
+ {
+ SendReply(550, "Delete operation failed.");
+ }
+ }
+ // delete directory
+ else if (StringStartsWith(clientMessage, "RMD"))
+ {
+ ReadFilename(3);
+ if (platform->GetMassStorage()->Delete(currentDir, filename))
+ {
+ 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))
+ {
+ NetworkTransaction *transaction = webserver->currentTransaction;
+ transaction->Printf("257 \"%s\" created\r\n", location);
+ transaction->Commit(true);
+ }
+ 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, FILENAME_LENGTH);
+ filename[FILENAME_LENGTH - 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[FILENAME_LENGTH];
+ strncpy(oldFilename, filename, FILENAME_LENGTH);
+ oldFilename[FILENAME_LENGTH - 1] = 0;
+ ReadFilename(4);
+
+ 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 (millis() - portOpenTime > ftpPasvPortTimeout)
+ {
+ SendReply(425, "Failed to establish connection.");
+
+ network->CloseDataPort();
+ state = authenticated;
+ }
+ else
+ {
+ webserver->currentTransaction->Defer(DeferralMode::ResetData);
+ }
+
+ 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"))
+ {
+ if (network->AcquireDataTransaction())
+ {
+ // send announcement via ftp main port
+ SendReply(150, "Here comes the directory listing.");
+
+ // send directory listing via data port
+ NetworkTransaction *dataTransaction = network->GetTransaction();
+
+ FileInfo fileInfo;
+ if (platform->GetMassStorage()->FindFirst(currentDir, 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);
+ dataTransaction->Printf("%crw-rw-rw- 1 ftp ftp %13lu %s %02d %04d %s\r\n",
+ dirChar, fileInfo.size, platform->GetMassStorage()->GetMonthName(timeInfo->tm_mon + 1),
+ timeInfo->tm_mday, timeInfo->tm_year + 1900, fileInfo.fileName);
+ } while (platform->GetMassStorage()->FindNext(fileInfo));
+ }
+
+ dataTransaction->Commit(false);
+ state = doingPasvIO;
+ }
+ else
+ {
+ SendReply(500, "Unknown error.");
+ network->CloseDataPort();
+ state = authenticated;
+ }
+ }
+ // upload a file
+ else if (StringStartsWith(clientMessage, "STOR"))
+ {
+ ReadFilename(4);
+
+ FileStore *file = platform->GetFileStore(currentDir, filename, true);
+ if (StartUpload(file, filename))
+ {
+ 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"))
+ {
+ ReadFilename(4);
+
+ FileStore *file = platform->GetFileStore(currentDir, filename, false);
+ if (file == nullptr)
+ {
+ SendReply(550, "Failed to open file.");
+ }
+ else
+ {
+ if (network->AcquireDataTransaction())
+ {
+ // send announcement via main ftp port
+ NetworkTransaction *transaction = webserver->currentTransaction;
+ transaction->Printf("150 Opening data connection for %s (%lu bytes).\r\n", filename, file->Length());
+ transaction->Commit(true);
+
+ // send the file via data port
+ NetworkTransaction *dataTransaction = network->GetTransaction();
+ dataTransaction->SetFileToWrite(file);
+ dataTransaction->Commit(false);
+ state = doingPasvIO;
+ }
+ else
+ {
+ file->Close();
+ 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 *transaction = webserver->currentTransaction;
+ transaction->Printf("%d %s\r\n", code, message);
+ transaction->Commit(keepConnection);
+}
+
+void Webserver::FtpInterpreter::SendFeatures()
+{
+ NetworkTransaction *transaction = webserver->currentTransaction;
+ transaction->Write("211-Features:\r\n");
+ transaction->Write("PASV\r\n"); // support PASV mode
+ transaction->Write("211 End\r\n");
+ transaction->Commit(true);
+}
+
+void Webserver::FtpInterpreter::ReadFilename(uint16_t start)
+{
+ int filenameLength = 0;
+ bool readingPath = false;
+ for(int i = start; i < (int)clientPointer && filenameLength < (int)(FILENAME_LENGTH - 1); 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[FILENAME_LENGTH];
+
+ if (newDirectory[0] != 0)
+ {
+ /* Prepare the new directory path */
+ if (newDirectory[0] == '/') // absolute path
+ {
+ strncpy(combinedPath, newDirectory, FILENAME_LENGTH);
+ combinedPath[FILENAME_LENGTH - 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, FILENAME_LENGTH);
+ 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, FILENAME_LENGTH);
+ if (strlen(currentDir) > 1)
+ {
+ strncat(combinedPath, "/", FILENAME_LENGTH - strlen(combinedPath) - 1);
+ }
+ strncat(combinedPath, newDirectory, FILENAME_LENGTH - 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()->DirectoryExists(combinedPath))
+ {
+ strncpy(currentDir, combinedPath, FILENAME_LENGTH);
+ 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), connectedClients(0), processNextLine(false), gcodeReadIndex(0), gcodeWriteIndex(0), gcodeReply(nullptr)
+{
+ ResetState();
+}
+
+void Webserver::TelnetInterpreter::Diagnostics(MessageType mt)
+{
+ platform->MessageF(mt, "Telnet connections: %d, state %d\n", connectedClients, state);
+}
+
+void Webserver::TelnetInterpreter::ConnectionEstablished()
+{
+ connectedClients++;
+ NetworkTransaction *transaction = network->GetTransaction();
+
+ // Only one client may be connected via Telnet at once, so check this first
+ if (state != idle)
+ {
+ transaction->Write("Sorry, only one client may be connected via Telnet at once.\r\n");
+ transaction->Commit(false);
+ return;
+ }
+ state = justConnected;
+ connectTime = millis();
+
+ // Check whether we need a password to log in
+ if (reprap.NoPasswordSet())
+ {
+ // Don't send a login prompt if no password is set, so we don't mess up Pronterface
+ transaction->Discard();
+ }
+ else
+ {
+ transaction->Write("RepRapFirmware Telnet interface\r\n\r\n");
+ transaction->Write("Please enter your password:\r\n");
+ transaction->Write("> ");
+ transaction->Commit(true);
+ }
+}
+
+// May be called from ISR!
+void Webserver::TelnetInterpreter::ConnectionLost(const ConnectionState *cs)
+{
+ connectedClients--;
+ if (connectedClients == 0)
+ {
+ ResetState();
+
+ // Don't save up output buffers if they can't be sent
+ OutputBuffer::ReleaseAll(gcodeReply);
+ gcodeReply = nullptr;
+ }
+}
+
+bool Webserver::TelnetInterpreter::CanParseData()
+{
+ // Is this an acquired transaction using which we can send the G-code reply?
+ TransactionStatus status = webserver->currentTransaction->GetStatus();
+ if (status == acquired)
+ {
+ SendGCodeReply();
+ return false;
+ }
+
+ // Is this connection still live? Check that for deferred requests
+ if (status == deferred && !webserver->currentTransaction->IsConnected())
+ {
+ webserver->currentTransaction->Discard();
+ return false;
+ }
+
+ // In order to support TCP streaming mode, check if we can store any more data at this time
+ if (GetGCodeBufferSpace() < clientPointer + 1)
+ {
+ webserver->currentTransaction->Defer(DeferralMode::DeferOnly);
+ return false;
+ }
+
+ // If that works and if the next line hasn't been processed yet, do it now
+ if (processNextLine)
+ {
+ return !ProcessLine();
+ }
+
+ // Otherwise just parse the next request
+ return true;
+}
+
+bool Webserver::TelnetInterpreter::CharFromClient(char c)
+{
+ // If this is likely to be a Telnet setup message (with some garbage in it), dump the first
+ // received packet and move on to the next state
+ if (state == justConnected)
+ {
+ if (reprap.NoPasswordSet())
+ {
+ state = authenticated;
+ network->SaveTelnetConnection();
+ }
+ else
+ {
+ state = authenticating;
+ }
+
+ if (millis() - connectTime < telnetSetupDuration)
+ {
+ network->GetTransaction()->Discard();
+ return true;
+ }
+ }
+
+ // Otherwise try to read one line at a time
+ switch (c)
+ {
+ case 0:
+ break;
+
+ case '\b':
+ // Allow backspace for pure Telnet clients like PuTTY
+ if (clientPointer != 0)
+ {
+ clientPointer--;
+ }
+ break;
+
+ case '\r':
+ case '\n':
+ if (clientPointer != 0)
+ {
+ // This line is complete, do we have enough space left to store it?
+ clientMessage[clientPointer] = 0;
+ if (GetGCodeBufferSpace() < clientPointer + 1)
+ {
+ // No - defer this transaction, so we can process more of it next time
+ webserver->currentTransaction->Defer(DeferralMode::DeferOnly);
+ processNextLine = true;
+ return true;
+ }
+
+ // Yes - try to process it
+ return ProcessLine();
+ }
+ break;
+
+ default:
+ clientMessage[clientPointer++] = c;
+
+ // Make sure we don't overflow the line buffer
+ if (clientPointer == ARRAY_UPB(clientMessage))
+ {
+ clientPointer = 0;
+ platform->Message(HOST_MESSAGE, "Webserver: Buffer overflow in Telnet server!\n");
+ return true;
+ }
+ break;
+ }
+
+ return false;
+}
+
+void Webserver::TelnetInterpreter::ResetState()
+{
+ state = idle;
+ connectTime = 0;
+ clientPointer = 0;
+ gcodeReadIndex = gcodeWriteIndex; // clear the buffer
+}
+
+// Usually we should not try to send any data here, because that would purge the packet's
+// payload and mess with TCP streaming mode if Pronterface is used. However, under special
+// circumstances this must happen and in this case this method must always return true.
+bool Webserver::TelnetInterpreter::ProcessLine()
+{
+ processNextLine = false;
+ clientPointer = 0;
+
+ NetworkTransaction *transaction = network->GetTransaction();
+ switch (state)
+ {
+ case idle:
+ case justConnected:
+ // Should never get here...
+ // no break
+
+ case authenticating:
+ if (reprap.CheckPassword(clientMessage))
+ {
+ network->SaveTelnetConnection();
+ state = authenticated;
+
+ transaction->Write("Log in successful!\r\n");
+ transaction->Commit(true);
+ }
+ else
+ {
+ transaction->Write("Invalid password.\r\n> ");
+ transaction->Commit(true);
+ }
+ return true;
+
+ case authenticated:
+ // Special commands for Telnet
+ if (StringEquals(clientMessage, "exit") || StringEquals(clientMessage, "quit"))
+ {
+ transaction->Write("Goodbye.\r\n");
+ transaction->Commit(false);
+ return true;
+ }
+ // All other codes are stored for the GCodes class
+ ProcessGcode(clientMessage);
+ break;
+ }
+ return false;
+}
+
+// Process a null-terminated gcode
+// We intercept one M Codes so we can deal with 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::TelnetInterpreter::ProcessGcode(const char* gc)
+{
+ if (StringStartsWith(gc, "M112") && !isdigit(gc[4])) // emergency stop
+ {
+ reprap.EmergencyStop();
+ gcodeReadIndex = gcodeWriteIndex; // clear the buffer
+ reprap.GetGCodes()->Reset();
+ }
+ else
+ {
+ StoreGcodeData(gc, strlen(gc) + 1);
+ }
+}
+
+// Process a received string of gcodes
+void Webserver::TelnetInterpreter::StoreGcodeData(const char* data, uint16_t len)
+{
+ if (len > GetGCodeBufferSpace())
+ {
+ platform->Message(HOST_MESSAGE, "Error: GCode buffer overflow in Telnet Webserver!\n");
+ }
+ else
+ {
+ uint16_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;
+ }
+}
+
+// Feeding G Codes to the GCodes class
+char Webserver::TelnetInterpreter::ReadGCode()
+{
+ char c;
+ if (gcodeReadIndex == gcodeWriteIndex)
+ {
+ c = 0;
+ }
+ else
+ {
+ c = gcodeBuffer[gcodeReadIndex];
+ gcodeReadIndex = (gcodeReadIndex + 1u) % gcodeBufferLength;
+ }
+ return c;
+}
+
+// Handle a G-Code reply from the GCodes class; replace \n with \r\n
+void Webserver::TelnetInterpreter::HandleGCodeReply(OutputBuffer *reply)
+{
+ if (reply != nullptr && state >= authenticated)
+ {
+ if (!network->AcquireTelnetTransaction())
+ {
+ // We must be able to send the response to the client on the next Spin call
+ return;
+ }
+
+ // We need a valid OutputBuffer to start the conversion from NL to CRNL
+ if (gcodeReply == nullptr)
+ {
+ OutputBuffer *buffer;
+ if (!OutputBuffer::Allocate(buffer))
+ {
+ OutputBuffer::Truncate(reply, OUTPUT_BUFFER_SIZE);
+ if (!OutputBuffer::Allocate(buffer))
+ {
+ // If we're really short on memory, release the G-Code reply instantly
+ OutputBuffer::ReleaseAll(reply);
+ return;
+ }
+ }
+ gcodeReply = buffer;
+ }
+
+ // Write entire content to new output buffers, but this time with \r\n instead of \n
+ do {
+ const char *data = reply->Data();
+ for(size_t i = 0; i < reply->DataLength(); i++)
+ {
+ if (*data == '\n')
+ {
+ gcodeReply->cat('\r');
+ }
+
+ gcodeReply->cat(*data);
+ data++;
+ }
+ reply = OutputBuffer::Release(reply);
+ } while (reply != nullptr);
+ }
+ else
+ {
+ // Don't store buffers that may never get released...
+ OutputBuffer::ReleaseAll(reply);
+ }
+}
+
+void Webserver::TelnetInterpreter::HandleGCodeReply(const char *reply)
+{
+ if (reply != nullptr && state >= authenticated)
+ {
+ if (!network->AcquireTelnetTransaction())
+ {
+ // We must be able to send the response to the client on the next Spin call
+ return;
+ }
+
+ // We need a valid OutputBuffer to start the conversion from NL to CRNL
+ if (gcodeReply == nullptr)
+ {
+ OutputBuffer *buffer;
+ if (!OutputBuffer::Allocate(buffer))
+ {
+ // No more space available to store this reply, stop here
+ return;
+ }
+ gcodeReply = buffer;
+ }
+
+ // Write entire content to new output buffers, but this time with \r\n instead of \n
+ while (*reply != 0)
+ {
+ if (*reply == '\n' && gcodeReply->cat('\r') == 0)
+ {
+ // No more space available, stop here
+ return;
+ }
+ if (gcodeReply->cat(*reply) == 0)
+ {
+ // No more space available, stop here
+ return;
+ }
+ reply++;
+ };
+ }
+}
+
+void Webserver::TelnetInterpreter::SendGCodeReply()
+{
+ NetworkTransaction *transaction = webserver->currentTransaction;
+
+ if (gcodeReply == nullptr)
+ {
+ transaction->Discard();
+ }
+ else
+ {
+ transaction->Write(gcodeReply);
+ transaction->Commit(true);
+ }
+ gcodeReply = nullptr;
}
-// End
+// vim: ts=4:sw=4