add an admin password to protect the changing api functions
This commit is contained in:
parent
df4b49ad5b
commit
30b23a72ce
|
@ -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.
|
||||
|
||||
|
|
37
src/main.cpp
37
src/main.cpp
|
@ -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 *
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
@ -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>
|
||||
|
|
172
web/index.js
172
web/index.js
|
@ -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) {
|
||||
|
|
Loading…
Reference in New Issue