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

View File

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

View File

@ -76,8 +76,8 @@
</div>
<div class="overlayContainer hidden" id="overlayContainer">
<div id="overlay">
<div id="overlayContent">
<div id="overlay" class="overlay">
<div id="overlayContent" class="overlayContent">
AHA
</div>
<div class="overlayButtons">
@ -85,6 +85,20 @@
</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">
<h1>XDR Help</h1>
<p>You can configure the mapping of various NMEA2000 entities to XDR records.</p>

View File

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