/* * HttpResponder.cpp * * Created on: 14 Apr 2017 * Author: David */ #include "HttpResponder.h" #if SUPPORT_HTTP #include "Network.h" #include "Socket.h" #include "GCodes/GCodes.h" #include "General/IP4String.h" #define KO_START "rr_" const size_t KoFirst = 3; const char* const overflowResponse = "overflow"; const char* const badEscapeResponse = "bad escape"; const char serviceUnavailableResponse[] = "HTTP/1.1 503 Service Unavailable\r\n\r\n"; static_assert(ARRAY_SIZE(serviceUnavailableResponse) <= OUTPUT_BUFFER_SIZE, "OUTPUT_BUFFER_SIZE too small"); const uint32_t HttpReceiveTimeout = 2000; // Text for a human-readable 404 page const char* const ErrorPagePart1 = "\n" "
\n" "\n" "\n" "Your Duet rejected the HTTP request: "; const char* const ErrorPagePart2 = "
\n" "\n"; HttpResponder::HttpResponder(NetworkResponder *n) noexcept : UploadingNetworkResponder(n) { } // Ask the responder to accept this connection, returns true if it did bool HttpResponder::Accept(Socket *s, NetworkProtocol protocol) noexcept { if (responderState == ResponderState::free && protocol == HttpProtocol) { responderState = ResponderState::reading; skt = s; timer = millis(); // Reset the parse state variables clientPointer = 0; parseState = HttpParseState::doingCommandWord; numCommandWords = 0; numQualKeys = 0; numHeaderKeys = 0; commandWords[0] = clientMessage; if (reprap.Debug(moduleWebserver)) { debugPrintf("HTTP connection accepted\n"); } return true; } return false; } // Do some work, returning true if we did anything significant bool HttpResponder::Spin() noexcept { switch (responderState) { case ResponderState::free: return false; case ResponderState::reading: { bool readSomething = false; char c; while (skt->ReadChar(c)) { if (CharFromClient(c)) { timer = millis(); // restart the timeout return true; } readSomething = true; } // Here when we were not able to read a character but we didn't receive a finished message if (readSomething) { timer = millis(); // restart the timeout return true; } if (!skt->CanRead() || millis() - timer >= HttpReceiveTimeout) { ConnectionLost(); return true; } return false; } case ResponderState::processingRequest: ProcessRequest(); return true; case ResponderState::gettingFileInfo: (void)SendFileInfo(millis() - startedProcessingRequestAt >= MaxFileInfoGetTime); return true; #if HAS_MASS_STORAGE case ResponderState::uploading: DoUpload(); return true; #endif case ResponderState::sending: SendData(); return true; default: // should not happen return false; } } // 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 HttpResponder::CharFromClient(char c) noexcept { switch (parseState) { case HttpParseState::doingCommandWord: switch(c) { case '\n': clientMessage[clientPointer++] = 0; ++numCommandWords; numHeaderKeys = 0; headers[0].key = clientMessage + clientPointer; parseState = HttpParseState::doingHeaderKey; break; case '\r': break; case ' ': case '\t': clientMessage[clientPointer++] = 0; { ++numCommandWords; if (numCommandWords < MaxCommandWords) { commandWords[numCommandWords] = clientMessage + clientPointer; if (numCommandWords == 1) { parseState = HttpParseState::doingFilename; } } else { RejectMessage("too many command words"); return true; } } break; default: clientMessage[clientPointer++] = c; break; } break; case HttpParseState::doingFilename: switch(c) { case '\n': clientMessage[clientPointer++] = 0; ++numCommandWords; numQualKeys = 0; numHeaderKeys = 0; headers[0].key = clientMessage + clientPointer; parseState = HttpParseState::doingHeaderKey; break; case '?': clientMessage[clientPointer++] = 0; ++numCommandWords; numQualKeys = 0; qualifiers[0].key = clientMessage + clientPointer; parseState = HttpParseState::doingQualifierKey; break; case '%': parseState = HttpParseState::doingFilenameEsc1; break; case '\r': break; case ' ': case '\t': clientMessage[clientPointer++] = 0; { ++numCommandWords; if (numCommandWords < MaxCommandWords) { commandWords[numCommandWords] = clientMessage + clientPointer; parseState = HttpParseState::doingCommandWord; } else { RejectMessage("too many command words"); return true; } } break; default: clientMessage[clientPointer++] = c; break; } break; case HttpParseState::doingQualifierKey: switch(c) { case '=': clientMessage[clientPointer++] = 0; qualifiers[numQualKeys].value = clientMessage + clientPointer; ++numQualKeys; parseState = HttpParseState::doingQualifierValue; break; case '\n': // key with no value case ' ': case '\t': case '\r': // IE11 sometimes puts a trailing '?' at the end of a GET request e.g. "GET /fonts/glyphicons.eot? HTTP/1.1" if (numQualKeys == 0 && qualifiers[0].key == clientMessage + clientPointer) { commandWords[numCommandWords] = clientMessage + clientPointer; // we have only 2 command words so far, so no need to check numCommandWords here parseState = HttpParseState::doingCommandWord; break; } // no break case '%': // none of our keys needs escaping, so treat an escape within a key as an error case '&': // key with no value RejectMessage("bad qualifier key"); return true; default: clientMessage[clientPointer++] = c; break; } break; case HttpParseState::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; parseState = HttpParseState::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; parseState = HttpParseState::doingCommandWord; break; case '\r': break; case '%': parseState = HttpParseState::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) { parseState = HttpParseState::doingQualifierKey; } else { RejectMessage("too many keys in qualifier"); return true; } break; case '+': clientMessage[clientPointer++] = ' '; break; default: clientMessage[clientPointer++] = c; break; } break; case HttpParseState::doingFilenameEsc1: case HttpParseState::doingQualifierValueEsc1: if (c >= '0' && c <= '9') { decodeChar = (c - '0') << 4; parseState = (HttpParseState)((int)parseState + 1); } else if (c >= 'A' && c <= 'F') { decodeChar = (c - ('A' - 10)) << 4; parseState = (HttpParseState)((int)parseState + 1); } else { RejectMessage(badEscapeResponse); return true; } break; case HttpParseState::doingFilenameEsc2: case HttpParseState::doingQualifierValueEsc2: if (c >= '0' && c <= '9') { clientMessage[clientPointer++] = decodeChar | (c - '0'); parseState = (HttpParseState)((int)parseState - 2); } else if (c >= 'A' && c <= 'F') { clientMessage[clientPointer++] = decodeChar | (c - ('A' - 10)); parseState = (HttpParseState)((int)parseState - 2); } else { RejectMessage(badEscapeResponse); return true; } break; case HttpParseState::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 { ProcessMessage(); return true; } else { RejectMessage("unexpected newline"); return true; } break; case '\r': break; case ':': if (numHeaderKeys == MaxHeaders - 1) { RejectMessage("too many header key-value pairs"); return true; } clientMessage[clientPointer++] = 0; headers[numHeaderKeys].value = clientMessage + clientPointer; ++numHeaderKeys; parseState = HttpParseState::expectingHeaderValue; break; default: clientMessage[clientPointer++] = c; break; } break; case HttpParseState::expectingHeaderValue: if (c == ' ' || c == '\t') { break; // ignore spaces between header key and value } parseState = HttpParseState::doingHeaderValue; // no break case HttpParseState::doingHeaderValue: if (c == '\n') { parseState = HttpParseState::doingHeaderContinuation; } else if (c != '\r') { clientMessage[clientPointer++] = c; } break; case HttpParseState::doingHeaderContinuation: switch(c) { case ' ': case '\t': // It's a continuation of the previous value clientMessage[clientPointer++] = c; parseState = HttpParseState::doingHeaderValue; break; case '\n': // It's the blank line clientMessage[clientPointer] = 0; ProcessMessage(); return true; 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; parseState = HttpParseState::doingHeaderKey; } else { RejectMessage(overflowResponse); return true; } break; } break; default: break; } if (clientPointer == ARRAY_SIZE(clientMessage)) { RejectMessage(overflowResponse); return true; } return false; } // 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. // Return true if we generated a json response to send, false if we didn't and changed the state instead. // This may also return true with response == nullptr if we tried to generate a response but ran out of buffers. bool HttpResponder::GetJsonResponse(const char* request, OutputBuffer *&response, bool& keepOpen) noexcept { keepOpen = false; // assume we don't want to persist the connection const char *parameter; if (StringEqualsIgnoreCase(request, "connect") && (parameter = GetKeyValue("password")) != nullptr) { if (!CheckAuthenticated()) { if (!reprap.CheckPassword(parameter)) { // Wrong password response->copy("{\"err\":1}"); reprap.GetPlatform().MessageF(LogWarn, "HTTP client %s attempted login with incorrect password\n", IP4String(GetRemoteIP()).c_str()); return true; } if (!Authenticate()) { // No more HTTP sessions available response->copy("{\"err\":2}"); reprap.GetPlatform().MessageF(LogWarn, "HTTP client %s attempted login but no more sessions available\n", IP4String(GetRemoteIP()).c_str()); return true; } } // Client has been logged in response->printf("{\"err\":0,\"sessionTimeout\":%" PRIu32 ",\"boardType\":\"%s\",\"apiLevel\":%u}", HttpSessionTimeout, GetPlatform().GetBoardString(), ApiLevel); reprap.GetPlatform().MessageF(LogWarn, "HTTP client %s login succeeded\n", IP4String(GetRemoteIP()).c_str()); // See if we can update the current RTC date and time const char* const timeString = GetKeyValue("time"); if (timeString != nullptr && !GetPlatform().IsDateTimeSet()) { struct tm timeInfo; memset(&timeInfo, 0, sizeof(timeInfo)); if (SafeStrptime(timeString, "%Y-%m-%dT%H:%M:%S", &timeInfo) != nullptr) { GetPlatform().SetDateTime(mktime(&timeInfo)); } } } else if (!CheckAuthenticated()) { RejectMessage("Not authorized", 401); return false; } else if (StringEqualsIgnoreCase(request, "disconnect")) { response->printf("{\"err\":%d}", (RemoveAuthentication()) ? 0 : 1); reprap.GetPlatform().MessageF(LogWarn, "HTTP client %s disconnected\n", IP4String(GetRemoteIP()).c_str()); } else if (StringEqualsIgnoreCase(request, "status")) { const char *typeString = GetKeyValue("type"); if (typeString != nullptr) { // New-style JSON status responses int32_t type = StrToI32(typeString); if (type < 1 || type > 3) { type = 1; } OutputBuffer::ReleaseAll(response); response = reprap.GetStatusResponse(type, ResponseSource::HTTP); // this may return nullptr } else { // Deprecated OutputBuffer::ReleaseAll(response); response = reprap.GetLegacyStatusResponse(1, 0); } } else if (StringEqualsIgnoreCase(request, "gcode")) { const char *command = GetKeyValue("gcode"); NetworkGCodeInput * const httpInput = reprap.GetGCodes().GetHTTPInput(); // If the command is empty, just report the buffer space. This allows rr_gcode to be used to poll the buffer space without using it up. if (command != nullptr && command[0] != 0) { httpInput->Put(HttpMessage, command); } response->printf("{\"buff\":%u}", httpInput->BufferSpaceLeft()); } #if HAS_MASS_STORAGE else if (StringEqualsIgnoreCase(request, "upload")) { response->printf("{\"err\":%d}", (uploadError) ? 1 : 0); } else if (StringEqualsIgnoreCase(request, "delete") && (parameter = GetKeyValue("name")) != nullptr) { const bool ok = MassStorage::Delete(parameter, false); response->printf("{\"err\":%d}", (ok) ? 0 : 1); } else if (StringEqualsIgnoreCase(request, "filelist") && (parameter = GetKeyValue("dir")) != nullptr) { OutputBuffer::ReleaseAll(response); const char* const firstVal = GetKeyValue("first"); const unsigned int startAt = (firstVal == nullptr) ? 0 : StrToU32(firstVal); response = reprap.GetFilelistResponse(parameter, startAt); // this may return nullptr } else if (StringEqualsIgnoreCase(request, "files")) { OutputBuffer::ReleaseAll(response); const char* dir = GetKeyValue("dir"); if (dir == nullptr) { dir = GetPlatform().GetGCodeDir(); } const char* const firstVal = GetKeyValue("first"); const unsigned int startAt = (firstVal == nullptr) ? 0 : StrToU32(firstVal); const char* const flagDirsVal = GetKeyValue("flagDirs"); const bool flagDirs = flagDirsVal != nullptr && StrToU32(flagDirsVal) == 1; response = reprap.GetFilesResponse(dir, startAt, flagDirs); // this may return nullptr } else if (StringEqualsIgnoreCase(request, "move")) { const char* const oldVal = GetKeyValue("old"); const char* const newVal = GetKeyValue("new"); bool success = false; if (oldVal != nullptr && newVal != nullptr) { if (StringEqualsIgnoreCase(GetKeyValue("deleteexisting"), "yes") && MassStorage::FileExists(oldVal) && MassStorage::FileExists(newVal)) { MassStorage::Delete(newVal, false); } success = MassStorage::Rename(oldVal, newVal, false); } response->printf("{\"err\":%d}", (success) ? 0 : 1); } else if (StringEqualsIgnoreCase(request, "mkdir")) { const char* const dirVal = GetKeyValue("dir"); bool success = false; if (dirVal != nullptr) { success = MassStorage::MakeDirectory(dirVal, false); } response->printf("{\"err\":%d}", (success) ? 0 : 1); } #else else if ( StringEqualsIgnoreCase(request, "upload") || StringEqualsIgnoreCase(request, "delete") || StringEqualsIgnoreCase(request, "filelist") || StringEqualsIgnoreCase(request, "files") || StringEqualsIgnoreCase(request, "move") || StringEqualsIgnoreCase(request, "mkdir") ) { response->copy("{err:1}"); } #endif else if (StringEqualsIgnoreCase(request, "fileinfo")) { const char* const nameVal = GetKeyValue("name"); if (nameVal != nullptr) { // Regular rr_fileinfo?name=xxx call filenameBeingProcessed.copy(nameVal); } else { // Simple rr_fileinfo call to get info about the file being printed filenameBeingProcessed.Clear(); } responderState = ResponderState::gettingFileInfo; return false; } #if SUPPORT_OBJECT_MODEL else if (StringEqualsIgnoreCase(request, "model")) { OutputBuffer::ReleaseAll(response); const char *const filterVal = GetKeyValue("key"); const char *const flagsVal = GetKeyValue("flags"); response = reprap.GetModelResponse(filterVal, flagsVal); } #endif else if (StringEqualsIgnoreCase(request, "config")) { OutputBuffer::ReleaseAll(response); response = reprap.GetConfigResponse(); } else { RejectMessage("Unknown request", 500); return false; } return true; } const char* HttpResponder::GetKeyValue(const char *key) const noexcept { for (size_t i = 0; i < numQualKeys; ++i) { if (StringEqualsIgnoreCase(qualifiers[i].key, key)) { return qualifiers[i].value; } } return nullptr; } // Called to process a FileInfo request, which may take several calls // Return true if complete bool HttpResponder::SendFileInfo(bool quitEarly) noexcept { OutputBuffer *jsonResponse = nullptr; bool gotFileInfo = (reprap.GetFileInfoResponse(filenameBeingProcessed.c_str(), jsonResponse, quitEarly) != GCodeResult::notFinished); if (gotFileInfo) { // Got it - send the response now outBuf->copy( "HTTP/1.1 200 OK\r\n" "Cache-Control: no-cache, no-store, must-revalidate\r\n" "Pragma: no-cache\r\n" "Expires: 0\r\n" "Content-Type: application/json\r\n" ); outBuf->catf("Content-Length: %u\r\n", (jsonResponse != nullptr) ? jsonResponse->Length() : 0); AddCorsHeader(); outBuf->cat("Connection: close\r\n\r\n"); outBuf->Append(jsonResponse); if (outBuf->HadOverflow()) { OutputBuffer::ReleaseAll(outBuf); ReportOutputBufferExhaustion(__FILE__, __LINE__); gotFileInfo = false; } else { filenameBeingProcessed.Clear(); Commit(); } } return gotFileInfo; } // Authenticate current IP and return true on success bool HttpResponder::Authenticate() noexcept { if (CheckAuthenticated()) { return true; } if (numSessions < MaxHttpSessions) { sessions[numSessions].ip = GetRemoteIP(); sessions[numSessions].lastQueryTime = millis(); sessions[numSessions].isPostUploading = false; numSessions++; return true; } return false; } // Check and update the authentication bool HttpResponder::CheckAuthenticated() noexcept { const IPAddress remoteIP = GetRemoteIP(); for (size_t i = 0; i < numSessions; i++) { if (sessions[i].ip == remoteIP) { sessions[i].lastQueryTime = millis(); return true; } } return false; } bool HttpResponder::RemoveAuthentication() noexcept { const IPAddress remoteIP = skt->GetRemoteIP(); for (size_t i = numSessions; 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; } RemoveSession(i); return true; } } return false; } /*static*/ void HttpResponder::RemoveSession(size_t sessionToRemove) noexcept { if (sessionToRemove < numSessions) { --numSessions; for (size_t k = sessionToRemove; k < numSessions; ++k) { sessions[k] = sessions[k + 1]; } } } void HttpResponder::SendFile(const char* nameOfFileToSend, bool isWebFile) noexcept { #if HAS_MASS_STORAGE FileStore *fileToSend = nullptr; bool zip = false; if (isWebFile) { if (nameOfFileToSend[0] == '/') { ++nameOfFileToSend; // all web files are relative to the /www folder, so remove the leading '/' } // If we are asked to return the root, return the index file if (nameOfFileToSend[0] == 0) { nameOfFileToSend = INDEX_PAGE_FILE; } if (strlen(nameOfFileToSend) > MaxExpectedWebDirFilenameLength) { // We have been asked for a file with a very long name. Don't try to open it, because that may lead to MassStorage::CombineName generating an error message. // Instead, report a possible virus attack from the sending IP address. // Exception: it if is an OCSP request, just return 404. if (!StringStartsWith(nameOfFileToSend, "/ocsp")) { GetPlatform().MessageF(WarningMessage, "IP %s requested file '%.20s...' from HTTP server, possibly a virus attack\n", IP4String(GetRemoteIP()).c_str(), nameOfFileToSend); } } else { for (;;) { // Try to open a gzipped version of the file first if (!StringEndsWithIgnoreCase(nameOfFileToSend, ".gz") && strlen(nameOfFileToSend) + 3 <= MaxFilenameLength) { String