diff --git a/Readme.md b/Readme.md
index 4b12d93..3fab105 100644
--- a/Readme.md
+++ b/Readme.md
@@ -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.
diff --git a/src/main.cpp b/src/main.cpp
index 6731cbc..2fa0ec9 100644
--- a/src/main.cpp
+++ b/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 *
diff --git a/web/index.css b/web/index.css
index 232097a..b338b27 100644
--- a/web/index.css
+++ b/web/index.css
@@ -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;
+ }
\ No newline at end of file
diff --git a/web/index.html b/web/index.html
index 25f0065..48f2544 100644
--- a/web/index.html
+++ b/web/index.html
@@ -76,8 +76,8 @@
-
-
+
+
AHA
@@ -85,6 +85,20 @@
+
+
+
+
Admin Password
+
+
+
+
+
+
+
+
+
+
XDR Help
You can configure the mapping of various NMEA2000 entities to XDR records.
diff --git a/web/index.js b/web/index.js
index 90df08d..4c6fe0b 100644
--- a/web/index.js
+++ b/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) {