From 542e496d3730c3adddd77fafbf67fd97a25e2a39 Mon Sep 17 00:00:00 2001 From: "Hansi, dl9rdz" Date: Sun, 22 Dec 2024 13:51:28 +0100 Subject: [PATCH] local http updates for development --- RX_FSK/RX_FSK.ino | 224 +++++++++++++++++++++++++++------------- RX_FSK/data/upd.html | 28 ++++- RX_FSK/version.h | 2 +- scripts/updateserver.py | 88 ++++++++++++++++ 4 files changed, 266 insertions(+), 76 deletions(-) create mode 100755 scripts/updateserver.py diff --git a/RX_FSK/RX_FSK.ino b/RX_FSK/RX_FSK.ino index 7a9b907..e6c4303 100644 --- a/RX_FSK/RX_FSK.ino +++ b/RX_FSK/RX_FSK.ino @@ -97,7 +97,8 @@ int updatePort = 80; const char *updatePrefixM = "/main/"; const char *updatePrefixD = "/dev2/"; const char *updatePrefix = updatePrefixM; - +const char *updateFs = "update.fs.bin"; +const char *updateIno = "update.ino.bin"; #define LOCALUDPPORT 9002 //Get real UTC time from NTP server @@ -127,7 +128,11 @@ const char *sondeTypeStrSH[NSondeTypes] = { "DFM", "RS41", "RS92", "Mxx"/*never WiFiServer rdzserver(14570); WiFiClient rdzclient; - +// If a file "localupd.txt" exists, firmware can be updated from a custom IP address read from this file, stored in localUpdates. +// By default (localUpdates==NULL) this is disabled to prevent abuse +// Note: by enabling this, someone with access to the web interface can replace the firmware arbitrarily! +// Make sure that only trustworthy persons have access to the web interface... +char *localUpdates = NULL; boolean forceReloadScreenConfig = false; @@ -156,6 +161,14 @@ void enterMode(int mode); void WiFiEvent(WiFiEvent_t event); +// Possibly we will need more fine grained permissions in the future... +// For now, disallow arbitrary firmware updates on standard installations +// development installations can add a file "localupd.txt" which enables updates from arbitrary locations +int checkAllowed(const char *filename) { + if(!localUpdates && (strstr(filename, "localupd.txt") != NULL)) return 0; + return 1; +} + // Read line from file, independent of line termination (LF or CR LF) String readLine(Stream &stream) { @@ -212,7 +225,7 @@ String processor(const String& var) { } if (var == "FULLNAMEID") { char tmp[128]; - snprintf(tmp, 128, "%s-%c%d", version_id, SPIFFS_MAJOR + 'A' - 1, SPIFFS_MINOR); + snprintf(tmp, 128, "%s-%c%d", version_id, FS_MAJOR + 'A' - 1, FS_MINOR); return String(tmp); } if (var == "AUTODETECT_INFO") { @@ -235,6 +248,10 @@ String processor(const String& var) { return String("Not supported"); #endif } + if (var == "LOCAL_UPDATES") { + if(localUpdates) return String(localUpdates); + else return String(); + } return String(); } @@ -949,10 +966,15 @@ const char *handleControlPost(AsyncWebServerRequest * request) { void handleUpload(AsyncWebServerRequest * request, String filename, size_t index, uint8_t *data, size_t len, bool final) { static File file; if (!index) { + const char *fn = filename.c_str(); + if(!checkAllowed(fn)) { + LOG_E(TAG, "UploadStart: writing %s prohibited\n", fn); + return; + } LOG_D(TAG, "UploadStart: %s\n", filename.c_str()); file = LittleFS.open("/" + filename, "w"); if (!file) { - Serial.println("There was an error opening the file '/config.txt' for reading"); + LOG_E(TAG, "Error opening the file '/%s' for writing", fn); } } if (!file) return; @@ -1066,8 +1088,15 @@ const char *handleEditPost(AsyncWebServerRequest * request) { AsyncWebParameter *filep = request->getParam("file"); if (!filep) return NULL; + String filename = filep->value(); - LOG_D(TAG, "Writing file <%s>\n", filename.c_str()); + const char *fn = filename.c_str(); + if(!checkAllowed(fn)) { + LOG_E(TAG, "handleEditPost: writing %s prohibited\n", fn); + return NULL; + } + + LOG_D(TAG, "Writing file <%s>\n", fn); AsyncWebParameter *textp = request->getParam("text", true); if (!textp) return NULL; LOG_D(TAG, "Parameter size is %d\n", textp->size()); @@ -1101,7 +1130,7 @@ const char *createUpdateForm(boolean run) { if (run) { strcat(ptr, "

