diff --git a/README b/README index 76fb497..23e72b8 100644 --- a/README +++ b/README @@ -128,6 +128,8 @@ Bauteilliste ------------ 1x ESP32-S3 Nano (Waveshare) + berrybase.de + eckstein-shop.de 5x Taster schwarz 2x Taster gelb 1x M12 Einbaubuchse diff --git a/platformio.ini b/platformio.ini index b79eef9..c8a8f50 100644 --- a/platformio.ini +++ b/platformio.ini @@ -1,6 +1,6 @@ [platformio] default_envs= - esp32-s3-nano + esp32-s3-nano [env] platform = espressif32 @@ -9,18 +9,19 @@ lib_deps = Preferences Wifi Wire + ArduinoJson @ 6.18.5 ESP32Async/AsyncTCP@3.4.9 ESP32Async/ESPAsyncWebServer@3.9.1 ttlappalainen/NMEA2000-library@4.24 robtillaart/SHT31@^0.5.2 -# adafruit/Adafruit NeoPixel # only these files will be emedded into firmware board_build.embed_files = - lib/generated/index.html.gz - lib/generated/index.js.gz - lib/generated/index.css.gz - lib/generated/config.json.gz + lib/generated/index.html.gz + lib/generated/index.js.gz + lib/generated/sha256.js.gz + lib/generated/index.css.gz + lib/generated/config.json.gz extra_scripts = pre:extra_pre.py diff --git a/src/main.cpp b/src/main.cpp index 677fa5f..12a2289 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -1,5 +1,6 @@ #include #include +#include #include #include #include @@ -12,6 +13,40 @@ #include "Nmea2kTwai.h" #include + +#include "mbedtls/md.h" // for SHA256 +#include // for cpu frequency + +String get_sha256(String payload) { + byte shaResult[32]; + mbedtls_md_context_t ctx; + mbedtls_md_type_t md_type = MBEDTLS_MD_SHA256; + mbedtls_md_init(&ctx); + mbedtls_md_setup(&ctx, mbedtls_md_info_from_type(md_type), 0); + mbedtls_md_starts(&ctx); + mbedtls_md_update(&ctx, (const unsigned char *) payload.c_str(), payload.length()); + mbedtls_md_finish(&ctx, shaResult); + mbedtls_md_free(&ctx); + // convert to hex string + char buffer[sizeof(shaResult)*2 + 1]; + const char hexmap[] = "0123456789abcdef"; + for (int i = 0; i < sizeof(shaResult); i++) { + buffer[i*2] = hexmap[(shaResult[i] >> 4) & 0x0F]; + buffer[i*2+1] = hexmap[shaResult[i] & 0x0F]; + } + buffer[sizeof(buffer) - 1] = '\0'; + String hash = String(buffer); + + Serial.print("SHA256 payload: "); + Serial.print(payload); + Serial.println(); + Serial.print("SHA256-Hash: "); + Serial.print(hash); + Serial.println(); + + return hash; +} + // Logging static const char* TAG = "main.cpp"; @@ -51,10 +86,13 @@ void send_embedded_file(String name, AsyncWebServerRequest *request) } } +uint64_t chipid = ESP.getEfuseMac(); + const char* wifi_ssid = "OBPKP61"; const char* wifi_pass = "keypad61"; AsyncWebServer server(80); +unsigned long lastSensor = 0; unsigned long lastPrint = 0; unsigned long counter = 0; @@ -70,6 +108,8 @@ uint buzzerpower = 50; // TBD make use of this SHT31 sht(SHT31_ADDRESS); bool sht_available = false; +float temp = 0.0; +float hum = 0.0; int nodeid; // NMEA2000 id on bus Nmea2kTwai &NMEA2000=*(new Nmea2kTwai(CAN_TX, CAN_RX, CAN_RECOVERY_PERIOD)); @@ -80,45 +120,26 @@ String processor(const String& var) { return ""; } -/* Low level wifi setup (alternative) -static void wifi_event_handler(void* arg, esp_event_base_t event_base, - int32_t event_id, void* event_data) -{ - // printf("Event nr: %ld!\n", event_id); +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; } -void wifi_init_softap() -{ - esp_netif_init(); - esp_event_loop_create_default(); - esp_netif_create_default_wifi_ap(); - - wifi_init_config_t cfg = WIFI_INIT_CONFIG_DEFAULT(); - esp_event_handler_instance_register(WIFI_EVENT, ESP_EVENT_ANY_ID, &wifi_event_handler, NULL, NULL); - esp_wifi_init(&cfg); - wifi_config_t wifi_config = { - .ap = { - .ssid = wifi_ssid, - .ssid_len = strlen(wifi_ssid), - .channel = WIFI_CHANNEL, - .password = wifi_pass, - .max_connection = WIFI_MAX_STA, - .authmode = WIFI_AUTH_WPA2_PSK, - .pmf_cfg = { - .required = true, - }, - }, - }; - esp_wifi_set_mode(WIFI_MODE_AP); - esp_wifi_set_config(WIFI_IF_AP, &wifi_config); - esp_wifi_start(); - - ESP_LOGI(TAG, "wifi_init_softap finished. SSID:%s password:%s channel:%d", - ESP_WIFI_SSID, ESP_WIFI_PASS, ESP_WIFI_CHANNEL); -} -*/ - - void setup() { Serial.begin(115200); @@ -167,16 +188,65 @@ void setup() { send_embedded_file(it->first, request); }); } - // WIP: API - /* - server.on("/api/status", HTTP_GET, [](AsyncWebServerRequest *request){ + // API fast hack + server.on("/api/capabilities", HTTP_GET, [](AsyncWebServerRequest *request){ StaticJsonDocument<100> doc; - doc["temp"] = 22.3; - doc["ip"] = WiFi.localIP().toString(); + doc["apPwChange"] = "true"; String out; serializeJson(doc, out); request->send(200, "application/json", out); - }); */ + }); + server.on("/api/checkpass", HTTP_GET, [](AsyncWebServerRequest *request){ + StaticJsonDocument<100> doc; + doc["status"] = "FAILED"; + String out; + serializeJson(doc, out); + request->send(200, "application/json", out); + }); + server.on("/api/config", HTTP_GET, [](AsyncWebServerRequest *request){ + StaticJsonDocument<100> doc; + doc["systemName"] = "Keypad1"; + doc["version"] = "0.0"; + doc["fwtype"] = "unknown"; + doc["salt"] = "secret"; + String out; + serializeJson(doc, out); + request->send(200, "application/json", out); + }); + server.on("/api/resetconfig", HTTP_GET, [](AsyncWebServerRequest *request){ + StaticJsonDocument<100> doc; + doc["status"] = "FAILED"; + String out; + serializeJson(doc, out); + request->send(200, "application/json", out); + }); + server.on("/api/status", HTTP_GET, [](AsyncWebServerRequest *request){ + StaticJsonDocument<200> doc; + + 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["chipid"] = String(ssid); + + doc["uptime"] = uptime_with_unit(); + doc["heap"]=(long)xPortGetFreeHeapSize(); + doc["temp"] = String(temp, 1); + doc["hum"] = String(hum, 1); + doc["status"] = "OK"; + String out; + serializeJson(doc, out); + request->send(200, "application/json", out); + }); + server.on("/api/setconfig", HTTP_POST, [](AsyncWebServerRequest *request){ + StaticJsonDocument<100> doc; + doc["status"] = "FAILED"; + String out; + serializeJson(doc, out); + request->send(200, "application/json", out); + }); + // TODO POST vom Client entgegennehmen server.begin(); @@ -299,6 +369,9 @@ void setup() { Serial.print(stat, HEX); Serial.println(); + // Additional tests + String passhash = get_sha256("secretTEST"); + } void loop() { @@ -390,10 +463,9 @@ void loop() { } if (button > 0) { - sht.read(); - Serial.print(sht.getTemperature(), 1); + Serial.print(temp, 1); Serial.print("\t"); - Serial.println(sht.getHumidity(), 1); + Serial.println(hum, 1); // Debounce delay to avoid multiple triggers delay(200); } @@ -402,6 +474,13 @@ void loop() { // NMEA2000.loop(); // NMEA2000.ParseMessages(); + if (millis() - lastSensor >= 5000) { + lastSensor = millis(); + sht.read(); + temp = sht.getTemperature(); + hum = sht.getHumidity(); + } + // development heartbeat if (millis() - lastPrint >= 1000) { lastPrint = millis(); diff --git a/web/config.json b/web/config.json index e9bd209..234293b 100644 --- a/web/config.json +++ b/web/config.json @@ -1,20 +1,129 @@ [ { "name": "systemName", - "label": "system name", + "label": "System name", "type": "string", "default": "OBPkeypad61", "check": "checkSystemName", - "description": "system name, used for the access point and for services", - "category": "system" + "description": "System name, used also for the access point SSID.", + "category": "System" +}, +{ + "name": "logLevel", + "label": "Log level", + "type": "list", + "default": "0", + "list": [ + {"l":"Off (0)","v":0}, + {"l":"Error (1)","v":1}, + {"l":"Warning (2)","v":2}, + {"l":"Info (3)","v":3}, + {"l":"Debug (4)","v":4}, + {"l":"Verbose (5)","v":5} + ], + "description": "Log level at the USB port.\nHigher level means more output.", + "category": "System" +}, +{ + "name": "adminPassword", + "label": "Admin Password", + "type": "password", + "default": "esp32admin", + "check": "checkAdminPass", + "description": "Set the password for config modifications", + "category": "System" +}, +{ + "name": "useAdminPass", + "label": "Use Admin-Pass", + "type": "boolean", + "default": "true", + "description": "A password for config modifications is required.", + "category": "System" }, { "name": "apPassword", + "label": "Wifi password", "type": "password", "default": "keypad61", "check": "checkApPass", - "description": "set the password for the Wifi access point", - "category": "system", + "description": "Set the password for the Wifi access point.", + "category": "Wifi", "capabilities":{"apPwChange":["true"]} +}, +{ + "name": "apIp", + "label": "AP IP-Address", + "type": "string", + "default":"192.168.15.1", + "check": "checkApIp", + "description": "The IP address for the wifi access point.\nClients will get addresses within the same subnet.", + "category":"Wifi" +}, +{ + "name": "apMask", + "label": "AP Net-Mask", + "type": "string", + "default": "255.255.255.0", + "check": "checkNetMask", + "description": "The network mask for the access point.", + "category": "Wifi" +}, +{ + "name": "stopApTime", + "label": "AP time off", + "type": "number", + "default": "0", + "min": 0, + "max": 60, + "check": "checkMinMax", + "description": "Stop the access point after that many minutes if not used.\n1 to 60 minutes.\n\n'0' means that the access point is permanently enabled.", + "category": "Wifi" +}, +{ + "name": "cpuSpeed", + "label": "CPU Speed [MHz]", + "type": "list", + "default": "160", + "list": [ + "80", + "160", + "240" + ], + "description": "CPU speed in MHz [80|160|240].\nSlower speed means less power consumption.", + "category": "Hardware" +}, +{ + "name": "ledBrightness", + "label": "LED brightness", + "type": "number", + "default": 64, + "min": 0, + "max": 255, + "description": "The brightness of the destination leds (0..255).", + "category": "Hardware" +}, +{ + "name": "ledRgbBrightness", + "label": "RGB-LED brightness", + "type": "number", + "default": 64, + "min": 0, + "max": 255, + "description": "The brightness of the rgb status led (0..255).", + "category": "Hardware" +}, +{ + "name": "tempFormat", + "label": "Temperature Format", + "type": "list", + "default": "C", + "list": [ + "K", + "C", + "F" + ], + "description": "Temperature format: Kelvin, Celsius or Fahrenheit [K|C|F].", + "category": "Units" } ] diff --git a/web/index.css b/web/index.css index e69de29..5945c98 100644 --- a/web/index.css +++ b/web/index.css @@ -0,0 +1,291 @@ +* { + box-sizing: border-box; +} + +body { + font-family: Arial, Helvetica, sans-serif; + margin: 0; + padding: 0; + display: flex; + flex-direction: column; + position: absolute; + top: 0; + bottom: 0; + left: 0; + right: 0; +} + +.main { + display: flex; + flex-direction: column; + margin: 0.2em; + overflow: hidden; +} + +#tabs { + display: flex; + flex-wrap: wrap; + border-bottom: 1px solid grey; + margin-bottom: 0.5em; +} +#tabs .tab { + background-color: lightgray; + padding: 0.5em; + /*border: 1px; + border-color: grey; + border-style: solid; */ + border: 1px solid grey; + opacity: 0.6; +} +#tabs .tab.active { + opacity: 1; +} + +#tabPages { + overflow: auto; + padding-bottom: 1ex; + border-bottom: 1px solid grey; +} + +.configForm { + padding-bottom: 0.5em; +} +.configForm .buttons { + margin-bottom: 0.5em; +} +.configForm .content>div:nth-child(even) { + background-color: rgb(211 211 211 / 43%); +} +#statusPage .even { + background-color: rgb(211 211 211 / 43%); +} +#statusPageContent { + margin-bottom: 0.5em; +} +.counter-row .value{ + text-align: right; + width: 6em; +} +.icon-row .label{ + width: 8.7em; +} +.category .title .label { + opacity: 1; + margin-left: 1em; +} +.changed input{ + color: green; +} +.changed select{ + color: green; +} +.category.changed{ + color: green; +} +span.label { + width: 10em; + display: inline-block; + opacity: 0.6; +} +.configForm .value { + width: 21em; + display: flex; + flex-direction: row; + margin-bottom: 0.2em; +} +span#connected { + display: inline-block; + background-color: red; + width: 1em; + height: 1em; + border-radius: 50%; +} +span#connected.ok{ + background-color: #13ac13; +} +.row { + padding: 0.5em; + display: flex; + flex-direction: row; + flex-wrap: wrap; + align-items: center; +} +input,select { + border: 1px solid #808080a1; + font-size: 0.9em; + padding: 0.2em; +} +.filter { + display: inline-block; +} +button.infoButton { + margin-left: 1em; + vertical-align: bottom; +} +.category .title { + padding-left: 0.5em; + background-color: lightgray; + padding-top: 0.3em; + padding-bottom: 0.5em; + border-bottom: 1px solid darkgray; + display: flex; + align-items: center; +} +.value button { + margin-left: 0.5em; +} +.hidden{ + display: none !important; +} +.dash.invalid{ + display: none; +} +button.addunassigned { + margin-left: 1em; +} +.msgDetails .value { + width: 5em; + text-align: right; +} +.msgDetails .label { + width: 5em; +} +.overlayContainer { + position: fixed; + top: 0; + bottom: 0; + left: 0; + right: 0; + background-color: #80808070; + display: flex; + overflow-y: auto; +} + +.overlay { + margin: auto; + background-color: white; + padding: 0.5em; + max-width: 100%; + box-sizing: border-box; +} +.overlayContent { + padding: 0.5em; +} +div#overlayContent.text{ + white-space: pre-line; +} +.overlayButtons { + border-top: 1px solid grey; + padding-top: 0.5em; + display: flex; + flex-direction: row; + justify-content: end; +} +.buttons button { + margin: 0.2ex 0; + padding: 0.5em; +} +.overlayButtons button { + padding: 0.5em; + margin-left: 0.3em; +} +button#reset { + padding: 0.5em; +} +h1 { + margin-bottom: 0; +} +.icon-eye { + background-image: url("data:image/svg+xml;utf-8, Layer 1 "); + margin-left: 0.5em; + opacity: 0.3; +} +.icon{ + height: 1.5em; + width: 1.5em; + display: inline-block; +} +.icon-more{ + background-image: url("data:image/svg+xml;utf-8, Layer 1 "); +} +.icon-less{ + background-image: url("data:image/svg+xml;utf-8, Layer 1 "); +} +.icon-eye.active{ + opacity: 1; +} + +.dash { + width: 6.5em; + height: 4em; + display: flex; + flex-direction: column; + border: 1px solid grey; + overflow: hidden; + margin: 0; + box-sizing: border-box; + font-size: 1.2em; + justify-content: space-between; +} +div#dashboardPage { + display: flex; + flex-wrap: wrap; + justify-content: center; +} +.dashTitle { + font-size: 0.8em; + background-color: lightgray; +} +.dashValue{ + font-size: 1.6em; + text-align: center; +} +.dashValue.formatLatitude { + font-size: 1.1em; + } +.dashValue.formatLongitude { + font-size: 1.1em; + } +.dashValue.formatDate { + font-size: 1.2em; +} +.footer { + display: flex; + flex-direction: row; + padding: 0.1em; + background-color: lightgray; + font-size: 0.7em; + } +.footer .unit{ +} +.footer .source{ + flex: 1; +} +#adminPassInput { + margin-bottom: 1em; +} +input#uploadFile { + margin-top: 0.5em; + margin-bottom: 0.5em; +} +div#uploadProgress { + width: 100%; + max-width: 20em; + height: 1em; + margin-left: 1em; + /* margin-right: auto; */ + border: 1px solid grey; + margin-top: 0.5em; + margin-bottom: 0.5em; +} +#uploadDone{ + background-color: blue; + width: 0px; + height: 100%; +} +.error{ + color: red; +} +input.error{ + background-color: rgba(255, 0, 0, 0.329); +} diff --git a/web/index.html b/web/index.html index cc2f321..0c3da1a 100644 --- a/web/index.html +++ b/web/index.html @@ -1,17 +1,166 @@ - - - -OBPkeypad 6/1 + + + +OBPkeyboard 6/1 +
-

