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.
|
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.
|
||||||
|
|
||||||
|
|
37
src/main.cpp
37
src/main.cpp
|
@ -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 *
|
||||||
|
|
|
@ -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;
|
||||||
|
}
|
||||||
|
|
|
@ -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>
|
||||||
|
|
102
web/index.js
102
web/index.js
|
@ -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) {
|
||||||
|
|
Loading…
Reference in New Issue