Doing update, wait until reboot

"); } else { - sprintf(ptr + strlen(ptr), "

Currently installed: %s-%c%d

\n", version_id, SPIFFS_MAJOR + 'A' - 1, SPIFFS_MINOR); + sprintf(ptr + strlen(ptr), "

Currently installed: %s-%c%d

\n", version_id, FS_MAJOR + 'A' - 1, FS_MINOR); strcat(ptr, "

Available main:
" "Available devel:

"); strcat(ptr, "
"); @@ -1115,6 +1144,7 @@ const char *createUpdateForm(boolean run) { const char *handleUpdatePost(AsyncWebServerRequest * request) { Serial.println("Handling post request"); int params = request->params(); + bool updateFromURL = false; for (int i = 0; i < params; i++) { String param = request->getParam(i)->name(); Serial.println(param.c_str()); @@ -1126,8 +1156,40 @@ const char *handleUpdatePost(AsyncWebServerRequest * request) { Serial.println("equals main"); updatePrefix = updatePrefixM; } + else if (localUpdates && param.equals("local")) { + // Local updates permitted. Expect URL as url parameter... + updateFromURL = true; + } + else if (updateFromURL && param.equals("url")) { + // Note: strdup allocates memory that is never free'd. + // Let's not care about this as we are going to reboot after the update anyway... + String localsrc = request->getParam(i)->value(); + int pos; + + if (-1 != (pos = localsrc.indexOf("://")) ){ + // strip off http:// + localsrc = localsrc.substring(pos+3); + } + + if (-1 != (pos = localsrc.indexOf("/")) ){ + // see if there's a directory or updates are in the root + String tmp = localsrc.substring(pos); + updatePrefix = strdup(tmp.c_str()); + localsrc = localsrc.substring(0, pos); + } else { + updatePrefix = strdup("/"); + } + + if (-1 != (pos = localsrc.indexOf(":"))) { + // extract port + updatePort = atoi(localsrc.substring(pos+1).c_str()); + updateHost = strdup(localsrc.substring(0, pos).c_str()); + } else { + updateHost = strdup(localsrc.c_str()); + } + } } - LOG_I(TAG, "Updating: %supdate.ino.bin\n", updatePrefix); + LOG_I(TAG, "Updating: %s%s from %s:%d\n", updatePrefix, updateIno, updateHost, updatePort); enterMode(ST_UPDATE); return ""; } @@ -1200,23 +1262,21 @@ const char *sendGPX(AsyncWebServerRequest * request) { return message; } -#define SPIFFS LittleFS - const char* PARAM_MESSAGE = "message"; void SetupAsyncServer() { Serial.println("SetupAsyncServer()\n"); server.reset(); // Route for root / web page server.on("/", HTTP_GET, [](AsyncWebServerRequest * request) { - request->send(SPIFFS, "/index.html", String(), false, processor); + request->send(LittleFS, "/index.html", String(), false, processor); }); server.on("/index.html", HTTP_GET, [](AsyncWebServerRequest * request) { - request->send(SPIFFS, "/index.html", String(), false, processor); + request->send(LittleFS, "/index.html", String(), false, processor); }); server.on("/test.html", HTTP_GET, [](AsyncWebServerRequest * request) { - request->send(SPIFFS, "/test.html", String(), false, processor); + request->send(LittleFS, "/test.html", String(), false, processor); }); server.on("/qrg.html", HTTP_GET, [](AsyncWebServerRequest * request) { @@ -1259,10 +1319,10 @@ void SetupAsyncServer() { request->send(200, "text/json", createLiveJson()); }); server.on("/livemap.html", HTTP_GET, [](AsyncWebServerRequest * request) { - request->send(SPIFFS, "/livemap.html", String(), false, processor); + request->send(LittleFS, "/livemap.html", String(), false, processor); }); server.on("/livemap.js", HTTP_GET, [](AsyncWebServerRequest * request) { - request->send(SPIFFS, "/livemap.js", String(), false, processor); + request->send(LittleFS, "/livemap.js", String(), false, processor); }); server.on("/update.html", HTTP_GET, [](AsyncWebServerRequest * request) { request->send(200, "text/html", createUpdateForm(0)); @@ -1287,7 +1347,7 @@ void SetupAsyncServer() { request->send(400, "error"); return; } - request->send(SPIFFS, filename, "text/plain"); + request->send(LittleFS, filename, "text/plain"); }); server.on("/file", HTTP_POST, [](AsyncWebServerRequest * request) { @@ -1352,7 +1412,7 @@ void SetupAsyncServer() { return; } const String filename = param->value(); - File file = SPIFFS.open("/" + filename, "r"); + File file = LittleFS.open("/" + filename, "r"); int state = 0; request->send("text/html", 0, [state, file, filename](uint8_t *buffer, size_t maxLen, size_t index) mutable -> size_t { LOG_D(TAG, "******* send callback: %d %d %d\n", state, maxLen, index); @@ -1380,7 +1440,7 @@ void SetupAsyncServer() { // Route to load style.css file server.on("/style.css", HTTP_GET, [](AsyncWebServerRequest * request) { - AsyncWebServerResponse *response = request->beginResponse(SPIFFS, "/style.css", "text/css"); + AsyncWebServerResponse *response = request->beginResponse(LittleFS, "/style.css", "text/css"); if(response) { response->addHeader("Cache-Control", "max-age=86400"); request->send(response); @@ -1398,7 +1458,7 @@ void SetupAsyncServer() { }); server.on("/upd.html", HTTP_GET, [](AsyncWebServerRequest * request) { - request->send(SPIFFS, "/upd.html", String(), false, processor); + request->send(LittleFS, "/upd.html", String(), false, processor); }); server.on("/status.json", HTTP_GET, [](AsyncWebServerRequest * request) { @@ -1432,7 +1492,7 @@ void SetupAsyncServer() { const char *type = "text/html"; if(url.endsWith(".js")) type="text/javascript"; LOG_D(TAG, "Responding with type %s (url %s)\n", type, url.c_str()); - AsyncWebServerResponse *response = request->beginResponse(SPIFFS, url, type); + AsyncWebServerResponse *response = request->beginResponse(LittleFS, url, type); if(response) { response->addHeader("Cache-Control", "max-age=900"); request->send(response); @@ -1838,6 +1898,14 @@ void heap_caps_alloc_failed_hook(size_t requested_size, uint32_t caps, const cha } #endif +static void enableLocalUpdates() { + localUpdates = NULL; + File local = LittleFS.open("/localupd.txt", "r"); + if(local && local.available()) { + localUpdates = strdup(readLine(local).c_str()); + } + LOG_I(TAG, "Local update server for development: %s\n", localUpdates?localUpdates:""); +} void setup() { @@ -1868,10 +1936,10 @@ void setup() sonde.defaultConfig(); // including autoconfiguration delay(1000); - Serial.println("Initializing SPIFFS"); - // Initialize SPIFFS - if (!SPIFFS.begin(true)) { - Serial.println("An Error has occurred while mounting SPIFFS"); + Serial.println("Initializing LittleFS"); + // Initialize LittleFS + if (!LittleFS.begin(true)) { + Serial.println("An Error has occurred while mounting LittleFS"); return; } @@ -2052,6 +2120,8 @@ void setup() connSDCard.init(); #endif + enableLocalUpdates(); // check if local updates from other servers is allowed + WiFi.onEvent(WiFiEvent); getKeyPress(); // clear key buffer @@ -2816,6 +2886,8 @@ void execOTA() { bool isValidContentType = false; sonde.clearDisplay(); uint8_t dispxs, dispys; + Serial.printf("Updater connecting to: %s:%d%s\n", updateHost, updatePort, updatePrefix); + if ( ISOLED(sonde.config) ) { disp.rdis->setFont(FONT_SMALL); dispxs = dispys = 1; @@ -2830,78 +2902,80 @@ void execOTA() { disp.rdis->drawString(0, 0, updateHost); } - Serial.print("Connecting to: "); Serial.println(updateHost); // Connect to Update host if (!client.connect(updateHost, updatePort)) { - Serial.println("Connection to " + String(updateHost) + " failed. Please check your setup"); - return; + LOG_E(TAG, "Connection to %s:%d for fs update failed\n", updateHost, updatePort); + enterMode(ST_DECODER); } - // First, update file system - Serial.println("Fetching fs update"); + // First, try update file system + LOG_I(TAG, "Fetching fs update from '%s:%d' '%s' '%s'\n", updateHost, updatePort, updatePrefix, updateFs); disp.rdis->drawString(0, 1 * dispys, "Fetching fs..."); - client.printf("GET %supdate.fs.bin HTTP/1.1\r\n" - "Host: %s\r\n" + client.printf("GET %s%s HTTP/1.1\r\n" + "Host: %s:%d\r\n" "Cache-Control: no-cache\r\n" - "Connection: close\r\n\r\n", updatePrefix, updateHost); + "Connection: close\r\n\r\n", updatePrefix, updateFs, updateHost, updatePort); // see if we get some data.... int type = 0; int res = fetchHTTPheader(&type); if (res < 0) { - return; - } - // process data... - while (client.available()) { - // get header... - char fn[128]; - fn[0] = '/'; - client.readBytesUntil('\n', fn + 1, 128); - char *sz = strchr(fn, ' '); - if (!sz) { - client.stop(); - return; - } - *sz = 0; - int len = atoi(sz + 1); - LOG_I(TAG, "Updating file %s (%d bytes)\n", fn, len); - char fnstr[17]; - memset(fnstr, ' ', 16); - strncpy(fnstr, fn, 16); - fnstr[16] = 0; - disp.rdis->drawString(0, 2 * dispys, fnstr); - File f = SPIFFS.open(fn, FILE_WRITE); - // read sz bytes........ - while (len > 0) { - unsigned char buf[1024]; - int r = client.read(buf, len > 1024 ? 1024 : len); - if (r == -1) { + ; // no-op + } else { + // process data... + while (client.available()) { + // get header... + char fn[128]; + fn[0] = '/'; + client.readBytesUntil('\n', fn + 1, 128); + char *sz = strchr(fn, ' '); + if (!sz) { client.stop(); + enterMode(ST_DECODER); return; } - f.write(buf, r); - len -= r; + *sz = 0; + int len = atoi(sz + 1); + LOG_I(TAG, "Updating file %s (%d bytes)\n", fn, len); + char fnstr[17]; + memset(fnstr, ' ', 16); + strncpy(fnstr, fn, 16); + fnstr[16] = 0; + disp.rdis->drawString(0, 2 * dispys, fnstr); + File f = LittleFS.open(fn, FILE_WRITE); + // read sz bytes........ + while (len > 0) { + unsigned char buf[1024]; + int r = client.read(buf, len > 1024 ? 1024 : len); + if (r == -1) { + client.stop(); + enterMode(ST_DECODER); + return; + } + f.write(buf, r); + len -= r; + } } + client.stop(); } - client.stop(); - Serial.print("Connecting to: "); Serial.println(updateHost); // Connect to Update host if (!client.connect(updateHost, updatePort)) { - Serial.println("Connection to " + String(updateHost) + " failed. Please check your setup"); + LOG_E(TAG, "Connection to %s:%d for app update failed\n", updateHost, updatePort); + enterMode(ST_DECODER); return; } // Connection succeeded, fecthing the bin - LOG_I(TAG, "Fetching bin: %supdate.ino.bin\n", updatePrefix); + LOG_I(TAG, "Fetching bin update from '%s:%d' '%s' '%s'", updateHost, updatePort, updatePrefix, updateIno); disp.rdis->drawString(0, 3 * dispys, "Fetching update"); // Get the contents of the bin file - client.printf("GET %supdate.ino.bin HTTP/1.1\r\n" - "Host: %s\r\n" + client.printf("GET %s%s HTTP/1.1\r\n" + "Host: %s:%d\r\n" "Cache-Control: no-cache\r\n" "Connection: close\r\n\r\n", - updatePrefix, updateHost); + updatePrefix, updateIno, updateHost, updatePort); // Check what is being sent // Serial.print(String("GET ") + bin + " HTTP/1.1\r\n" + @@ -2914,7 +2988,7 @@ void execOTA() { if (validType == 1) isValidContentType = true; // Check what is the contentLength and if content type is `application/octet-stream` - Serial.println("contentLength : " + String(contentLength) + ", isValidContentType : " + String(isValidContentType)); + Serial.printf("contentLength : %d, isValidContentType : %d\n",contentLength, isValidContentType); disp.rdis->drawString(0, 4 * dispys, "Len: "); String cls = String(contentLength); disp.rdis->drawString(5 * dispxs, 4 * dispys, cls.c_str()); @@ -3023,16 +3097,18 @@ int fetchHTTPheader(int *validType) { // extract headers here // Start with content length - if (line.startsWith("Content-Length: ")) { - contentLength = atoi((getHeaderValue(line, "Content-Length: ")).c_str()); + static const char *HEADER_CL = "Content-Length: "; + if (strncasecmp( line.c_str(), HEADER_CL, strlen(HEADER_CL) ) == 0 ) { + contentLength = atoi( line.c_str() + strlen(HEADER_CL) ); Serial.println("Got " + String(contentLength) + " bytes from server"); } // Next, the content type - if (line.startsWith("Content-Type: ")) { - String contentType = getHeaderValue(line, "Content-Type: "); - Serial.println("Got " + contentType + " payload."); - if (contentType == "application/octet-stream") { + static const char *HEADER_CT = "Content-Type: "; + if (strncasecmp( line.c_str(), HEADER_CT, strlen(HEADER_CT) ) == 0 ) { + const char *contentType = line.c_str() + strlen(HEADER_CT); + LOG_I(TAG, "Content type: %s\n", contentType); + if (strcmp(contentType, "application/octet-stream")==0) { if (validType) *validType = 1; } } diff --git a/RX_FSK/data/upd.html b/RX_FSK/data/upd.html index 44def61..3ee17a8 100644 --- a/RX_FSK/data/upd.html +++ b/RX_FSK/data/upd.html @@ -14,9 +14,35 @@ Available dev2: (...checking...)



-

Note: If suffix is the same, update should work fully. If the number is different, update contains changes in the file system. A full re-flash is required to get all new features, but the update should not break anything. If the letter is different, a full re-flash is mandatory, update will not work

+
+ from +
+
+ + +

Note: If suffix is the same, update should work fully. If the number is +different, update contains changes in the file system. A full re-flash is +required to get all new features, but the update should not break anything. +If the letter is different, a full re-flash is mandatory, update will not +work.

+

+The local-update feature is for developers and requires a path containing +update.fs.bin and update.ino.bin. +