OBPkeypad 6/1

-

Work in progress

-

Furter Information

+

OBPkb

+
+ disconnected +
+
+
Status
+
Config
+
Update
+
Help
+
+
+ +
+
+
+ Firmware + --- + +
+
+ MCU-ID + --- +
+
+ CPU speed + --- +
+
+ Free heap + --- +
+
+ Uptime + --- +
+
+ NMEA2000 State + [---]  + UNKNOWN +
+ +
+ Dest A + --- +
+
+ Dest B + --- +
+
+ Dest C + --- +
+ +
+ Sensor: Temperature + ---°C +
+
+ Sensor: Humidity + ---% +
+ +
+ +
+ + + + +
+ +
+ + + + + diff --git a/web/index.js b/web/index.js index e69de29..2ddf424 100644 --- a/web/index.js +++ b/web/index.js @@ -0,0 +1,1643 @@ +(function () { + let lastUpdate = (new Date()).getTime(); + let updateInterval = 3000; // milliseconds + let reloadConfig = false; + let needAdminPass = true; + let lastSalt = ""; + let channelList = {}; + let minUser = 200; + let listeners = []; + let buttonHandlers = {}; + let checkers = {}; + let userFormatters = {}; + let apiPrefix = ""; + function addEl(type, clazz, parent, text) { + let el = document.createElement(type); + if (clazz) { + if (!(clazz instanceof Array)) { + clazz = clazz.split(/ */); + } + clazz.forEach(function (ce) { + el.classList.add(ce); + }); + } + if (text) el.textContent = text; + if (parent) { + if (typeof(parent) != 'object'){ + parent=document.querySelector(parent); + } + if (parent) parent.appendChild(el); + } + return el; + } + function forEl(query, callback, base) { + if (!base) base = document; + let all = base.querySelectorAll(query); + for (let i = 0; i < all.length; i++) { + callback(all[i]); + } + } + function closestParent(element, clazz) { + while (true) { + let parent = element.parentElement; + if (!parent) return; + if (parent.classList.contains(clazz)) return parent; + element = parent; + } + } + function alertRestart() { + reloadConfig = true; + alert("Board reset triggered, reconnect WLAN if necessary"); + } + function getJson(url) { + return fetch(url) + .then(function (r) { return r.json() }); + } + function getText(url) { + return fetch(url) + .then(function (r) { return r.text() }); + } + buttonHandlers.reset=function() { + ensurePass() + .then(function (hash) { + fetch(apiPrefix + '/api/reset?_hash=' + encodeURIComponent(hash)); + alertRestart(); + }) + .catch(function (e) { }); + } + function update() { + let now = (new Date()).getTime(); + let ce = document.getElementById('connected'); + let cl = document.getElementById('conn_label'); + if (ce) { + if ((lastUpdate + 3 * updateInterval) > now) { + ce.classList.add('ok'); + cl.textContent = 'connected'; + } + else { + ce.classList.remove('ok'); + cl.textContent = 'disconnected'; + } + } + getJson(apiPrefix + '/api/status') + .then(function (jsonData) { + if (jsonData.salt !== undefined) { + lastSalt=jsonData.salt; + delete jsonData.salt; + } + if (jsonData.minUser !== undefined){ + minUser=jsonData.minUser; + delete jsonData.minUser; + } + callListeners(api.EVENTS.status,jsonData); + let statusPage = document.getElementById('statusPageContent'); + let even = true; //first counter + if (statusPage){ + for (let k in jsonData) { + if (typeof (jsonData[k]) === 'object') { + if (k.indexOf('count') == 0) { + createCounterDisplay(statusPage, k.replace("count", "").replace(/in$/, " in").replace(/out$/, " out"), k, even); + even = !even; + for (let sk in jsonData[k]) { + let key = k + "." + sk; + if (typeof (jsonData[k][sk]) === 'object') { + //msg details + updateMsgDetails(key, jsonData[k][sk]); + } + else { + let el = document.getElementById(key); + if (el) el.textContent = jsonData[k][sk]; + } + } + } + if (k.indexOf("ch") == 0) { + //channel def + let name = k.substring(2); + channelList[name] = jsonData[k]; + } + } + else { + let el = document.getElementById(k); + if (el) el.textContent = jsonData[k]; + forEl('.status-' + k, function (el) { + el.textContent = jsonData[k]; + }); + } + } + } + lastUpdate = (new Date()).getTime(); + if (reloadConfig) { + reloadConfig = false; + resetForm(); + } + }) + } + function resetForm(ev) { + getJson(apiPrefix + "/api/config") + .then(function (jsonData) { + callListeners(api.EVENTS.config,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]; + let def = getConfigDefition(k); + if (def.check == 'checkMinMax') { + //simple migration if the current value is outside the range + //we even "hide" this from the user + v = parseFloat(v); + if (!isNaN(v)) { + if (def.min !== undefined) { + if (v < parseFloat(def.min)) v = parseFloat(def.min); + } + if (def.max !== undefined) { + if (v > parseFloat(def.max)) v = parseFloat(def.max); + } + } + } + if (el.tagName === 'SELECT') { + setSelect(el, v); + } + else { + el.value = v; + } + el.setAttribute('data-loaded', v); + let changeEvent = new Event('change'); + el.dispatchEvent(changeEvent); + } + } + let name = jsonData.systemName; + if (name) { + let el = document.getElementById('headline'); + if (el) el.textContent = name; + document.title = name; + } + }); + } + buttonHandlers.resetForm=resetForm; + + checkers.checkMinMax=function(v, allValues, def) { + let parsed = parseFloat(v); + if (isNaN(parsed)) return "must be a number"; + if (def.min !== undefined) { + if (parsed < parseFloat(def.min)) return "must be >= " + def.min; + } + if (def.max !== undefined) { + if (parsed > parseFloat(def.max)) return "must be <= " + def.max; + } + } + checkers.checkPort=function(v,allValues,def){ + let parsed=parseInt(v); + if (isNaN(parsed)) return "must be a number"; + if (parsed <1 || parsed >= 65536) return "port must be in the range 1..65536"; + } + checkers.checkSystemName=function(v) { + //2...32 characters for ssid + let allowed = v.replace(/[^a-zA-Z0-9]*/g, ''); + if (allowed != v) return "contains invalid characters, only a-z, A-Z, 0-9"; + if (v.length < 2 || v.length > 32) return "invalid length (2...32)"; + } + function checkApPass(v) { + //min 8 characters + if (v.length < 8) { + return "password must be at least 8 characters"; + } + } + checkers.checkAdminPass=function(v) { + return checkApPass(v); + } + checkers.checkApIp=function(v, allValues) { + if (!v) return "cannot be empty"; + let err1 = "must be in the form 192.168.x.x"; + if (!v.match(/[0-9]+\.[0-9]+\.[0-9]+\.[0-9]+/)) return err1; + let parts = v.split("."); + if (parts.length != 4) return err1; + for (let idx = 0; idx < 4; idx++) { + let iv = parseInt(parts[idx]); + if (iv < 0 || iv > 255) return err1; + } + } + checkers.checkNetMask=function(v, allValues) { + return checkers.checkApIp(v, allValues); + } + checkers.checkIpAddress=function(v, allValues, def) { + if (!v) return "cannot be empty"; + if (!v.match(/[0-9]*\.[0-9]*\.[0-9]*\.[0-9]*/) + && !v.match(/.*\.local/)) + return "must be either in the form 192.168.1.1 or xxx.local"; + } + checkers.checkMCAddress=function(v, allValues, def) { + if (!v) return "cannot be empty"; + if (!v.match(/[0-9]*\.[0-9]*\.[0-9]*\.[0-9]*/)) + return "must be in the form 224.0.0.1"; + let parts=v.split("."); + let o1=parseInt(parts[0]); + if (o1 < 224 || o1 > 239) return "mulicast address must be in the range 224.0.0.0 to 239.255.255.255" + + } + let loggedChecks={}; + function getAllConfigs(omitPass) { + 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; + if (v.getAttribute('disabled')) continue; + let def = getConfigDefition(name); + if (def.type === 'password' && (v.value == '' || omitPass)) { + continue; + } + let check = v.getAttribute('data-check'); + if (check && conditionOk(name)) { + let cfgDef=getConfigDefition(name); + let checkFunction=checkers[check]; + if (typeof (checkFunction) === 'function') { + if (! loggedChecks[check]){ + loggedChecks[check]=true; + //console.log("check:"+check); + } + let res = checkFunction(v.value, allValues, cfgDef); + if (res) { + let value = v.value; + if (v.type === 'password') value = "******"; + let label = v.getAttribute('data-label'); + if (!label) label = v.getAttribute('name'); + v.classList.add("error"); + alert("invalid config for "+cfgDef.category+":" + label + "(" + value + "):\n" + res); + return; + } + } + else{ + console.log("check not found:",check); + } + } + allValues[name] = v.value; + } + return allValues; + } + buttonHandlers.changeConfig=function() { + ensurePass() + .then(function (pass) { + let newAdminPass; + let url = apiPrefix + "/api/setconfig" + let body = "_hash=" + encodeURIComponent(pass) + "&"; + let allValues = getAllConfigs(); + if (!allValues) return; + for (let name in allValues) { + if (name == 'adminPassword') { + newAdminPass = allValues[name]; + } + body += encodeURIComponent(name) + "=" + encodeURIComponent(allValues[name]) + "&"; + } + fetch(url, { + method: 'POST', + headers: { + //'Content-Type': 'application/octet-stream' //we must lie here + 'Content-Type': 'application/x-www-form-urlencoded' + }, + body: body + }) + .then((rs) => rs.json()) + .then(function (status) { + if (status.status == 'OK') { + if (newAdminPass !== undefined) { + forEl('#adminPassInput', function (el) { + el.valu = newAdminPass; + }); + saveAdminPass(newAdminPass, true); + } + alertRestart(); + } + else { + alert("unable to set config: " + status.status); + } + }) + }) + .catch(function (e) { alert(e); }) + } + buttonHandlers.factoryReset=function() { + ensurePass() + .then(function (hash) { + if (!confirm("Really delete all configuration?\n" + + "This will reset all your Wifi settings and disconnect you.")) { + return; + } + getJson(apiPrefix + "/api/resetconfig?_hash=" + encodeURIComponent(hash)) + .then(function (status) { + alertRestart(); + }) + }) + .catch(function (e) { }); + } + function createCounterDisplay(parent, label, key, isEven) { + if (parent.querySelector("#" + key)) { + return; + } + let clazz = "row icon-row counter-row"; + if (isEven) clazz += " even"; + let row = addEl('div', clazz, parent); + row.setAttribute("id", key); + let icon = addEl('span', 'icon icon-more', row); + addEl('span', 'label', row, label); + let value = addEl('span', 'value', row, '---'); + value.setAttribute('id', key + ".sumOk"); + let display = addEl('div', clazz + " msgDetails hidden", parent); + display.setAttribute('id', key + ".ok"); + row.addEventListener('click', function (ev) { + let rs = display.classList.toggle('hidden'); + if (rs) { + icon.classList.add('icon-more'); + icon.classList.remove('icon-less'); + } + else { + icon.classList.remove('icon-more'); + icon.classList.add('icon-less'); + } + }); + callListeners(api.EVENTS.counterDisplayCreated,row); + } + function validKey(key) { + if (!key) return; + return key.replace(/[^a-z_:A-Z0-9-]/g, ''); + } + function updateMsgDetails(key, details) { + forEl('.msgDetails', function (frame) { + if (frame.getAttribute('id') !== key) return; + for (let k in details) { + k = validKey(k); + let el = frame.querySelector("[data-id=\"" + k + "\"] "); + if (!el) { + el = addEl('div', 'row', frame); + let cv = addEl('span', 'label', el, k); + cv = addEl('span', 'value', el, details[k]); + cv.setAttribute('data-id', k); + } + else { + el.textContent = details[k]; + } + } + forEl('.value', function (el) { + let k = el.getAttribute('data-id'); + if (k && !details[k]) { + el.parentElement.remove(); + } + }, frame); + }); + } + + function showOverlay(text, isHtml, buttons) { + let el = document.getElementById('overlayContent'); + if (text instanceof Object) { + el.textContent = ''; + el.appendChild(text); + } + else { + if (isHtml) { + el.innerHTML = text; + el.classList.remove("text"); + } + else { + el.textContent = text; + el.classList.add("text"); + } + } + buttons = (buttons ? buttons : []).concat([{ label: "Close", click: hideOverlay }]); + let container = document.getElementById('overlayContainer'); + let btframe = container.querySelector('.overlayButtons'); + btframe.textContent = ''; + buttons.forEach((btconfig) => { + let bt = addEl('button', '', btframe, btconfig.label); + bt.addEventListener("click", btconfig.click); + }); + container.classList.remove('hidden'); + } + function hideOverlay() { + let container = document.getElementById('overlayContainer'); + container.classList.add('hidden'); + let el = document.getElementById('overlayContent'); + el.textContent = ''; + } + function checkChange(el, row, name) { + el.classList.remove("error"); + let loaded = el.getAttribute('data-loaded'); + if (loaded !== undefined) { + if (loaded != el.value) { + row.classList.add('changed'); + } + else { + row.classList.remove("changed"); + } + } + let dependend = conditionRelations[name]; + if (dependend) { + for (let el in dependend) { + checkCondition(dependend[el]); + } + } + } + let configDefinitions = {}; + //a map between the name of a config item and a list of dependend items + let conditionRelations = {}; + function getConfigDefition(name) { + if (!name) return {}; + let def; + for (let k in configDefinitions) { + if (configDefinitions[k].name === name) { + def = configDefinitions[k]; + break; + } + } + if (!def) return {}; + return def; + } + function getConditions(name) { + let def = getConfigDefition(name); + if (!def) return; + let condition = def.condition; + if (!condition) return; + if (!(condition instanceof Array)) condition = [condition]; + return condition; + } + + function conditionOk(name){ + let condition = getConditions(name); + if (!condition) return true; + let visible = false; + if (!condition instanceof Array) condition = [condition]; + condition.forEach(function (cel) { + let lvis = true; + for (let k in cel) { + let item = document.querySelector('[name=' + k + ']'); + if (item) { + let compare = cel[k]; + if (compare instanceof Array) { + if (compare.indexOf(item.value) < 0) lvis = false; + } + else { + if (item.value != cel[k]) lvis = false; + } + } + } + if (lvis) visible = true; + }); + return visible; + } + + function checkCondition(element) { + let name = element.getAttribute('name'); + let visible=conditionOk(name); + let row = closestParent(element, 'row'); + if (!row) return; + if (visible) row.classList.remove('hidden'); + else row.classList.add('hidden'); + } + let caliv = 0; + function createCalSetInput(configItem, frame, clazz) { + let el = addEl('input', clazz, frame); + let cb = addEl('button', '', frame, 'C'); + //el.disabled=true; + cb.addEventListener('click', (ev) => { + let cs = document.getElementById("calset").cloneNode(true); + cs.classList.remove("hidden"); + cs.querySelector(".heading").textContent = configItem.label || configItem.name; + let vel = cs.querySelector(".val"); + if (caliv != 0) window.clearInterval(caliv); + caliv = window.setInterval(() => { + if (document.body.contains(cs)) { + fetch(apiPrefix + "/api/calibrate?name=" + encodeURIComponent(configItem.name)) + .then((r) => r.text()) + .then((txt) => { + if (txt != vel.textContent) { + vel.textContent = txt; + } + }) + .catch((e) => { + alert(e); + hideOverlay(); + window.clearInterval(caliv); + }) + } + else { + window.clearInterval(caliv); + } + }, 200); + showOverlay(cs, false, [{ + label: 'Set', click: () => { + el.value = vel.textContent; + let cev = new Event('change'); + el.dispatchEvent(cev); + } + }]); + }) + el.setAttribute('name', configItem.name) + return el; + } + function createCalValInput(configItem, frame, clazz) { + let el = addEl('input', clazz, frame); + let cb = addEl('button', '', frame, 'C'); + //el.disabled=true; + cb.addEventListener('click', (ev) => { + const sv = function (val, cfg) { + if (configItem.eval) { + let v = parseFloat(val); + let c = parseFloat(cfg); + return (eval(configItem.eval)); + } + return v; + }; + let cs = document.getElementById("calval").cloneNode(true); + cs.classList.remove("hidden"); + cs.querySelector(".heading").textContent = configItem.label || configItem.name; + let vel = cs.querySelector(".val"); + let vinp = cs.querySelector("input"); + vinp.value = el.value; + if (caliv != 0) window.clearInterval(caliv); + caliv = window.setInterval(() => { + if (document.body.contains(cs)) { + fetch(apiPrefix + "/api/calibrate?name=" + encodeURIComponent(configItem.name)) + .then((r) => r.text()) + .then((txt) => { + txt = sv(txt, vinp.value); + if (txt != vel.textContent) { + vel.textContent = txt; + } + }) + .catch((e) => { + alert(e); + hideOverlay(); + window.clearInterval(caliv); + }) + } + else { + window.clearInterval(caliv); + } + }, 200); + showOverlay(cs, false, [{ + label: 'Set', click: () => { + el.value = vinp.value; + let cev = new Event('change'); + el.dispatchEvent(cev); + } + }]); + }) + el.setAttribute('name', configItem.name) + return el; + } + function createInput(configItem, frame, clazz) { + let el; + if (configItem.type === 'boolean' || configItem.type === 'list') { + el = addEl('select', clazz, frame); + if (configItem.readOnly) el.setAttribute('disabled', true); + el.setAttribute('name', configItem.name) + let slist = []; + if (configItem.list) { + configItem.list.forEach(function (v) { + if (v instanceof Object) { + slist.push({ l: v.l, v: v.v }); + } + else { + slist.push({ l: v, v: v }); + } + }) + } + else { + slist.push({ l: 'on', v: 'true' }) + slist.push({ l: 'off', v: 'false' }) + } + slist.forEach(function (sitem) { + let sitemEl = addEl('option', '', el, sitem.l); + sitemEl.setAttribute('value', sitem.v); + }) + return el; + } + if (configItem.type === 'filter') { + return createFilterInput(configItem, frame, clazz); + } + el = addEl('input', clazz, frame); + if (configItem.readOnly) el.setAttribute('disabled', true); + el.setAttribute('name', configItem.name) + if (configItem.type === 'password') { + el.setAttribute('type', 'password'); + let vis = addEl('span', 'icon-eye icon', frame); + vis.addEventListener('click', function (v) { + if (vis.classList.toggle('active')) { + el.setAttribute('type', 'text'); + } + else { + el.setAttribute('type', 'password'); + } + }); + } + else if (configItem.type === 'number') { + el.setAttribute('type', 'number'); + } + else { + el.setAttribute('type', 'text'); + } + return el; + } + + function setSelect(item, value) { + if (!item) return; + item.value = value; + if (item.value !== value) { + //missing option with his value + let o = addEl('option', undefined, item, value); + o.setAttribute('value', value); + item.value = value; + } + } + + function updateSelectList(item, slist, opt_keepValue) { + let idx = 0; + let value; + if (opt_keepValue) value = item.value; + item.innerHTML = ''; + slist.forEach(function (sitem) { + let sitemEl = addEl('option', '', item, sitem.l); + sitemEl.setAttribute('value', sitem.v !== undefined ? sitem.v : idx); + idx++; + }) + if (value !== undefined) { + setSelect(item, value); + } + } + + function createFilterInput(configItem, frame) { + let el = addEl('div', 'filter', frame); + let ais = createInput({ + type: 'list', + name: configItem.name + "_ais", + list: ['aison', 'aisoff'], + readOnly: configItem.readOnly + }, el); + let mode = createInput({ + type: 'list', + name: configItem.name + "_mode", + list: ['whitelist', 'blacklist'], + readOnly: configItem.readOnly + }, el); + let sentences = createInput({ + type: 'text', + name: configItem.name + "_sentences", + readOnly: configItem.readOnly + }, el); + let data = addEl('input', undefined, el); + data.setAttribute('type', 'hidden'); + let changeFunction = function () { + let cv = data.value || ""; + let parts = cv.split(":"); + ais.value = (parts[0] == '0') ? "aisoff" : "aison"; + mode.value = (parts[1] == '0') ? "whitelist" : "blacklist"; + sentences.value = parts[2] || ""; + } + let updateFunction = function () { + let nv = (ais.value == 'aison') ? "1" : "0"; + nv += ":"; + nv += (mode.value == 'blacklist') ? "1" : "0"; + nv += ":"; + nv += sentences.value; + data.value = nv; + let chev = new Event('change'); + data.dispatchEvent(chev); + } + mode.addEventListener('change', updateFunction); + ais.addEventListener("change", updateFunction); + sentences.addEventListener("change", updateFunction); + data.addEventListener('change', function (ev) { + changeFunction(); + }); + data.setAttribute('name', configItem.name); + if (configItem.readOnly) data.setAttribute('disabled', true); + return data; + } + let moreicons = ['icon-more', 'icon-less']; + + function collapseCategories(parent, expand) { + let doExpand = expand; + forEl('.category', function (cat) { + if (typeof (expand) === 'function') doExpand = expand(cat); + forEl('.content', function (content) { + if (doExpand) { + content.classList.remove('hidden'); + } + else { + content.classList.add('hidden'); + } + }, cat); + forEl('.title .icon', function (icon) { + toggleClass(icon, doExpand ? 1 : 0, moreicons); + }, cat); + }, parent); + } + + function formatDateForFilename(usePrefix, d) { + let rt = ""; + if (usePrefix) { + let hdl= document.getElementById('headline'); + if (hdl){ + rt=hdl.textContent+"_"; + } + let fwt = document.querySelector('.status-fwtype'); + if (fwt) rt += fwt.textContent; + rt += "_"; + } + if (!d) d = new Date(); + [d.getFullYear(), d.getMonth(), d.getDate(), d.getHours(), d.getMinutes(), d.getSeconds()] + .forEach(function (v) { + if (v < 10) rt += "0" + v; + else rt += "" + v; + }) + return rt; + } + function downloadData(data, name) { + let url = "data:application/octet-stream," + encodeURIComponent(JSON.stringify(data, undefined, 2)); + let target = document.getElementById('downloadXdr'); + if (!target) return; + target.setAttribute('href', url); + target.setAttribute('download', name); + target.click(); + } + buttonHandlers.exportConfig=function() { + let data = getAllConfigs(true); + if (!data) return; + downloadData(data, formatDateForFilename(true) + ".json"); + } + function importJson(opt_keyPattern) { + let clazz = 'importJson'; + forEl('.' + clazz, function (ul) { + ul.remove(); + }); + let ip = addEl('input', clazz, document.body); + ip.setAttribute('type', 'file'); + ip.addEventListener('change', function (ev) { + if (ip.files.length > 0) { + let f = ip.files[0]; + let reader = new FileReader(); + reader.onloadend = function (status) { + try { + let idata = JSON.parse(reader.result); + let hasOverwrites = false; + for (let k in idata) { + if (opt_keyPattern && !k.match(opt_keyPattern)) { + alert("file contains invalid key " + k); + return; + } + let del = document.querySelector('[name=' + k + ']'); + if (del) { + hasOverwrites = true; + } + } + if (hasOverwrites) { + if (!confirm("overwrite existing data?")) return; + } + for (let k in idata) { + let del = document.querySelector('[name=' + k + ']'); + if (del) { + if (del.tagName === 'SELECT') { + setSelect(del, idata[k]); + } + else { + del.value = idata[k]; + } + let ev = new Event('change'); + del.dispatchEvent(ev); + } + } + } catch (error) { + alert("unable to parse upload: " + error); + return; + } + }; + reader.readAsBinaryString(f); + } + ip.remove(); + }); + ip.click(); + } + buttonHandlers.importConfig=function() { + importJson(); + } + function toggleClass(el, id, classList) { + let nc = classList[id]; + let rt = false; + if (nc && !el.classList.contains(nc)) rt = true; + for (let i in classList) { + if (i == id) continue; + el.classList.remove(classList[i]) + } + if (nc) el.classList.add(nc); + return rt; + } + + function createConfigDefinitions(parent, capabilities, defs) { + let categories = {}; + let frame = parent.querySelector('.configFormRows'); + if (!frame) throw Error("no config form"); + frame.innerHTML = ''; + configDefinitions = defs; + let currentCategoryPopulated = true; + defs.forEach(function (item) { + if (!item.type || item.category === undefined) return; + let catEntry; + if (categories[item.category] === undefined) { + catEntry = { + populated: false, + frame: undefined, + element: undefined + } + categories[item.category] = catEntry + catEntry.frame = addEl('div', 'category', frame); + catEntry.frame.setAttribute('data-category', item.category) + let categoryTitle = addEl('div', 'title', catEntry.frame); + let categoryButton = addEl('span', 'icon icon-more', categoryTitle); + addEl('span', 'label', categoryTitle, item.category); + addEl('span', 'categoryAdd', categoryTitle); + catEntry.element = addEl('div', 'content', catEntry.frame); + catEntry.element.classList.add('hidden'); + categoryTitle.addEventListener('click', function (ev) { + let rs = catEntry.element.classList.toggle('hidden'); + if (rs) { + toggleClass(categoryButton, 0, moreicons); + } + else { + toggleClass(categoryButton, 1, moreicons); + } + }) + } + else { + catEntry = categories[item.category]; + } + let showItem = true; + let itemCapabilities = item.capabilities || {}; + itemCapabilities['HIDE' + item.name] = null; + for (let capability in itemCapabilities) { + let values = itemCapabilities[capability]; + let found = false; + if (!(values instanceof Array)) values = [values]; + values.forEach(function (v) { + if (v === null) { + if (capabilities[capability] === undefined) found = true; + } + else { + if (capabilities[capability] == v) found = true; + } + }); + if (!found) showItem = false; + } + let readOnly = false; + let mode = capabilities['CFGMODE' + item.name]; + if (mode == 1) { + //hide + showItem = false; + } + if (mode == 2) { + readOnly = true; + } + if (showItem) { + item.readOnly = readOnly; + catEntry.populated = true; + let row = addEl('div', 'row', catEntry.element); + let label = item.label || item.name; + addEl('span', 'label', row, label); + let valueFrame = addEl('div', 'value', row); + let valueEl = createInput(item, valueFrame); + if (!valueEl) return; + valueEl.setAttribute('data-default', item.default); + if (!readOnly) valueEl.addEventListener('change', function (ev) { + let el = ev.target; + checkChange(el, row, item.name); + }) + let condition = getConditions(item.name); + if (condition) { + condition.forEach(function (cel) { + for (let c in cel) { + if (!conditionRelations[c]) { + conditionRelations[c] = []; + } + conditionRelations[c].push(valueEl); + } + }) + } + if (item.check) valueEl.setAttribute('data-check', item.check); + valueEl.setAttribute('data-label', label); + let btContainer = addEl('div', 'buttonContainer', row); + if (!readOnly) { + let bt = addEl('button', 'defaultButton', btContainer, 'X'); + bt.setAttribute('data-default', item.default); + bt.addEventListener('click', function (ev) { + valueEl.value = valueEl.getAttribute('data-default'); + let changeEvent = new Event('change'); + valueEl.dispatchEvent(changeEvent); + }) + } + bt = addEl('button', 'infoButton', btContainer, '?'); + bt.addEventListener('click', function (ev) { + if (item.description) { + showOverlay(item.description); + } + }); + } + }); + for (let cat in categories) { + let catEntry = categories[cat]; + if (!catEntry.populated) { + catEntry.frame.remove(); + } + } + } + function loadConfigDefinitions() { + getJson("api/capabilities") + .then(function (capabilities) { + if (capabilities.HELP_URL) { + let el = document.getElementById('helpButton'); + if (el) el.setAttribute('data-url', capabilities.HELP_URL); + } + try{ + Object.freeze(capabilities); + callListeners(api.EVENTS.init,capabilities); + }catch (e){ + console.log(e); + } + getJson("config.json") + .then(function (defs) { + configDefinitions = defs; + let normalConfig = document.getElementById('configPage'); + if (normalConfig) createConfigDefinitions(normalConfig, capabilities, defs); + resetForm(); + }) + }) + .catch(function (err) { alert("unable to load config: " + err) }) + } + // New hash function by SubtleCrypto Browser API + async function hashString(str) { + const enc = new TextEncoder(); + const buffer = enc.encode(str); + const hashBuffer = await crypto.subtle.digest('SHA-256', buffer); + const hashArray = new Uint8Array(hashBuffer); + const hashHex = Array.from(hashArray).map(byte => byte.toString(16).padStart(2, '0')).join(''); + return hashHex; + } + function verifyPass(pass) { + return new Promise(function (resolve, reject) { + let sha256hash = ""; + if (window.isSecureContext) { + hashString(lastSalt + pass).then(hashHex => { + sha256hash = hashHex; + }); + } else { + let hash = sha256.create(); + hash.update(lastSalt + pass); + sha256hash = hash.hex(); + } + getJson('api/checkpass?hash=' + encodeURIComponent(sha256hash)) + .then(function (jsonData) { + if (jsonData.status == 'OK') resolve(sha256hash); + else reject(jsonData.status); + return; + }) + .catch(function (error) { reject(error); }) + }); + } + + buttonHandlers.adminPassOk=function() { + // Dummy so far + } + buttonHandlers.adminPassCancel=function() { + forEl('#adminPassOverlay', function (el) { el.classList.add('hidden') }); + forEl('#adminPassInput', function (el) { el.value = '' }); + } + function saveAdminPass(pass, forceIfSet) { + forEl('#adminPassKeep', function (el) { + try { + let old = localStorage.getItem('adminPass'); + if (el.value == 'true' || (forceIfSet && old !== undefined)) { + localStorage.setItem('adminPass', pass); + } + else { + localStorage.removeItem('adminPass'); + } + } catch (e) { } + }); + } + buttonHandlers.forgetPass=function() { + localStorage.removeItem('adminPass'); + 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') }); + saveAdminPass(pe.value); + 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; + } + }); + } + buttonHandlers.converterInfo=function() { + getJson("api/converterInfo").then(function (json) { + let text = "

Converted entities

"; + text += "

NMEA0183 to NMEA2000:
"; + text += " " + (json.nmea0183 || "").replace(/,/g, ", "); + text += "

"; + text += "

NMEA2000 to NMEA0183:
"; + text += " " + (json.nmea2000 || "").replace(/,/g, ", "); + text += "

"; + showOverlay(text, true); + }); + } + function handleTab(el) { + let activeName = el.getAttribute('data-page'); + if (!activeName) { + let extUrl = el.getAttribute('data-url'); + if (!extUrl) return; + window.open(extUrl, el.getAttribute('data-window') || '_'); + } + let activeTab = document.getElementById(activeName); + if (!activeTab) return; + forEl('.tabPage',function(pel){ + pel.classList.add('hidden'); + }); + forEl('.tab',function(tel){ + tel.classList.remove('active'); + }); + el.classList.add('active'); + activeTab.classList.remove('hidden'); + callListeners(api.EVENTS.tab,activeName); + } + /** + * + * @param {number} coordinate + * @param axis + * @returns {string} + */ + function formatLonLatsDecimal(coordinate, axis) { + coordinate = (coordinate + 540) % 360 - 180; // normalize for sphere being round + + let abscoordinate = Math.abs(coordinate); + let coordinatedegrees = Math.floor(abscoordinate); + + let coordinateminutes = (abscoordinate - coordinatedegrees) / (1 / 60); + let numdecimal = 2; + //correctly handle the toFixed(x) - will do math rounding + if (coordinateminutes.toFixed(numdecimal) == 60) { + coordinatedegrees += 1; + coordinateminutes = 0; + } + if (coordinatedegrees < 10) { + coordinatedegrees = "0" + coordinatedegrees; + } + if (coordinatedegrees < 100 && axis == 'lon') { + coordinatedegrees = "0" + coordinatedegrees; + } + let str = coordinatedegrees + "\u00B0"; + + if (coordinateminutes < 10) { + str += "0"; + } + str += coordinateminutes.toFixed(numdecimal) + "'"; + if (axis == "lon") { + str += coordinate < 0 ? "W" : "E"; + } else { + str += coordinate < 0 ? "S" : "N"; + } + return str; + }; + function formatFixed(v, dig, fract) { + v = parseFloat(v); + if (dig === undefined) return v.toFixed(fract); + let s = v < 0 ? "-" : ""; + v = Math.abs(v); + let rv = v.toFixed(fract); + let parts = rv.split('.'); + parts[0] = "0000000000" + parts[0]; + if (dig >= 10) dig = 10; + if (fract > 0) { + return s + parts[0].substr(parts[0].length - dig) + "." + parts[1]; + } + return s + parts[0].substr(parts[0].length - dig); + } + let valueFormatters = { + formatCourse: { + f: function (v) { + let x = parseFloat(v); + let rt = x * 180.0 / Math.PI; + if (rt > 360) rt -= 360; + if (rt < 0) rt += 360; + return rt.toFixed(0); + }, + u: '°' + }, + formatKnots: { + f: function (v) { + let x = parseFloat(v); + x = x * 3600.0 / 1852.0; + return x.toFixed(2); + }, + u: 'kn' + }, + formatWind: { + f: function (v) { + let x = parseFloat(v); + x = x * 180.0 / Math.PI; + if (x > 180) x = -1 * (360 - x); + return x.toFixed(0); + }, + u: '°' + }, + mtr2nm: { + f: function (v) { + let x = parseFloat(v); + x = x / 1852.0; + return x.toFixed(2); + }, + u: 'nm' + }, + kelvinToC: { + f: function (v) { + let x = parseFloat(v); + x = x - 273.15; + return x.toFixed(0); + }, + u: '°' + }, + formatFixed0: { + f: function (v) { + let x = parseFloat(v); + return x.toFixed(0); + }, + u: '' + }, + formatDepth: { + f: function (v) { + let x = parseFloat(v); + return x.toFixed(1); + }, + u: 'm' + }, + formatLatitude: { + f: function (v) { + let x = parseFloat(v); + if (isNaN(x)) return '-----'; + return formatLonLatsDecimal(x, 'lat'); + }, + u: '' + }, + formatLongitude: { + f: function (v) { + let x = parseFloat(v); + if (isNaN(x)) return '-----'; + return formatLonLatsDecimal(x, 'lon'); + }, + u: '' + }, + formatRot: { + f: function (v) { + let x = parseFloat(v); + if (isNaN(x)) return '---'; + x = x * 180.0 / Math.PI; + return x.toFixed(2); + }, + u: '°/s' + }, + formatXte: { + f: function (v) { + let x = parseFloat(v); + if (isNaN(x)) return '---'; + return x.toFixed(0); + }, + u: 'm' + }, + formatDate: { + f: function (v) { + v = parseFloat(v); + if (isNaN(v)) return "----/--/--"; + //strange day offset from NMEA0183 lib + let d = new Date("2010/01/01"); + let days = 14610 - d.getTime() / 1000 / 86400; + let tbase = (v - days) * 1000 * 86400; + let od = new Date(tbase); + return formatFixed(od.getFullYear(), 4, 0) + + "/" + formatFixed(od.getMonth() + 1, 2, 0) + + "/" + formatFixed(od.getDate(), 2, 0); + }, + u: '' + }, + formatTime: { + f: function (v) { + v = parseFloat(v); + if (isNaN(v)) return "--:--:--"; + let hr = Math.floor(v / 3600.0); + let min = Math.floor((v - hr * 3600.0) / 60); + let s = Math.floor((v - hr * 3600.0 - min * 60.0)); + return formatFixed(hr, 2, 0) + ':' + + formatFixed(min, 2, 0) + ':' + + formatFixed(s, 2, 0); + }, + u: '' + } + + + } + Object.freeze(valueFormatters); + for (let k in valueFormatters){ + Object.freeze(valueFormatters[k]); + } + function resizeFont(el, reset, maxIt) { + if (maxIt === undefined) maxIt = 10; + if (!el) return; + if (reset) el.style.fontSize = ''; + while (el.scrollWidth > el.clientWidth && maxIt) { + let next = parseFloat(window.getComputedStyle(el).fontSize) * 0.9; + el.style.fontSize = next + "px"; + } + } + function getUnit(def,useUser){ + let fmt = useUser?(userFormatters[def.name] || valueFormatters[def.format]):valueFormatters[def.format] ; + let u = fmt ? fmt.u : ' '; + if (!fmt && def.format && def.format.match(/formatXdr/)) { + u = def.format.replace(/formatXdr:[^:]*:/, ''); + } + return u; + } + function sourceName(v) { + if (v == 0) return "N2K"; + for (let n in channelList) { + if (v == channelList[n].id) return n; + if (v >= channelList[n].id && v <= channelList[n].max) { + return n; + } + } + if (v < minUser) return "---"; + return "USER[" + v + "]"; + } + let lastSelectList = []; + buttonHandlers.uploadBin=function(ev) { + let el = document.getElementById("uploadFile"); + let progressEl = document.getElementById("uploadDone"); + if (!el) return; + if (el.files.length < 1) return; + ev.target.disabled = true; + let file = el.files[0]; + checkImageFile(file) + .then(function (result) { + let currentType; + let currentVersion; + let chipid; + forEl('.status-version', function (el) { currentVersion = el.textContent }); + forEl('.status-fwtype', function (el) { currentType = el.textContent }); + forEl('.status-chipid', function (el) { chipid = el.textContent }); + let confirmText = 'Ready to update firmware?\n'; + if (result.chipId != chipid) { + confirmText += "WARNING: the chipid in the image (" + result.chipId; + confirmText += ") does not match the current chip id (" + chipid + ").\n"; + } + if (currentType != result.fwtype) { + confirmText += "WARNING: image has different type: " + result.fwtype + "\n"; + confirmText += "** Really update anyway? - device can become unusable **"; + } + else { + if (currentVersion == result.version) { + confirmText += "WARNING: image has the same version as current " + result.version; + } + else { + confirmText += "version in image: " + result.version; + } + } + if (!confirm(confirmText)) { + ev.target.disabled = false; + return; + } + ensurePass() + .then(function (hash) { + let len = file.size; + let req = new XMLHttpRequest(); + req.onloadend = function () { + ev.target.disabled = false; + let result = "unknown error"; + try { + let jresult = JSON.parse(req.responseText); + if (jresult.status == 'OK') { + result = ''; + } + else { + if (jresult.status) { + result = jresult.status; + } + } + } catch (e) { + result = "Error " + req.status; + } + if (progressEl) { + progressEl.style.width = 0; + } + if (!result) { + alertRestart(); + } + else { + alert("update error: " + result); + } + } + req.onerror = function (e) { + ev.target.disabled = false; + alert("unable to upload: " + e); + } + if (progressEl) { + progressEl.style.width = 0; + req.upload.onprogress = function (ev) { + if (ev.lengthComputable) { + let percent = 100 * ev.loaded / ev.total; + progressEl.style.width = percent + "%"; + } + } + } + let formData = new FormData(); + formData.append("file1", el.files[0]); + req.open("POST", apiPrefix + '/api/update?_hash=' + encodeURIComponent(hash)); + req.send(formData); + }) + .catch(function (e) { + ev.target.disabled = false; + }); + }) + .catch(function (e) { + alert("This file is an invalid image file:\n" + e); + ev.target.disabled = false; + }) + } + let HDROFFSET = 288; + let VERSIONOFFSET = 16; + let NAMEOFFSET = 48; + let MINSIZE = HDROFFSET + NAMEOFFSET + 32; + let CHIPIDOFFSET = 12; //2 bytes chip id here + let imageCheckBytes = { + 0: 0xe9, //image magic + 288: 0x32, //app header magic + 289: 0x54, + 290: 0xcd, + 291: 0xab + }; + function decodeFromBuffer(buffer, start, length) { + while (length > 0 && buffer[start + length - 1] == 0) { + length--; + } + if (length <= 0) return ""; + let decoder = new TextDecoder(); + let rt = decoder.decode(buffer.slice( + start, + start + length)); + return rt; + } + function getChipId(buffer) { + if (buffer.length < CHIPIDOFFSET + 2) return -1; + return buffer[CHIPIDOFFSET] + 256 * buffer[CHIPIDOFFSET + 1]; + } + function checkImageFile(file) { + return new Promise(function (resolve, reject) { + if (!file) reject("no file"); + if (file.size < MINSIZE) reject("file is too small"); + let slice = file.slice(0, MINSIZE); + let reader = new FileReader(); + reader.addEventListener('load', function (e) { + let content = new Uint8Array(e.target.result); + for (let idx in imageCheckBytes) { + if (content[idx] != imageCheckBytes[idx]) { + reject("missing magic byte at position " + idx + ", expected " + + imageCheckBytes[idx] + ", got " + content[idx]); + } + } + let version = decodeFromBuffer(content, HDROFFSET + VERSIONOFFSET, 32); + let fwtype = decodeFromBuffer(content, HDROFFSET + NAMEOFFSET, 32); + let chipId = getChipId(content); + let rt = { + fwtype: fwtype, + version: version, + chipId: chipId + }; + resolve(rt); + }); + reader.readAsArrayBuffer(slice); + }); + } + function addTabPage(name,label,url){ + if (label === undefined) label=name; + let tab=addEl('div','tab','#tabs',label); + tab.addEventListener('click',function(ev){ + handleTab(ev.target); + }) + if (url !== undefined){ + tab.setAttribute('data-url',url); + return; + } + tab.setAttribute('data-page',name); + let page=addEl('div','tabPage hidden','#tabPages'); + page.setAttribute('id',name); + return page; + } + function addUserFormatter(name,unit,formatter){ + if (unit !== undefined && formatter !== undefined){ + userFormatters[name]={ + u:unit, + f:formatter + } + } + else{ + delete userFormatters[name]; + } + hideDashboardItem(name); //will recreate it on next data receive + } + const api= { + registerListener: function (callback,opt_event) { + if (opt_event === undefined){ + listeners.push(callback); + } + else{ + listeners.push({ + event:opt_event, + callback:callback + }) + } + }, + /** + * helper for creating dom elements + * parameters: + * type: the element type (e.g. div) + * class: a list of classes separated by space + * parent (opt): a parent element (either a dom element vor a query selector) + * text (opt): the text to be set as textContent + * returns: the newly created element + */ + addEl: addEl, + /** + * iterator helper for a query selector + * parameters: + * query: the query selector + * callback: the callback function (will be called with the element as param) + * base (opt): a dome element to be used as the root (defaults to document) + */ + forEl: forEl, + /** + * find the closest parent that has a particular class + * parameters: + * element: the element to start with + * class: the class to be searched for + * returns: the element or undefined/null + */ + closestParent: closestParent, + /** + * add a new tab + * parameters: + * name - the name of the page + * label (opt): the label for the new page + * returns: the newly created element + */ + addTabPage: addTabPage, + /** + * add a user defined formatter for a boat data item + * parameters: + * name : the boat data item name + * unit: the unit to be displayed + * formatter: the formatter function (must return a string) + */ + addUserFormatter: addUserFormatter, + removeUserFormatter: function(name){ + addUserFormatter(name); + }, + /** + * a dict of formatters + * each one has 2 members: + * u: the unit + * f: the formatter function + */ + formatters: valueFormatters, + EVENTS: { + init: 0, //called when capabilities are loaded, data is capabilities + tab: 1, //tab page activated, data is the id of the tab page + config: 2, //called when the config data is loaded,data is the config object + dataItemCreated: 4, //data is an object with + // name: the item name, element: the frame item of the boat data display + status: 5, //status received, data is the status object + counterDisplayCreated: 6 //data is the row for the display + } + }; + function callListeners(event,data){ + listeners.forEach((listener)=>{ + if (typeof(listener) === 'function'){ + listener(event,data); + } + else if (typeof(listener) === 'object'){ + if (listener.event === event){ + if (typeof(listener.callback) === 'function'){ + listener.callback(event,data); + } + } + } + }) + } + window.esp32nmea2k = api; + window.setInterval(update, updateInterval); + window.addEventListener('load', function () { + let buttons = document.querySelectorAll('button'); + for (let i = 0; i < buttons.length; i++) { + let be = buttons[i]; + let buttonFunction=buttonHandlers[be.id]; + if (typeof(buttonFunction) === 'function'){ + be.onclick = buttonFunction; //assume a function with the button id + //console.log("button: "+be.id); + } + else{ + console.log("no handler for button "+be.id); + } + } + forEl('.showMsgDetails', function (cd) { + cd.addEventListener('change', function (ev) { + let key = ev.target.getAttribute('data-key'); + if (!key) return; + let el = document.getElementById(key); + if (!el) return; + if (ev.target.checked) el.classList.remove('hidden'); + else (el.classList).add('hidden'); + }); + }); + let tabs = document.querySelectorAll('.tab'); + for (let i = 0; i < tabs.length; i++) { + tabs[i].addEventListener('click', function (ev) { + handleTab(ev.target); + }); + } + loadConfigDefinitions(); + try { + let storedPass = localStorage.getItem('adminPass'); + if (storedPass) { + forEl('#adminPassInput', function (el) { + el.value = storedPass; + }); + } + } catch (e) { } + forEl('#uploadFile', function (el) { + el.addEventListener('change', function (ev) { + if (ev.target.files.length < 1) return; + let file = ev.target.files[0]; + checkImageFile(file) + .then(function (res) { + forEl('#imageProperties', function (iel) { + let txt = "[" + res.chipId + "] "; + txt += res.fwtype + ", " + res.version; + iel.textContent = txt; + iel.classList.remove("error"); + }) + }) + .catch(function (e) { + forEl('#imageProperties', function (iel) { + iel.textContent = e; + iel.classList.add("error"); + }) + }) + }) + }) + }); +}()); + diff --git a/web/sha256.js b/web/sha256.js new file mode 100644 index 0000000..906663b --- /dev/null +++ b/web/sha256.js @@ -0,0 +1,526 @@ +/** + * [js-sha256]{@link https://github.com/emn178/js-sha256} + * + * @version 0.11.1 + * @author Chen, Yi-Cyuan [emn178@gmail.com] + * @copyright Chen, Yi-Cyuan 2014-2025 + * @license MIT + */ +/*jslint bitwise: true */ +(function () { + 'use strict'; + + var ERROR = 'input is invalid type'; + var WINDOW = typeof window === 'object'; + var root = WINDOW ? window : {}; + if (root.JS_SHA256_NO_WINDOW) { + WINDOW = false; + } + var WEB_WORKER = !WINDOW && typeof self === 'object'; + var NODE_JS = !root.JS_SHA256_NO_NODE_JS && typeof process === 'object' && process.versions && process.versions.node && process.type != 'renderer'; + if (NODE_JS) { + root = global; + } else if (WEB_WORKER) { + root = self; + } + var COMMON_JS = !root.JS_SHA256_NO_COMMON_JS && typeof module === 'object' && module.exports; + var AMD = typeof define === 'function' && define.amd; + var ARRAY_BUFFER = !root.JS_SHA256_NO_ARRAY_BUFFER && typeof ArrayBuffer !== 'undefined'; + var HEX_CHARS = '0123456789abcdef'.split(''); + var EXTRA = [-2147483648, 8388608, 32768, 128]; + var SHIFT = [24, 16, 8, 0]; + var K = [ + 0x428a2f98, 0x71374491, 0xb5c0fbcf, 0xe9b5dba5, 0x3956c25b, 0x59f111f1, 0x923f82a4, 0xab1c5ed5, + 0xd807aa98, 0x12835b01, 0x243185be, 0x550c7dc3, 0x72be5d74, 0x80deb1fe, 0x9bdc06a7, 0xc19bf174, + 0xe49b69c1, 0xefbe4786, 0x0fc19dc6, 0x240ca1cc, 0x2de92c6f, 0x4a7484aa, 0x5cb0a9dc, 0x76f988da, + 0x983e5152, 0xa831c66d, 0xb00327c8, 0xbf597fc7, 0xc6e00bf3, 0xd5a79147, 0x06ca6351, 0x14292967, + 0x27b70a85, 0x2e1b2138, 0x4d2c6dfc, 0x53380d13, 0x650a7354, 0x766a0abb, 0x81c2c92e, 0x92722c85, + 0xa2bfe8a1, 0xa81a664b, 0xc24b8b70, 0xc76c51a3, 0xd192e819, 0xd6990624, 0xf40e3585, 0x106aa070, + 0x19a4c116, 0x1e376c08, 0x2748774c, 0x34b0bcb5, 0x391c0cb3, 0x4ed8aa4a, 0x5b9cca4f, 0x682e6ff3, + 0x748f82ee, 0x78a5636f, 0x84c87814, 0x8cc70208, 0x90befffa, 0xa4506ceb, 0xbef9a3f7, 0xc67178f2 + ]; + var OUTPUT_TYPES = ['hex', 'array', 'digest', 'arrayBuffer']; + + var blocks = []; + + if (root.JS_SHA256_NO_NODE_JS || !Array.isArray) { + Array.isArray = function (obj) { + return Object.prototype.toString.call(obj) === '[object Array]'; + }; + } + + if (ARRAY_BUFFER && (root.JS_SHA256_NO_ARRAY_BUFFER_IS_VIEW || !ArrayBuffer.isView)) { + ArrayBuffer.isView = function (obj) { + return typeof obj === 'object' && obj.buffer && obj.buffer.constructor === ArrayBuffer; + }; + } + + var createOutputMethod = function (outputType, is224) { + return function (message) { + return new Sha256(is224, true).update(message)[outputType](); + }; + }; + + var createMethod = function (is224) { + var method = createOutputMethod('hex', is224); + if (NODE_JS) { + method = nodeWrap(method, is224); + } + method.create = function () { + return new Sha256(is224); + }; + method.update = function (message) { + return method.create().update(message); + }; + for (var i = 0; i < OUTPUT_TYPES.length; ++i) { + var type = OUTPUT_TYPES[i]; + method[type] = createOutputMethod(type, is224); + } + return method; + }; + + var nodeWrap = function (method, is224) { + var crypto = require('crypto') + var Buffer = require('buffer').Buffer; + var algorithm = is224 ? 'sha224' : 'sha256'; + var bufferFrom; + if (Buffer.from && !root.JS_SHA256_NO_BUFFER_FROM) { + bufferFrom = Buffer.from; + } else { + bufferFrom = function (message) { + return new Buffer(message); + }; + } + var nodeMethod = function (message) { + if (typeof message === 'string') { + return crypto.createHash(algorithm).update(message, 'utf8').digest('hex'); + } else { + if (message === null || message === undefined) { + throw new Error(ERROR); + } else if (message.constructor === ArrayBuffer) { + message = new Uint8Array(message); + } + } + if (Array.isArray(message) || ArrayBuffer.isView(message) || + message.constructor === Buffer) { + return crypto.createHash(algorithm).update(bufferFrom(message)).digest('hex'); + } else { + return method(message); + } + }; + return nodeMethod; + }; + + var createHmacOutputMethod = function (outputType, is224) { + return function (key, message) { + return new HmacSha256(key, is224, true).update(message)[outputType](); + }; + }; + + var createHmacMethod = function (is224) { + var method = createHmacOutputMethod('hex', is224); + method.create = function (key) { + return new HmacSha256(key, is224); + }; + method.update = function (key, message) { + return method.create(key).update(message); + }; + for (var i = 0; i < OUTPUT_TYPES.length; ++i) { + var type = OUTPUT_TYPES[i]; + method[type] = createHmacOutputMethod(type, is224); + } + return method; + }; + + function Sha256(is224, sharedMemory) { + if (sharedMemory) { + blocks[0] = blocks[16] = blocks[1] = blocks[2] = blocks[3] = + blocks[4] = blocks[5] = blocks[6] = blocks[7] = + blocks[8] = blocks[9] = blocks[10] = blocks[11] = + blocks[12] = blocks[13] = blocks[14] = blocks[15] = 0; + this.blocks = blocks; + } else { + this.blocks = [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]; + } + + if (is224) { + this.h0 = 0xc1059ed8; + this.h1 = 0x367cd507; + this.h2 = 0x3070dd17; + this.h3 = 0xf70e5939; + this.h4 = 0xffc00b31; + this.h5 = 0x68581511; + this.h6 = 0x64f98fa7; + this.h7 = 0xbefa4fa4; + } else { // 256 + this.h0 = 0x6a09e667; + this.h1 = 0xbb67ae85; + this.h2 = 0x3c6ef372; + this.h3 = 0xa54ff53a; + this.h4 = 0x510e527f; + this.h5 = 0x9b05688c; + this.h6 = 0x1f83d9ab; + this.h7 = 0x5be0cd19; + } + + this.block = this.start = this.bytes = this.hBytes = 0; + this.finalized = this.hashed = false; + this.first = true; + this.is224 = is224; + } + + Sha256.prototype.update = function (message) { + if (this.finalized) { + return; + } + var notString, type = typeof message; + if (type !== 'string') { + if (type === 'object') { + if (message === null) { + throw new Error(ERROR); + } else if (ARRAY_BUFFER && message.constructor === ArrayBuffer) { + message = new Uint8Array(message); + } else if (!Array.isArray(message)) { + if (!ARRAY_BUFFER || !ArrayBuffer.isView(message)) { + throw new Error(ERROR); + } + } + } else { + throw new Error(ERROR); + } + notString = true; + } + var code, index = 0, i, length = message.length, blocks = this.blocks; + while (index < length) { + if (this.hashed) { + this.hashed = false; + blocks[0] = this.block; + this.block = blocks[16] = blocks[1] = blocks[2] = blocks[3] = + blocks[4] = blocks[5] = blocks[6] = blocks[7] = + blocks[8] = blocks[9] = blocks[10] = blocks[11] = + blocks[12] = blocks[13] = blocks[14] = blocks[15] = 0; + } + + if (notString) { + for (i = this.start; index < length && i < 64; ++index) { + blocks[i >>> 2] |= message[index] << SHIFT[i++ & 3]; + } + } else { + for (i = this.start; index < length && i < 64; ++index) { + code = message.charCodeAt(index); + if (code < 0x80) { + blocks[i >>> 2] |= code << SHIFT[i++ & 3]; + } else if (code < 0x800) { + blocks[i >>> 2] |= (0xc0 | (code >>> 6)) << SHIFT[i++ & 3]; + blocks[i >>> 2] |= (0x80 | (code & 0x3f)) << SHIFT[i++ & 3]; + } else if (code < 0xd800 || code >= 0xe000) { + blocks[i >>> 2] |= (0xe0 | (code >>> 12)) << SHIFT[i++ & 3]; + blocks[i >>> 2] |= (0x80 | ((code >>> 6) & 0x3f)) << SHIFT[i++ & 3]; + blocks[i >>> 2] |= (0x80 | (code & 0x3f)) << SHIFT[i++ & 3]; + } else { + code = 0x10000 + (((code & 0x3ff) << 10) | (message.charCodeAt(++index) & 0x3ff)); + blocks[i >>> 2] |= (0xf0 | (code >>> 18)) << SHIFT[i++ & 3]; + blocks[i >>> 2] |= (0x80 | ((code >>> 12) & 0x3f)) << SHIFT[i++ & 3]; + blocks[i >>> 2] |= (0x80 | ((code >>> 6) & 0x3f)) << SHIFT[i++ & 3]; + blocks[i >>> 2] |= (0x80 | (code & 0x3f)) << SHIFT[i++ & 3]; + } + } + } + + this.lastByteIndex = i; + this.bytes += i - this.start; + if (i >= 64) { + this.block = blocks[16]; + this.start = i - 64; + this.hash(); + this.hashed = true; + } else { + this.start = i; + } + } + if (this.bytes > 4294967295) { + this.hBytes += this.bytes / 4294967296 << 0; + this.bytes = this.bytes % 4294967296; + } + return this; + }; + + Sha256.prototype.finalize = function () { + if (this.finalized) { + return; + } + this.finalized = true; + var blocks = this.blocks, i = this.lastByteIndex; + blocks[16] = this.block; + blocks[i >>> 2] |= EXTRA[i & 3]; + this.block = blocks[16]; + if (i >= 56) { + if (!this.hashed) { + this.hash(); + } + blocks[0] = this.block; + blocks[16] = blocks[1] = blocks[2] = blocks[3] = + blocks[4] = blocks[5] = blocks[6] = blocks[7] = + blocks[8] = blocks[9] = blocks[10] = blocks[11] = + blocks[12] = blocks[13] = blocks[14] = blocks[15] = 0; + } + blocks[14] = this.hBytes << 3 | this.bytes >>> 29; + blocks[15] = this.bytes << 3; + this.hash(); + }; + + Sha256.prototype.hash = function () { + var a = this.h0, b = this.h1, c = this.h2, d = this.h3, e = this.h4, f = this.h5, g = this.h6, + h = this.h7, blocks = this.blocks, j, s0, s1, maj, t1, t2, ch, ab, da, cd, bc; + + for (j = 16; j < 64; ++j) { + // rightrotate + t1 = blocks[j - 15]; + s0 = ((t1 >>> 7) | (t1 << 25)) ^ ((t1 >>> 18) | (t1 << 14)) ^ (t1 >>> 3); + t1 = blocks[j - 2]; + s1 = ((t1 >>> 17) | (t1 << 15)) ^ ((t1 >>> 19) | (t1 << 13)) ^ (t1 >>> 10); + blocks[j] = blocks[j - 16] + s0 + blocks[j - 7] + s1 << 0; + } + + bc = b & c; + for (j = 0; j < 64; j += 4) { + if (this.first) { + if (this.is224) { + ab = 300032; + t1 = blocks[0] - 1413257819; + h = t1 - 150054599 << 0; + d = t1 + 24177077 << 0; + } else { + ab = 704751109; + t1 = blocks[0] - 210244248; + h = t1 - 1521486534 << 0; + d = t1 + 143694565 << 0; + } + this.first = false; + } else { + s0 = ((a >>> 2) | (a << 30)) ^ ((a >>> 13) | (a << 19)) ^ ((a >>> 22) | (a << 10)); + s1 = ((e >>> 6) | (e << 26)) ^ ((e >>> 11) | (e << 21)) ^ ((e >>> 25) | (e << 7)); + ab = a & b; + maj = ab ^ (a & c) ^ bc; + ch = (e & f) ^ (~e & g); + t1 = h + s1 + ch + K[j] + blocks[j]; + t2 = s0 + maj; + h = d + t1 << 0; + d = t1 + t2 << 0; + } + s0 = ((d >>> 2) | (d << 30)) ^ ((d >>> 13) | (d << 19)) ^ ((d >>> 22) | (d << 10)); + s1 = ((h >>> 6) | (h << 26)) ^ ((h >>> 11) | (h << 21)) ^ ((h >>> 25) | (h << 7)); + da = d & a; + maj = da ^ (d & b) ^ ab; + ch = (h & e) ^ (~h & f); + t1 = g + s1 + ch + K[j + 1] + blocks[j + 1]; + t2 = s0 + maj; + g = c + t1 << 0; + c = t1 + t2 << 0; + s0 = ((c >>> 2) | (c << 30)) ^ ((c >>> 13) | (c << 19)) ^ ((c >>> 22) | (c << 10)); + s1 = ((g >>> 6) | (g << 26)) ^ ((g >>> 11) | (g << 21)) ^ ((g >>> 25) | (g << 7)); + cd = c & d; + maj = cd ^ (c & a) ^ da; + ch = (g & h) ^ (~g & e); + t1 = f + s1 + ch + K[j + 2] + blocks[j + 2]; + t2 = s0 + maj; + f = b + t1 << 0; + b = t1 + t2 << 0; + s0 = ((b >>> 2) | (b << 30)) ^ ((b >>> 13) | (b << 19)) ^ ((b >>> 22) | (b << 10)); + s1 = ((f >>> 6) | (f << 26)) ^ ((f >>> 11) | (f << 21)) ^ ((f >>> 25) | (f << 7)); + bc = b & c; + maj = bc ^ (b & d) ^ cd; + ch = (f & g) ^ (~f & h); + t1 = e + s1 + ch + K[j + 3] + blocks[j + 3]; + t2 = s0 + maj; + e = a + t1 << 0; + a = t1 + t2 << 0; + this.chromeBugWorkAround = true; + } + + this.h0 = this.h0 + a << 0; + this.h1 = this.h1 + b << 0; + this.h2 = this.h2 + c << 0; + this.h3 = this.h3 + d << 0; + this.h4 = this.h4 + e << 0; + this.h5 = this.h5 + f << 0; + this.h6 = this.h6 + g << 0; + this.h7 = this.h7 + h << 0; + }; + + Sha256.prototype.hex = function () { + this.finalize(); + + var h0 = this.h0, h1 = this.h1, h2 = this.h2, h3 = this.h3, h4 = this.h4, h5 = this.h5, + h6 = this.h6, h7 = this.h7; + + var hex = HEX_CHARS[(h0 >>> 28) & 0x0F] + HEX_CHARS[(h0 >>> 24) & 0x0F] + + HEX_CHARS[(h0 >>> 20) & 0x0F] + HEX_CHARS[(h0 >>> 16) & 0x0F] + + HEX_CHARS[(h0 >>> 12) & 0x0F] + HEX_CHARS[(h0 >>> 8) & 0x0F] + + HEX_CHARS[(h0 >>> 4) & 0x0F] + HEX_CHARS[h0 & 0x0F] + + HEX_CHARS[(h1 >>> 28) & 0x0F] + HEX_CHARS[(h1 >>> 24) & 0x0F] + + HEX_CHARS[(h1 >>> 20) & 0x0F] + HEX_CHARS[(h1 >>> 16) & 0x0F] + + HEX_CHARS[(h1 >>> 12) & 0x0F] + HEX_CHARS[(h1 >>> 8) & 0x0F] + + HEX_CHARS[(h1 >>> 4) & 0x0F] + HEX_CHARS[h1 & 0x0F] + + HEX_CHARS[(h2 >>> 28) & 0x0F] + HEX_CHARS[(h2 >>> 24) & 0x0F] + + HEX_CHARS[(h2 >>> 20) & 0x0F] + HEX_CHARS[(h2 >>> 16) & 0x0F] + + HEX_CHARS[(h2 >>> 12) & 0x0F] + HEX_CHARS[(h2 >>> 8) & 0x0F] + + HEX_CHARS[(h2 >>> 4) & 0x0F] + HEX_CHARS[h2 & 0x0F] + + HEX_CHARS[(h3 >>> 28) & 0x0F] + HEX_CHARS[(h3 >>> 24) & 0x0F] + + HEX_CHARS[(h3 >>> 20) & 0x0F] + HEX_CHARS[(h3 >>> 16) & 0x0F] + + HEX_CHARS[(h3 >>> 12) & 0x0F] + HEX_CHARS[(h3 >>> 8) & 0x0F] + + HEX_CHARS[(h3 >>> 4) & 0x0F] + HEX_CHARS[h3 & 0x0F] + + HEX_CHARS[(h4 >>> 28) & 0x0F] + HEX_CHARS[(h4 >>> 24) & 0x0F] + + HEX_CHARS[(h4 >>> 20) & 0x0F] + HEX_CHARS[(h4 >>> 16) & 0x0F] + + HEX_CHARS[(h4 >>> 12) & 0x0F] + HEX_CHARS[(h4 >>> 8) & 0x0F] + + HEX_CHARS[(h4 >>> 4) & 0x0F] + HEX_CHARS[h4 & 0x0F] + + HEX_CHARS[(h5 >>> 28) & 0x0F] + HEX_CHARS[(h5 >>> 24) & 0x0F] + + HEX_CHARS[(h5 >>> 20) & 0x0F] + HEX_CHARS[(h5 >>> 16) & 0x0F] + + HEX_CHARS[(h5 >>> 12) & 0x0F] + HEX_CHARS[(h5 >>> 8) & 0x0F] + + HEX_CHARS[(h5 >>> 4) & 0x0F] + HEX_CHARS[h5 & 0x0F] + + HEX_CHARS[(h6 >>> 28) & 0x0F] + HEX_CHARS[(h6 >>> 24) & 0x0F] + + HEX_CHARS[(h6 >>> 20) & 0x0F] + HEX_CHARS[(h6 >>> 16) & 0x0F] + + HEX_CHARS[(h6 >>> 12) & 0x0F] + HEX_CHARS[(h6 >>> 8) & 0x0F] + + HEX_CHARS[(h6 >>> 4) & 0x0F] + HEX_CHARS[h6 & 0x0F]; + if (!this.is224) { + hex += HEX_CHARS[(h7 >>> 28) & 0x0F] + HEX_CHARS[(h7 >>> 24) & 0x0F] + + HEX_CHARS[(h7 >>> 20) & 0x0F] + HEX_CHARS[(h7 >>> 16) & 0x0F] + + HEX_CHARS[(h7 >>> 12) & 0x0F] + HEX_CHARS[(h7 >>> 8) & 0x0F] + + HEX_CHARS[(h7 >>> 4) & 0x0F] + HEX_CHARS[h7 & 0x0F]; + } + return hex; + }; + + Sha256.prototype.toString = Sha256.prototype.hex; + + Sha256.prototype.digest = function () { + this.finalize(); + + var h0 = this.h0, h1 = this.h1, h2 = this.h2, h3 = this.h3, h4 = this.h4, h5 = this.h5, + h6 = this.h6, h7 = this.h7; + + var arr = [ + (h0 >>> 24) & 0xFF, (h0 >>> 16) & 0xFF, (h0 >>> 8) & 0xFF, h0 & 0xFF, + (h1 >>> 24) & 0xFF, (h1 >>> 16) & 0xFF, (h1 >>> 8) & 0xFF, h1 & 0xFF, + (h2 >>> 24) & 0xFF, (h2 >>> 16) & 0xFF, (h2 >>> 8) & 0xFF, h2 & 0xFF, + (h3 >>> 24) & 0xFF, (h3 >>> 16) & 0xFF, (h3 >>> 8) & 0xFF, h3 & 0xFF, + (h4 >>> 24) & 0xFF, (h4 >>> 16) & 0xFF, (h4 >>> 8) & 0xFF, h4 & 0xFF, + (h5 >>> 24) & 0xFF, (h5 >>> 16) & 0xFF, (h5 >>> 8) & 0xFF, h5 & 0xFF, + (h6 >>> 24) & 0xFF, (h6 >>> 16) & 0xFF, (h6 >>> 8) & 0xFF, h6 & 0xFF + ]; + if (!this.is224) { + arr.push((h7 >>> 24) & 0xFF, (h7 >>> 16) & 0xFF, (h7 >>> 8) & 0xFF, h7 & 0xFF); + } + return arr; + }; + + Sha256.prototype.array = Sha256.prototype.digest; + + Sha256.prototype.arrayBuffer = function () { + this.finalize(); + + var buffer = new ArrayBuffer(this.is224 ? 28 : 32); + var dataView = new DataView(buffer); + dataView.setUint32(0, this.h0); + dataView.setUint32(4, this.h1); + dataView.setUint32(8, this.h2); + dataView.setUint32(12, this.h3); + dataView.setUint32(16, this.h4); + dataView.setUint32(20, this.h5); + dataView.setUint32(24, this.h6); + if (!this.is224) { + dataView.setUint32(28, this.h7); + } + return buffer; + }; + + function HmacSha256(key, is224, sharedMemory) { + var i, type = typeof key; + if (type === 'string') { + var bytes = [], length = key.length, index = 0, code; + for (i = 0; i < length; ++i) { + code = key.charCodeAt(i); + if (code < 0x80) { + bytes[index++] = code; + } else if (code < 0x800) { + bytes[index++] = (0xc0 | (code >>> 6)); + bytes[index++] = (0x80 | (code & 0x3f)); + } else if (code < 0xd800 || code >= 0xe000) { + bytes[index++] = (0xe0 | (code >>> 12)); + bytes[index++] = (0x80 | ((code >>> 6) & 0x3f)); + bytes[index++] = (0x80 | (code & 0x3f)); + } else { + code = 0x10000 + (((code & 0x3ff) << 10) | (key.charCodeAt(++i) & 0x3ff)); + bytes[index++] = (0xf0 | (code >>> 18)); + bytes[index++] = (0x80 | ((code >>> 12) & 0x3f)); + bytes[index++] = (0x80 | ((code >>> 6) & 0x3f)); + bytes[index++] = (0x80 | (code & 0x3f)); + } + } + key = bytes; + } else { + if (type === 'object') { + if (key === null) { + throw new Error(ERROR); + } else if (ARRAY_BUFFER && key.constructor === ArrayBuffer) { + key = new Uint8Array(key); + } else if (!Array.isArray(key)) { + if (!ARRAY_BUFFER || !ArrayBuffer.isView(key)) { + throw new Error(ERROR); + } + } + } else { + throw new Error(ERROR); + } + } + + if (key.length > 64) { + key = (new Sha256(is224, true)).update(key).array(); + } + + var oKeyPad = [], iKeyPad = []; + for (i = 0; i < 64; ++i) { + var b = key[i] || 0; + oKeyPad[i] = 0x5c ^ b; + iKeyPad[i] = 0x36 ^ b; + } + + Sha256.call(this, is224, sharedMemory); + + this.update(iKeyPad); + this.oKeyPad = oKeyPad; + this.inner = true; + this.sharedMemory = sharedMemory; + } + HmacSha256.prototype = new Sha256(); + + HmacSha256.prototype.finalize = function () { + Sha256.prototype.finalize.call(this); + if (this.inner) { + this.inner = false; + var innerHash = this.array(); + Sha256.call(this, this.is224, this.sharedMemory); + this.update(this.oKeyPad); + this.update(innerHash); + Sha256.prototype.finalize.call(this); + } + }; + + var exports = createMethod(); + exports.sha256 = exports; + exports.sha224 = createMethod(true); + exports.sha256.hmac = createHmacMethod(); + exports.sha224.hmac = createHmacMethod(true); + + if (COMMON_JS) { + module.exports = exports; + } else { + root.sha256 = exports.sha256; + root.sha224 = exports.sha224; + if (AMD) { + define(function () { + return exports; + }); + } + } +})();