add an admin password to protect the changing api functions

This commit is contained in:
wellenvogel 2021-12-13 21:13:42 +01:00
parent df4b49ad5b
commit 30b23a72ce
5 changed files with 180 additions and 60 deletions

View File

@ -55,6 +55,8 @@ Afterwards use a Bonjour Browser, the address ESP32NMEA2k.local or the ip addres
You will get a small UI to watch the status and make settings. You will get a small UI to watch the status and make settings.
If you want to connect to another wifi network, just enter the credentials in the wifi client tab and enable the wifi client. If you want to connect to another wifi network, just enter the credentials in the wifi client tab and enable the wifi client.
For all the potential inputs and outputs (NMEA2000, USB, TCP, RS485) you can set the configuration including NMEA0183 filtering. For all the potential inputs and outputs (NMEA2000, USB, TCP, RS485) you can set the configuration including NMEA0183 filtering.
To store your changes you will be asked for an admin password. The initial one is esp32admin. You can change this password at the config/system tab (and even completely disable it).
Be careful to notice the password - you can only recover from a lost password with a factory reset of the device (long press the led button until it goes blue->red->green).
On the data page you will have a small dashboard for the currently received data. On the data page you will have a small dashboard for the currently received data.
On the status page you can check the number of messages flowing in and out. On the status page you can check the number of messages flowing in and out.

View File

