diff --git a/lib/buttons/GwButtons.cpp b/lib/buttons/GwButtons.cpp index cf64329..c169e48 100644 --- a/lib/buttons/GwButtons.cpp +++ b/lib/buttons/GwButtons.cpp @@ -14,7 +14,7 @@ class FactoryResetRequest: public GwMessage{ protected: virtual void processImpl(){ api->getLogger()->logDebug(GwLog::LOG,"reset request processing"); - api->getConfig()->reset(true); + api->getConfig()->reset(); xTaskCreate([](void *p){ delay(500); ESP.restart(); diff --git a/lib/config/GWConfig.cpp b/lib/config/GWConfig.cpp index f7316df..cdbfdbe 100644 --- a/lib/config/GWConfig.cpp +++ b/lib/config/GWConfig.cpp @@ -1,6 +1,7 @@ #include "GWConfig.h" #include #include +#include #define B(v) (v?"true":"false") @@ -53,6 +54,7 @@ GwConfigInterface * GwConfigHandler::getConfigItem(const String name, bool dummy #define PREF_NAME "gwprefs" GwConfigHandler::GwConfigHandler(GwLog *logger): GwConfigDefinitions(){ this->logger=logger; + saltBase=esp_random(); } bool GwConfigHandler::loadConfig(){ prefs.begin(PREF_NAME,true); @@ -63,18 +65,6 @@ bool GwConfigHandler::loadConfig(){ prefs.end(); return true; } -bool GwConfigHandler::saveConfig(){ - prefs.begin(PREF_NAME,false); - for (int i=0;ihasChangedValue){ - LOG_DEBUG(GwLog::LOG,"saving %s=%s",configs[i]->getName().c_str(),configs[i]->changedValue.c_str()); - prefs.putString(configs[i]->getName().c_str(),configs[i]->changedValue); - } - } - prefs.end(); - LOG_DEBUG(GwLog::LOG,"saved config"); - return true; -} bool GwConfigHandler::updateValue(String name, String value){ GwConfigInterface *i=getConfigItem(name); @@ -83,18 +73,24 @@ bool GwConfigHandler::updateValue(String name, String value){ LOG_DEBUG(GwLog::LOG,"skip empty password %s",name.c_str()); } else{ + if (i->asString() == value){ + return false; + } LOG_DEBUG(GwLog::LOG,"update config %s=>%s",name.c_str(),i->isSecret()?"***":value.c_str()); - i->updateValue(value); + prefs.begin(PREF_NAME,false); + prefs.putString(i->getName().c_str(),value); + prefs.end(); } return true; } -bool GwConfigHandler::reset(bool save){ +bool GwConfigHandler::reset(){ LOG_DEBUG(GwLog::LOG,"reset config"); + prefs.begin(PREF_NAME,false); for (int i=0;iupdateValue(configs[i]->getDefault()); + prefs.putString(configs[i]->getName().c_str(),configs[i]->getDefault()); } - if (!save) return true; - return saveConfig(); + prefs.end(); + return true; } String GwConfigHandler::getString(const String name, String defaultv) const{ GwConfigInterface *i=getConfigItem(name,false); @@ -122,6 +118,47 @@ bool GwConfigHandler::setValue(String name,String value){ return true; } +bool GwConfigHandler::checkPass(String hash){ + if (! getBool(useAdminPass)) return true; + String pass=getString(adminPassword); + unsigned long now=millis()/1000UL & ~0x7UL; + MD5Builder builder; + char buffer[2*sizeof(now)+1]; + for (int i=0;i< 5 ;i++){ + unsigned long base=saltBase+now; + toHex(base,buffer,2*sizeof(now)+1); + builder.begin(); + builder.add(buffer); + builder.add(pass); + builder.calculate(); + String md5=builder.toString(); + bool rt=hash == md5; + logger->logDebug(GwLog::DEBUG,"checking pass %s, base=%ld, hash=%s, res=%d", + hash.c_str(),base,md5.c_str(),(int)rt); + if (rt) return true; + now -= 8; + } + return false; +} +static char hv(uint8_t nibble){ + nibble=nibble&0xf; + if (nibble < 10) return (char)('0'+nibble); + return (char)('A'+nibble-10); +} +void GwConfigHandler::toHex(unsigned long v, char *buffer, size_t bsize) +{ + uint8_t *bp = (uint8_t *)&v; + size_t i = 0; + for (; i < sizeof(v) && (2 * i + 1) < bsize; i++) + { + buffer[2 * i] = hv((*bp) >> 4); + buffer[2 * i + 1] = hv(*bp); + bp++; + } + if ((2 * i) < bsize) + buffer[2 * i] = 0; +} + void GwNmeaFilter::handleToken(String token, int index){ switch(index){ case 0: diff --git a/lib/config/GWConfig.h b/lib/config/GWConfig.h index 1dcb28c..4d0b105 100644 --- a/lib/config/GWConfig.h +++ b/lib/config/GWConfig.h @@ -18,22 +18,25 @@ class GwConfigHandler: public GwConfigDefinitions{ public: GwConfigHandler(GwLog *logger); bool loadConfig(); - bool saveConfig(); void stopChanges(); bool updateValue(String name, String value); - bool reset(bool save); + bool reset(); String toString() const; String toJson() const; String getString(const String name,const String defaultv="") const; bool getBool(const String name,bool defaultv=false) const ; int getInt(const String name,int defaultv=0) const; GwConfigInterface * getConfigItem(const String name, bool dummy=false) const; + bool checkPass(String hash); /** * change the value of a config item * will become a noop after stopChanges has been called * !use with care! no checks of the value */ bool setValue(String name, String value); + static void toHex(unsigned long v,char *buffer,size_t bsize); + unsigned long getSaltBase(){return saltBase;} private: + unsigned long saltBase=0; }; #endif \ No newline at end of file diff --git a/lib/webserver/GwWebServer.cpp b/lib/webserver/GwWebServer.cpp index eb4dcef..2b3a79a 100644 --- a/lib/webserver/GwWebServer.cpp +++ b/lib/webserver/GwWebServer.cpp @@ -118,4 +118,12 @@ bool GwWebServer::registerMainHandler(const char *url,RequestCreator creator){ return true; } +bool GwWebServer::registerPostHandler(const char *url, ArRequestHandlerFunction requestHandler, + ArBodyHandlerFunction bodyHandler){ + server->on(url,HTTP_POST,requestHandler, + [](AsyncWebServerRequest *request, const String& filename, size_t index, uint8_t *data, size_t len, bool final){}, + bodyHandler); + return true; +} + diff --git a/lib/webserver/GwWebServer.h b/lib/webserver/GwWebServer.h index 35b5cba..795988e 100644 --- a/lib/webserver/GwWebServer.h +++ b/lib/webserver/GwWebServer.h @@ -1,6 +1,7 @@ #ifndef _GWWEBSERVER_H #define _GWWEBSERVER_H #include +#include #include "GwMessage.h" #include "GwLog.h" class GwWebServer{ @@ -14,6 +15,7 @@ class GwWebServer{ ~GwWebServer(); void begin(); bool registerMainHandler(const char *url,RequestCreator creator); + bool registerPostHandler(const char *url, ArRequestHandlerFunction requestHandler, ArBodyHandlerFunction bodyHandler); void handleAsyncWebRequest(AsyncWebServerRequest *request, GwRequestMessage *msg); AsyncWebServer * getServer(){return server;} }; diff --git a/src/main.cpp b/src/main.cpp index becdce6..0f586aa 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -141,47 +141,12 @@ GwWebServer webserver(&logger,&mainQueue,80); GwCounter countNMEA2KIn("count2Kin"); GwCounter countNMEA2KOut("count2Kout"); -unsigned long saltBase=esp_random(); - -char hv(uint8_t nibble){ - nibble=nibble&0xf; - if (nibble < 10) return (char)('0'+nibble); - return (char)('A'+nibble-10); -} -void toHex(unsigned long v,char *buffer,size_t bsize){ - uint8_t *bp=(uint8_t *)&v; - size_t i=0; - for (;i> 4); - buffer[2*i+1]=hv(*bp); - bp++; - } - if ((2*i) < bsize) buffer[2*i]=0; -} bool checkPass(String hash){ - if (! config.getBool(config.useAdminPass)) return true; - String pass=config.getString(config.adminPassword); - unsigned long now=millis()/1000UL & ~0x7UL; - MD5Builder builder; - char buffer[2*sizeof(now)+1]; - for (int i=0;i< 5 ;i++){ - unsigned long base=saltBase+now; - toHex(base,buffer,2*sizeof(now)+1); - builder.begin(); - builder.add(buffer); - builder.add(pass); - builder.calculate(); - String md5=builder.toString(); - bool rt=hash == md5; - logger.logDebug(GwLog::DEBUG,"checking pass %s, base=%ld, hash=%s, res=%d", - hash.c_str(),base,md5.c_str(),(int)rt); - if (rt) return true; - now -= 8; - } - return false; + return config.checkPass(hash); } + GwUpdate updater(&logger,&webserver,&checkPass); GwConfigInterface *systemName=config.getConfigItem(config.systemName,true); @@ -398,9 +363,9 @@ protected: status["clientIP"] = WiFi.localIP().toString(); status["apIp"] = gwWifi.apIP(); size_t bsize=2*sizeof(unsigned long)+1; - unsigned long base=saltBase + ( millis()/1000UL & ~0x7UL); + unsigned long base=config.getSaltBase() + ( millis()/1000UL & ~0x7UL); char buffer[bsize]; - toHex(base,buffer,bsize); + GwConfigHandler::toHex(base,buffer,bsize); status["salt"] = buffer; status["fwtype"]= firmwareType; //nmea0183Converter->toJson(status); @@ -512,7 +477,6 @@ protected: if (!rt) { logger.logDebug(GwLog::ERROR,"ERR: unable to update %s to %s", name.c_str(), value.c_str()); - ok = false; error += name; error += "="; error += value; @@ -524,7 +488,6 @@ protected: { result = JSON_OK; logger.logDebug(GwLog::ERROR,"update config and restart"); - config.saveConfig(); logger.flush(); logger.logDebug(GwLog::DEBUG,"Heap free=%ld, minFree=%ld", (long)xPortGetFreeHeapSize(), @@ -557,7 +520,7 @@ protected: result=JSON_INVALID_PASS; return; } - config.reset(true); + config.reset(); logger.logDebug(GwLog::ERROR,"reset config, restart"); result = JSON_OK; delayedRestart(); @@ -626,6 +589,134 @@ protected: }; +void handleConfigRequestData(AsyncWebServerRequest *request, uint8_t *data, size_t len, size_t index, size_t total){ + typedef struct{ + char notFirst; + char hashChecked; + char parsingValue; + int bName; + char name[16]; + int bValue; + char value[512]; + }RequestNV; + logger.logDebug(GwLog::DEBUG,"handleConfigRequestData len=%d,idx=%d,total=%d",(int)len,(int)index,(int)total); + if (request->_tempObject == NULL){ + logger.logDebug(GwLog::DEBUG,"handleConfigRequestData create receive struct"); + //we cannot use new here as it will be deleted with free + request->_tempObject=malloc(sizeof(RequestNV)); + memset(request->_tempObject,0,sizeof(RequestNV)); + } + RequestNV *nv=(RequestNV*)(request->_tempObject); + if (nv->notFirst && ! nv->hashChecked){ + return; //ignore data + } + int parsed=0; + while (parsed < len) + { + if (!nv->parsingValue) + { + int maxSize = sizeof(RequestNV::name) - 1; + if (nv->bName >= maxSize) + { + nv->name[maxSize] = 0; + logger.logDebug(GwLog::DEBUG, "parse error name too long %s", nv->name); + nv->bName = 0; + } + while (nv->bName < maxSize && parsed < len) + { + bool endName = *data == '='; + nv->name[nv->bName] = endName ? 0 : *data; + nv->bName++; + parsed++; + data++; + if (endName) + { + nv->parsingValue = 1; + break; + } + if (nv->bName >= maxSize) + { + nv->name[maxSize] = 0; + logger.logDebug(GwLog::DEBUG, "parse error name too long %s", nv->name); + nv->bName = 0; + } + } + } + bool valueDone = false; + if (nv->parsingValue) + { + int maxSize = sizeof(RequestNV::value) - 1; + if (nv->bValue >= maxSize) + { + nv->value[maxSize] = 0; + logger.logDebug(GwLog::DEBUG, "parse error value too long %s:%s", nv->name, nv->value); + nv->bValue = 0; + } + while (nv->bValue < maxSize && parsed < len) + { + valueDone = *data == '&'; + nv->value[nv->bValue] = valueDone ? 0 : *data; + nv->bValue++; + parsed++; + data++; + if (nv->bValue >= maxSize) + { + nv->value[maxSize] = 0; + logger.logDebug(GwLog::DEBUG, "parse error value too long %s:%s", nv->name, nv->value); + nv->bValue = 0; + } + } + if (! valueDone){ + if (parsed >= len && (len+index) >= total){ + //request ends here + nv->value[nv->bValue]=0; + valueDone=true; + } + } + if (valueDone){ + String name(nv->name); + String value(nv->value); + if (! nv->notFirst){ + nv->notFirst=1; + //we expect the _hash as first parameter + if (name != String("_hash")){ + logger.logDebug(GwLog::ERROR,"missing first parameter _hash in setConfig"); + request->send(200,"application/json","{\"status\":\"missing _hash\"}"); + return; + } + if (! config.checkPass(request->urlDecode(value))){ + request->send(200,"application/json",JSON_INVALID_PASS); + return; + } + else{ + nv->hashChecked=1; + } + } + else{ + if (nv->hashChecked){ + logger.logDebug(GwLog::DEBUG,"update value ns=%d,n=%d,vs=%d,v=%d",nv->bName,nv->name,nv->bValue,nv->value); + config.updateValue(request->urlDecode(name),request->urlDecode(value)); + } + } + nv->parsingValue=0; + nv->bName=0; + nv->bValue=0; + } + } + } + if (parsed >= len && (len+index)>= total){ + if (nv->notFirst){ + if (nv->hashChecked){ + request->send(200,"application/json",JSON_OK); + delayedRestart(); + } + } + else{ + request->send(200,"application/json","{\"status\":\"missing _hash\"}"); + } + } +} + void setup() { mainLock=xSemaphoreCreateMutex(); @@ -666,12 +757,6 @@ void setup() { { return new StatusRequest(); }); webserver.registerMainHandler("/api/config", [](AsyncWebServerRequest *request)->GwRequestMessage * { return new ConfigRequest(); }); - webserver.registerMainHandler("/api/setConfig", - [](AsyncWebServerRequest *request)->GwRequestMessage * - { - SetConfigRequest *msg = new SetConfigRequest(request); - return msg; - }); webserver.registerMainHandler("/api/resetConfig", [](AsyncWebServerRequest *request)->GwRequestMessage * { return new ResetConfigRequest(request->arg("_hash")); }); webserver.registerMainHandler("/api/boatData", [](AsyncWebServerRequest *request)->GwRequestMessage * @@ -690,7 +775,12 @@ void setup() { { String hash=request->arg("hash"); return new CheckPassRequest(hash); - }); + }); + webserver.registerPostHandler("/api/setConfig", + [](AsyncWebServerRequest *request){ + + }, + handleConfigRequestData); webserver.begin(); xdrMappings.begin(); diff --git a/web/index.js b/web/index.js index 509d7a6..c7f399f 100644 --- a/web/index.js +++ b/web/index.js @@ -236,16 +236,24 @@ function changeConfig() { ensurePass() .then(function (pass) { let newAdminPass; - let url = "/api/setConfig?_hash="+encodeURIComponent(pass)+"&"; + let url = "/api/setConfig" + let body="hash="+encodeURIComponent(pass)+"&"; let allValues=getAllConfigs(); if (!allValues) return; for (let name in allValues){ if (name == 'adminPassword'){ newAdminPass=allValues[name]; } - url += name + "=" + encodeURIComponent(allValues[name]) + "&"; + body += encodeURIComponent(name) + "=" + encodeURIComponent(allValues[name]) + "&"; } - getJson(url) + fetch(url,{ + method:'POST', + headers:{ + 'Content-Type': 'application/octet-stream' //we must lie here + }, + body: body + }) + .then((rs)=>rs.json()) .then(function (status) { if (status.status == 'OK') { if (newAdminPass !== undefined) {