Files
OBPkp61/src/webserver.cpp

433 lines
16 KiB
C++

#include "main.h"
#include "config.h"
#include "webserver.h"
#include "hash.h"
#include "led.h"
#include <map>
#include <AsyncTCP.h>
#include <ArduinoJson.h>
#include <esp32/clk.h> // for cpu frequency
#include "esp_rom_uart.h" // for uart wait idle
#include <Update.h>
// TODO
// - replace StaticJsonDocument by AsyncResponseStream
// - add lastseen to devices in devicelist
// Logging
static const char* TAG = "WEB";
AsyncWebServer server(80);
uint32_t apiToken = esp_random();
bool updateSuccess = false;
String updateError = "";
class EmbeddedFile;
static std::map<String, EmbeddedFile*> embeddedFiles;
class EmbeddedFile {
public:
const uint8_t *start;
int len;
String contentType;
EmbeddedFile(String name, String contentType, const uint8_t *start, int len) {
this->start = start;
this->len = len;
this->contentType = contentType;
embeddedFiles[name] = this;
}
};
#define EMBED_GZ_FILE(fileName, binName, contentType) \
extern const uint8_t binName##_File[] asm("_binary_" #binName "_start"); \
extern const uint8_t binName##_FileLen[] asm("_binary_" #binName "_size"); \
const EmbeddedFile binName##_Config(fileName,contentType,(const uint8_t*)binName##_File,(int)binName##_FileLen);
#include "embeddedfiles.h"
void send_embedded_file(String name, AsyncWebServerRequest *request)
{
std::map<String, EmbeddedFile*>::iterator it = embeddedFiles.find(name);
if (it != embeddedFiles.end()) {
EmbeddedFile* found = it->second;
AsyncWebServerResponse *response = request->beginResponse(200, found->contentType, found->start, found->len);
response->addHeader(F("Content-Encoding"), F("gzip"));
request->send(response);
} else {
request->send(404, "text/plain", "Not found");
}
}
String uptime_with_unit() {
int64_t uptime = esp_timer_get_time() / 1000000;
String uptime_unit;
if (uptime < 120) {
uptime_unit = " seconds";
} else {
if (uptime < 2 * 3600) {
uptime /= 60;
uptime_unit = " minutes";
} else if (uptime < 2 * 3600 * 24) {
uptime /= 3600;
uptime_unit = " hours";
} else {
uptime /= 86400;
uptime_unit = " days";
}
}
return String(uptime) + " " + uptime_unit;
}
bool check_pass(String hash) {
if (! config.getBool("useAdminPass")) {
return true;
}
char salt[9]; // use to easy get upper case hex
sprintf(salt, "%08X", apiToken + (millis()/1000UL & ~0x7UL));
String passhash = get_sha256(String(salt) + config.getString("adminPassword"));
LOGD(TAG, "check hash: %s", hash.c_str());
LOGD(TAG, "check against: %s", passhash.c_str());
return hash == passhash;
}
void webserver_init() {
// Route for root / web page
server.on("/", HTTP_GET, [](AsyncWebServerRequest *request) {
send_embedded_file("index.html", request);
});
// Route for all other defined pages
for (auto it = embeddedFiles.begin(); it != embeddedFiles.end(); it++) {
String uri = String("/") + it->first;
server.on(uri.c_str(), HTTP_GET, [it](AsyncWebServerRequest *request) {
send_embedded_file(it->first, request);
});
}
// API fast hack
server.on("/api/capabilities", HTTP_GET, [](AsyncWebServerRequest *request) {
LOGD(TAG, "capabilities called");
StaticJsonDocument<100> doc;
doc["apPwChange"] = "true";
String out;
serializeJson(doc, out);
request->send(200, "application/json", out);
});
server.on("/api/checkpass", HTTP_GET, [](AsyncWebServerRequest *request) {
// hash has to be in sha256 format
AsyncResponseStream *response = request->beginResponseStream("application/json");
LOGD(TAG, "checkpass called");
String hash = request->arg("hash");
//StaticJsonDocument<100> doc;
//char salt[9];
//sprintf(salt, "%08X", apiToken + (millis()/1000UL & ~0x7UL));
//String passhash = get_sha256(String(salt) + config.getString("adminPassword"));
//LOGD(TAG, "check hash: %s", hash.c_str());
//LOGD(TAG, "check against: %s", passhash.c_str());
//doc["status"] = check_pass(hash) ? "OK" : "FAILED";
//String out;
//serializeJson(doc, out);
//request->send(200, "application/json", out);
response->print(R"({"status":)");
response->print(check_pass(hash) ? R"("OK")" : R"("FAILED")");
response->print("}");
request->send(response);
});
server.on("/api/config", HTTP_GET, [](AsyncWebServerRequest *request) {
LOGD(TAG, "config called");
StaticJsonDocument<1024> doc;
doc["systemName"] = config.getString("systemName");
doc["systemMode"] = String(config.getChar("systemMode"));
doc["nightMode"] = config.getBool("nightMode") ? "true" : "false";
doc["instDesc1"] = config.getString("instDesc1");
doc["instDesc2"] = config.getString("instDesc2");
doc["logLevel"] = loglevel;
doc["version"] = VERSION;
doc["useAdminPass"] = config.getBool("useAdminPass") ? "true" : "false";
doc["apEnable"] = config.getBool("apEnable") ? "true" : "false";
doc["apIp"] = config.getString("apIp");
doc["apMask"] = config.getString("apMask");
doc["stopApTime"] = config.getShort("stopApTime");
doc["apHidden"] = config.getBool("apHidden") ? "true" : "false";
doc["cpuSpeed"] = config.getShort("cpuSpeed");
doc["tempFormat"] = String(config.getChar("tempFormat"));
doc["ledBrightness"] = config.getShort("ledBrightness");
doc["rgbBrightness"] = config.getShort("rgbBrightness");
doc["buzEnable"] = config.getBool("buzEnable") ? "true" : "false";
doc["buzPower"] = config.getByte("buzPower");
doc["key1"] = keycode[BUTTON_1];
doc["key2"] = keycode[BUTTON_2];
doc["key3"] = keycode[BUTTON_3];
doc["key4"] = keycode[BUTTON_4];
doc["key5"] = keycode[BUTTON_5];
doc["key6"] = keycode[BUTTON_6];
doc["key1long"] = longcode[BUTTON_1];
doc["key2long"] = longcode[BUTTON_2];
doc["key3long"] = longcode[BUTTON_3];
doc["key4long"] = longcode[BUTTON_4];
doc["key5long"] = longcode[BUTTON_5];
doc["key6long"] = longcode[BUTTON_6];
doc["n2kSysInst"] = config.getByte("n2kSysInst");
doc["n2kDevInst"] = config.getByte("n2kDevInst");
doc["n2kDestA"] = config.getString("n2kDestA");
doc["switchBankA"] = config.getByte("switchBankA");
doc["n2kDestB"] = config.getString("n2kDestB");
doc["switchBankB"] = config.getByte("switchBankB");
doc["n2kDestC"] = config.getString("n2kDestC");
doc["switchBankC"] = config.getByte("switchBankC");
doc["envInterval"] = config.getShort("envInterval");
// TODO needed? Perhaps because entry fields are created by this list
doc["AdminPassword"] = "********";
doc["apPassword"] = "********";
String out;
serializeJson(doc, out);
request->send(200, "application/json", out);
});
server.on("/api/reset", HTTP_GET, [](AsyncWebServerRequest *request) {
AsyncResponseStream *response = request->beginResponseStream("application/json");
String hash = request->arg("hash");
response->print(R"([{"status":)");
if (check_pass(hash)) {
LOGD(TAG, "reset called");
response->print(R"("OK")");
response->print("}]");
ledcWrite(LEDC_RGBLED_B, 0); // blue config light off
led_blink(LEDC_RGBLED_G, 3, 4095, 500);
esp_rom_uart_tx_wait_idle(0);
ESP.restart();
}
LOGD(TAG, "reset failed: wrong password");
response->print(R"("FAILED")");
response->print("}]");
/*StaticJsonDocument<100> doc;
doc["status"] = "OK";
String out;
serializeJson(doc, out);
request->send(200, "application/json", out); */
});
server.on("/api/resetconfig", HTTP_GET, [](AsyncWebServerRequest *request) {
AsyncResponseStream *response = request->beginResponseStream("application/json");
String hash = request->arg("hash");
if (check_pass(hash)) {
LOGD(TAG, "resetconfig: checkpass successful");
} else {
LOGD(TAG, "resetconfig: checkpass failed");
}
LOGD(TAG, "resetconfig called");
// config.clear();
response->print("{");
response->print(R"DELIM({"status": "FAILED"})DELIM");
response->print("}");
request->send(response);
});
server.on("/api/status", HTTP_GET, [](AsyncWebServerRequest *request) {
StaticJsonDocument<512> doc;
doc["version"] = VERSION;
doc["pcbversion"] = PCBVERSION;
int cpu_freq = esp_clk_cpu_freq() / 1000000;
doc["cpuspeed"] = String(cpu_freq) + "MHz";
char ssid[13];
snprintf(ssid, 13, "%04X%08X", (uint16_t)(chipid >> 32), (uint32_t)chipid);
doc["ssid"] = ssid;
doc["fwtype"] = FIRMWARE_TYPE; // TODO ?
doc["chipid"] = CONFIG_IDF_FIRMWARE_CHIP_ID; // IDF chipid NOT device!
doc["uptime"] = uptime_with_unit();
doc["heap"]=(long)xPortGetFreeHeapSize();
doc["temp"] = String(temp, 1);
doc["hum"] = String(hum, 1);
switch (globalmode) {
case 'K':
doc["mode"] = "Keyboard";
break;
case 'A':
doc["mode"] = "Autopilot";
break;
case 'L':
doc["mode"] = "Logbook";
break;
default:
doc["mode"] = "*unknown*";
}
doc["n2kDestA"] = config.getString("n2kDestA");
doc["n2kDestB"] = config.getString("n2kDestB");
doc["n2kDestC"] = config.getString("n2kDestC");
Nmea2kTwai::Status n2kState = NMEA2000.getStatus();
Nmea2kTwai::STATE driverState = n2kState.state;
doc["n2kstate"] = NMEA2000.stateStr(driverState);
doc["n2knode"] = NMEA2000.GetN2kSource();
// use timeBucket of 8s
char salt[9];
sprintf(salt, "%08X", apiToken + (millis()/1000UL & ~0x7UL));
doc["salt"] = salt;
// security warnings
doc["warnAdminPass"] = config.getString("adminPassword") == ADMIN_PASS ? "true" : "false";
doc["warnApPass"] = config.getString("apPassword") == WIFI_PASS ? "true" : "false";
doc["status"] = "OK";
String out;
serializeJson(doc, out);
request->send(200, "application/json", out);
});
server.on("/api/fwinfo", HTTP_GET, [](AsyncWebServerRequest *request) {
StaticJsonDocument<200> doc;
doc["version"] = VERSION;
doc["build_date"] = BUILD_DATE;
doc["build_time"] = BUILD_TIME;
doc["idf"] = IDF_VERSION;
String out;
serializeJson(doc, out);
request->send(200, "application/json", out);
});
server.on("/api/setconfig", HTTP_POST, [](AsyncWebServerRequest *request) {
LOGD(TAG, "setconfig called");
// TODO _hash must be first parameter
int count = request->params();
bool need_save = false;
LOGD(TAG, "setconfig Received %d params", count);
for (int i = 0; i < count; i++) {
const AsyncWebParameter* p = request->getParam(i);
if (!config.hasKey(p->name().c_str())) {
LOGD(TAG, "POST %s=%s; key not found!", p->name(), p->value());
continue;
}
LOGD(TAG, "POST %s=%s", p->name(), p->value().c_str());
// get old value for comparison
ConfigValue oldval = config.get(p->name().c_str());
config.setValue(p->name().c_str(), p->value().c_str());
// check if value was changed
if (config.get(p->name().c_str()) == oldval) {
// comparison of variants!
LOGD(TAG, "Values are equal. No change detected.");
} else {
need_save = true;
}
}
if (need_save) {
LOGI(TAG, "Writing changed values to NVS");
config.save();
} else {
LOGI(TAG, "No changes, no action taken");
}
StaticJsonDocument<100> doc;
doc["status"] = "OK";
String out;
serializeJson(doc, out);
request->send(200, "application/json", out);
});
server.on("/api/update", HTTP_POST, [](AsyncWebServerRequest *request) {
// the request handler is triggered after the upload has finished...
// create the response, add header, and send response
LOGD(TAG, "Firmware update finished");
StaticJsonDocument<100> doc;
if (updateSuccess) {
doc["status"] = "OK";
doc["message"] = "Update sucessfull, rebooting...";
// Wait for TCP ACK to ensure response is received completely
request->client()->onAck([](void* arg, AsyncClient* c, size_t len, uint32_t time) {
ESP.restart();
});
} else {
doc["status"] = "FAILED";
doc["message"] = updateError;
}
String out;
serializeJson(doc, out);
request->send(200, "application/json", out);
}, [](AsyncWebServerRequest *request, String filename, size_t index, uint8_t *data, size_t len, bool final) {
// this is the new image upload part
if (index == 0) {
LOGI(TAG, "Retrieving firmware image named: %s", filename.c_str());
if (request->hasParam("md5", true)) {
String md5 = request->getParam("md5", true)->value();
LOGI(TAG, "MD5 hash: %s", md5.c_str());
Update.setMD5(md5.c_str());
}
if (! Update.begin(UPDATE_SIZE_UNKNOWN)) {
Update.printError(Serial);
updateError = "Update.begin() failed";
}
}
if (Update.write(data, len) != len) {
Update.printError(Serial);
updateError = "Update.write() failed";
}
if (final) {
if (Update.end(true)) {
updateSuccess = true;
} else {
Update.printError(Serial);
updateError = "Update.end() failed";
}
}
});
server.on("/api/devicelist", HTTP_GET, [](AsyncWebServerRequest *request) {
// NMEA2000 device list
LOGD(TAG, "devicelist called");
AsyncResponseStream *response = request->beginResponseStream("application/json");
response->print("[");
bool first = true;
for (uint8_t i = 0; i <= 252; i++) {
const tNMEA2000::tDevice *d = pN2kDeviceList->FindDeviceBySource(i);
if (d == nullptr) {
continue;
}
uint64_t NAME = d->GetName();
char hex_name[17];
snprintf(hex_name, sizeof(hex_name), "%08X%08X", (uint32_t)(NAME >> 32), (uint32_t)(NAME & 0xFFFFFFFF));
if (!first) {
response->print(",");
} else {
first = false;
}
// TODO last seen?
uint16_t mfcode = d->GetManufacturerCode();
response->printf(R"DELIM({"source":%d,"name":"%s","manufcode":%d,"model":"%s","manufname":"%s"})DELIM",
i, hex_name, mfcode, d->GetModelID(), NMEA2000.GetManufacturerName(mfcode));
}
response->print("]");
request->send(response);
});
server.on("/api/dyndevlist", HTTP_GET, [](AsyncWebServerRequest *request) {
// NMEA2000 dynmic config list: devices
AsyncResponseStream *response = request->beginResponseStream("application/json");
response->print("[");
bool first = true;
for (uint8_t i = 0; i <= 252; i++) {
const tNMEA2000::tDevice *d = pN2kDeviceList->FindDeviceBySource(i);
if (d == nullptr) {
continue;
}
uint64_t NAME = d->GetName();
char hex_name[17];
snprintf(hex_name, sizeof(hex_name), "%08X%08X", (uint32_t)(NAME >> 32), (uint32_t)(NAME & 0xFFFFFFFF));
if (!first) {
response->print(",");
} else {
first = false;
}
uint16_t mfcode = d->GetManufacturerCode();
response->printf(R"DELIM({"v":"%s","l":"%s - %s (%d)"})DELIM",
hex_name, d->GetModelID(), NMEA2000.GetManufacturerName(mfcode), mfcode);
}
response->print("]");
request->send(response);
});
}