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.cpp2942
1 files changed, 0 insertions, 2942 deletions
diff --git a/src/DuetNG/DuetEthernet/Webserver.cpp b/src/DuetNG/DuetEthernet/Webserver.cpp
deleted file mode 100644
index af1bdcbe..00000000
--- a/src/DuetNG/DuetEthernet/Webserver.cpp
+++ /dev/null
@@ -1,2942 +0,0 @@
-/****************************************************************************************************
-
- 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"
-
-//***************************************************************************************************
-
-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()
-{
- // Check if we are enabled and we can actually send something back to the client
- if (webserverActive && OutputBuffer::GetBytesLeft(nullptr) != 0)
- {
- // We must ensure that we have exclusive access to LWIP
- if (!network->Lock())
- {
- // 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;
- const 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;
-}
-
-// vim: ts=4:sw=4