@ -407,28 +407,36 @@ bool delayedRestart(){
return xTaskCreate([](void *p){ return xTaskCreate([](void *p){
GwLog *logRef=(GwLog *)p; GwLog *logRef=(GwLog *)p;
logRef->logDebug(GwLog::LOG,"delayed reset started"); logRef->logDebug(GwLog::LOG,"delayed reset started");
delay(500); delay(800);
ESP.restart(); ESP.restart();
vTaskDelete(NULL); vTaskDelete(NULL);
},"reset",1000,&logger,0,NULL) == pdPASS; },"reset",2000,&logger,0,NULL) == pdPASS;
} }
ApiImpl *apiImpl=new ApiImpl(MIN_USER_TASK); ApiImpl *apiImpl=new ApiImpl(MIN_USER_TASK);
GwUserCode userCodeHandler(apiImpl,&mainLock); GwUserCode userCodeHandler(apiImpl,&mainLock);
#define JSON_OK "{\"status\":\"OK\"}" #define JSON_OK "{\"status\":\"OK\"}"
#define JSON_INVALID_PASS F("{\"status\":\"invalid password\"}")
//WebServer requests that should //WebServer requests that should
//be processed inside the main loop //be processed inside the main loop
//this prevents us from the need to sync all the accesses //this prevents us from the need to sync all the accesses
class ResetRequest : public GwRequestMessage class ResetRequest : public GwRequestMessage
{ {
String hash;
public: public:
ResetRequest() : GwRequestMessage(F("application/json"),F("reset")){}; ResetRequest(String hash) : GwRequestMessage(F("application/json"),F("reset")){
this->hash=hash;
};
protected: protected:
virtual void processRequest() virtual void processRequest()
{ {
logger.logDebug(GwLog::LOG, "Reset Button"); logger.logDebug(GwLog::LOG, "Reset Button");
if (! checkPass(hash)){
result=JSON_INVALID_PASS;
return;
}
result = JSON_OK; result = JSON_OK;
if (!delayedRestart()){ if (!delayedRestart()){
logger.logDebug(GwLog::ERROR,"cannot initiate restart"); logger.logDebug(GwLog::ERROR,"cannot initiate restart");
@ -552,8 +560,18 @@ protected:
{ {
bool ok = true; bool ok = true;
String error; String error;
String hash;
auto it=args.find("_hash");
if (it != args.end()){
hash=it->second;
}
if (! checkPass(hash)){
result=JSON_INVALID_PASS;
return;
}
for (StringMap::iterator it = args.begin(); it != args.end(); it++) for (StringMap::iterator it = args.begin(); it != args.end(); it++)
{ {
if (it->first.indexOf("_")>= 0) continue;
bool rt = config.updateValue(it->first, it->second); bool rt = config.updateValue(it->first, it->second);
if (!rt) if (!rt)
{ {
@ -582,12 +600,19 @@ protected:
}; };
class ResetConfigRequest : public GwRequestMessage class ResetConfigRequest : public GwRequestMessage
{ {
String hash;
public: public:
ResetConfigRequest() : GwRequestMessage(F("application/json"),F("resetConfig")){}; ResetConfigRequest(String hash) : GwRequestMessage(F("application/json"),F("resetConfig")){
this->hash=hash;
};
protected: protected:
virtual void processRequest() virtual void processRequest()
{ {
if (! checkPass(hash)){
result=JSON_INVALID_PASS;
return;
}
config.reset(true); config.reset(true);
logger.logString("reset config, restart"); logger.logString("reset config, restart");
result = JSON_OK; result = JSON_OK;
@ -732,7 +757,7 @@ void setup() {
logger.flush(); logger.flush();
webserver.registerMainHandler("/api/reset", [](AsyncWebServerRequest *request)->GwRequestMessage *{ webserver.registerMainHandler("/api/reset", [](AsyncWebServerRequest *request)->GwRequestMessage *{
return new ResetRequest(); return new ResetRequest(request->arg("_hash"));
}); });
webserver.registerMainHandler("/api/capabilities", [](AsyncWebServerRequest *request)->GwRequestMessage *{ webserver.registerMainHandler("/api/capabilities", [](AsyncWebServerRequest *request)->GwRequestMessage *{
return new CapabilitiesRequest(); return new CapabilitiesRequest();
@ -757,7 +782,7 @@ void setup() {
return msg; return msg;
}); });
webserver.registerMainHandler("/api/resetConfig", [](AsyncWebServerRequest *request)->GwRequestMessage * webserver.registerMainHandler("/api/resetConfig", [](AsyncWebServerRequest *request)->GwRequestMessage *
{ return new ResetConfigRequest(); }); { return new ResetConfigRequest(request->arg("_hash")); });
webserver.registerMainHandler("/api/boatData", [](AsyncWebServerRequest *request)->GwRequestMessage * webserver.registerMainHandler("/api/boatData", [](AsyncWebServerRequest *request)->GwRequestMessage *
{ return new BoatDataRequest(); }); { return new BoatDataRequest(); });
webserver.registerMainHandler("/api/boatDataString", [](AsyncWebServerRequest *request)->GwRequestMessage * webserver.registerMainHandler("/api/boatDataString", [](AsyncWebServerRequest *request)->GwRequestMessage *

View File

@ -195,14 +195,14 @@ body {
overflow-y: auto; overflow-y: auto;
} }
div#overlay { .overlay {
margin: auto; margin: auto;
background-color: white; background-color: white;
padding: 0.5em; padding: 0.5em;
max-width: 100%; max-width: 100%;
box-sizing: border-box; box-sizing: border-box;
} }
div#overlayContent { .overlayContent {
padding: 0.5em; padding: 0.5em;
} }
div#overlayContent.text{ div#overlayContent.text{
@ -237,6 +237,10 @@ body {
.buttons button{ .buttons button{
padding: 0.5em; padding: 0.5em;
} }
.overlayButtons button{
padding: 0.5em;
margin-left: 0.3em;
}
button#reset{ button#reset{
padding: 0.5em; padding: 0.5em;
} }
@ -309,4 +313,7 @@ body {
.footer .source{ .footer .source{
flex: 1; flex: 1;
} }
#adminPassInput {
margin-bottom: 1em;
}

View File

@ -76,8 +76,8 @@
</div> </div>
<div class="overlayContainer hidden" id="overlayContainer"> <div class="overlayContainer hidden" id="overlayContainer">
<div id="overlay"> <div id="overlay" class="overlay">
<div id="overlayContent"> <div id="overlayContent" class="overlayContent">
AHA AHA
</div> </div>
<div class="overlayButtons"> <div class="overlayButtons">
@ -85,6 +85,20 @@
</div> </div>
</div> </div>
</div> </div>
<div class="overlayContainer hidden" id="adminPassOverlay">
<div id="adminPassOverlay" class="overlay">
<div id="adminPassOverlayContent" class="overlayContent">
<h2>Admin Password</h2>
<div id="adminPassHint"></div>
<div id="adminPassError" ></div>
<input id="adminPassInput" type="password">
<div class="overlayButtons">
<button id="adminPassCancel">Cancel</button>
<button id="adminPassOk">OK</button>
</div>
</div>
</div>
</div>
<div id="xdrHelp" class="hidden"> <div id="xdrHelp" class="hidden">
<h1>XDR Help</h1> <h1>XDR Help</h1>
<p>You can configure the mapping of various NMEA2000 entities to XDR records.</p> <p>You can configure the mapping of various NMEA2000 entities to XDR records.</p>

View File

@ -1,7 +1,7 @@
let self = this; let self = this;
let lastUpdate = (new Date()).getTime(); let lastUpdate = (new Date()).getTime();
let reloadConfig = false; let reloadConfig = false;
let adminPass="ABCDE"; let needAdminPass=true;
let lastSalt=""; let lastSalt="";
function addEl(type, clazz, parent, text) { function addEl(type, clazz, parent, text) {
let el = document.createElement(type); let el = document.createElement(type);
@ -45,8 +45,12 @@ function getText(url){
.then(function (r) { return r.text() }); .then(function (r) { return r.text() });
} }
function reset() { function reset() {
fetch('/api/reset'); ensurePass()
.then(function (hash) {
fetch('/api/reset?_hash='+encodeURIComponent(hash));
alertRestart(); alertRestart();
})
.catch(function (e) { });
} }
function update() { function update() {
let now = (new Date()).getTime(); let now = (new Date()).getTime();
@ -94,6 +98,9 @@ function resetForm(ev) {
getJson("/api/config") getJson("/api/config")
.then(function (jsonData) { .then(function (jsonData) {
for (let k in jsonData) { for (let k in jsonData) {
if (k == "useAdminPass"){
needAdminPass=jsonData[k] != 'false';
}
let el = document.querySelector("[name='" + k + "']"); let el = document.querySelector("[name='" + k + "']");
if (el) { if (el) {
let v = jsonData[k]; let v = jsonData[k];
@ -182,22 +189,25 @@ function checkXDR(v,allValues){
} }
} }
function changeConfig() { function changeConfig() {
let url = "/api/setConfig?"; ensurePass()
.then(function (pass) {
let newAdminPass;
let url = "/api/setConfig?_hash="+encodeURIComponent(pass)+"&";
let values = document.querySelectorAll('.configForm select , .configForm input'); let values = document.querySelectorAll('.configForm select , .configForm input');
let allValues={}; let allValues = {};
for (let i = 0; i < values.length; i++) { for (let i = 0; i < values.length; i++) {
let v = values[i]; let v = values[i];
let name = v.getAttribute('name'); let name = v.getAttribute('name');
if (!name) continue; if (!name) continue;
if (name.indexOf("_") >= 0) continue; if (name.indexOf("_") >= 0) continue;
let def=getConfigDefition(name); let def = getConfigDefition(name);
if (def.type === 'password' && v.value == '') { if (def.type === 'password' && v.value == '') {
continue; continue;
} }
let check = v.getAttribute('data-check'); let check = v.getAttribute('data-check');
if (check) { if (check) {
if (typeof (self[check]) === 'function') { if (typeof (self[check]) === 'function') {
let res = self[check](v.value,allValues,getConfigDefition(name)); let res = self[check](v.value, allValues, getConfigDefition(name));
if (res) { if (res) {
let value = v.value; let value = v.value;
if (v.type === 'password') value = "******"; if (v.type === 'password') value = "******";
@ -206,28 +216,42 @@ function changeConfig() {
} }
} }
} }
allValues[name]=v.value; if (name == 'adminPassword'){
newAdminPass=v.value;
}
allValues[name] = v.value;
url += name + "=" + encodeURIComponent(v.value) + "&"; url += name + "=" + encodeURIComponent(v.value) + "&";
} }
getJson(url) getJson(url)
.then(function (status) { .then(function (status) {
if (status.status == 'OK') { if (status.status == 'OK') {
if (newAdminPass !== undefined) {
forEl('#adminPassInput', function (el) {
el.valu = newAdminPass;
});
}
alertRestart(); alertRestart();
} }
else { else {
alert("unable to set config: " + status.status); alert("unable to set config: " + status.status);
} }
}) })
})
.catch(function (e) { alert("Invalid password"); })
} }
function factoryReset() { function factoryReset() {
ensurePass()
.then(function (hash) {
if (!confirm("Really delete all configuration?\n" + if (!confirm("Really delete all configuration?\n" +
"This will reset all your Wifi settings and disconnect you.")) { "This will reset all your Wifi settings and disconnect you.")) {
return; return;
} }
getJson("/api/resetConfig") getJson("/api/resetConfig?_hash="+encodeURIComponent(hash))
.then(function (status) { .then(function (status) {
alertRestart(); alertRestart();
}) })
})
.catch(function (e) { });
} }
function createCounterDisplay(parent,label,key,isEven){ function createCounterDisplay(parent,label,key,isEven){
let clazz="row icon-row counter-row"; let clazz="row icon-row counter-row";
@ -998,23 +1022,71 @@ function loadConfigDefinitions() {
}) })
.catch(function (err) { alert("unable to load config: " + err) }) .catch(function (err) { alert("unable to load config: " + err) })
} }
function verifyPass(){ function verifyPass(pass){
return new Promise(function(resolve,reject){ return new Promise(function(resolve,reject){
let hash=lastSalt+adminPass; let hash=lastSalt+pass;
let md5hash=MD5(hash); let md5hash=MD5(hash);
getJson('api/checkPass?hash='+encodeURIComponent(md5hash)) getJson('api/checkPass?hash='+encodeURIComponent(md5hash))
.then(function(jsonData){ .then(function(jsonData){
if (jsonData.status == 'OK') resolve('ok'); if (jsonData.status == 'OK') resolve(md5hash);
else reject(jsonData.status); else reject(jsonData.status);
return; return;
}) })
.catch(function(error){reject(error);}) .catch(function(error){reject(error);})
}); });
} }
function testPass(){
verifyPass()
.then(function(r){console.log("password ok");}) function adminPassCancel(){
.catch(function(e){alert("check failed: "+e);}); forEl('#adminPassOverlay',function(el){el.classList.add('hidden')});
forEl('#adminPassInput',function(el){el.value=''});
}
function ensurePass(){
return new Promise(function(resolve,reject){
if (! needAdminPass) {
resolve('');
return;
}
let pe=document.getElementById('adminPassInput');
let hint=document.getElementById('adminPassError');
if (!pe) {
reject('no input');
return;
}
if (pe.value == ''){
let ok=document.getElementById('adminPassOk');
if (!ok) {
reject('no button');
return;
}
ok.onclick=function(){
verifyPass(pe.value)
.then(function(pass){
forEl('#adminPassOverlay',function(el){el.classList.add('hidden')});
resolve(pass);
})
.catch(function(err){
if (hint){
hint.textContent="invalid password";
}
});
};
if (hint) hint.textContent='';
forEl('#adminPassOverlay',function(el){el.classList.remove('hidden')});
}
else{
verifyPass(pe.value)
.then(function(np){resolve(np);})
.catch(function(err){
pe.value='';
ensurePass()
.then(function(p){resolve(p);})
.catch(function(e){reject(e);});
});
return;
}
});
} }
function converterInfo() { function converterInfo() {
getJson("api/converterInfo").then(function (json) { getJson("api/converterInfo").then(function (json) {