diff --git a/.github/workflows/beta.yml b/.github/workflows/beta.yml index 491d865..08c5df7 100644 --- a/.github/workflows/beta.yml +++ b/.github/workflows/beta.yml @@ -13,7 +13,7 @@ jobs: fail-fast: false matrix: board: [esp32, esp32-s3, esp32-s3-oct, esp32-c3, esp32-c6, esp32-h2, esp32-solo1] - build: [release, debug] + build: [release] env: BOARD: ${{ matrix.board }} VARIANT: ${{ matrix.name || matrix.board }} @@ -107,24 +107,16 @@ jobs: with: path: release pattern: '*-release-assets' - - name: Download debug assets - uses: actions/download-artifact@v4 - with: - path: debug - pattern: '*-debug-assets' - name: Copy binaries to ota/beta env: Version: ${{ github.run_id }}.${{ github.run_number }}.${{ github.run_attempt }} run: | mkdir -p ota/beta/ - mkdir -p ota/debug/beta/ mkdir -p ota/master/ - mkdir -p ota/debug/master/ mkdir -p webflash/ mkdir -p resources/ mkdir -p src/ cp -vf release/*/nuki_hub_*.bin ota/beta/ - cp -vf debug/*/nuki_hub_*.bin ota/debug/beta/ cp -vf master/resources/ota_manifest.py resources/ota_manifest.py cp -vf master/src/Config.h src/Config.h python3 resources/ota_manifest.py beta $Version @@ -132,14 +124,12 @@ jobs: rm -rf .github .gitignore .gitmodules touch ota/beta/empty touch ota/master/empty - touch ota/debug/beta/empty - touch ota/debug/master/empty touch webflash/empty - name: Commit binaries to binary uses: stefanzweifel/git-auto-commit-action@v5 with: commit_message: "Update binaries" - file_pattern: 'ota/* ota/debug/* ota/master/* ota/debug/master/* ota/beta/* ota/debug/beta/* webflash/*' + file_pattern: 'ota/* ota/master/* ota/beta/* webflash/*' branch: binary skip_dirty_check: true skip_fetch: true diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 6305dc1..6c9b069 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -19,7 +19,7 @@ jobs: fail-fast: false matrix: board: [esp32, esp32-s3, esp32-s3-oct, esp32-c3, esp32-c6, esp32-h2, esp32-solo1] - build: [release, debug] + build: [release] env: BOARD: ${{ matrix.board }} VARIANT: ${{ matrix.name || matrix.board }} diff --git a/.github/workflows/nightly.yml b/.github/workflows/nightly.yml index 652ee9d..58073db 100644 --- a/.github/workflows/nightly.yml +++ b/.github/workflows/nightly.yml @@ -35,7 +35,7 @@ jobs: fail-fast: false matrix: board: [esp32, esp32-s3, esp32-s3-oct, esp32-c3, esp32-c6, esp32-h2, esp32-solo1] - build: [release, debug] + build: [release] env: BOARD: ${{ matrix.board }} VARIANT: ${{ matrix.name || matrix.board }} @@ -130,19 +130,12 @@ jobs: with: path: release pattern: '*-release-assets' - - name: Download debug assets - uses: actions/download-artifact@v4 - with: - path: debug - pattern: '*-debug-assets' - name: Copy binaries to ota/master env: Version: ${{ github.run_id }}.${{ github.run_number }}.${{ github.run_attempt }} run: | mkdir -p ota/beta/ - mkdir -p ota/debug/beta/ mkdir -p ota/master/ - mkdir -p ota/debug/master/ mkdir -p webflash/ mkdir -p resources/ mkdir -p src/ @@ -155,14 +148,12 @@ jobs: rm -rf .github .gitignore .gitmodules touch ota/beta/empty touch ota/master/empty - touch ota/debug/beta/empty - touch ota/debug/master/empty touch webflash/empty - name: Commit binaries to binary uses: stefanzweifel/git-auto-commit-action@v5 with: commit_message: "Update binaries" - file_pattern: 'ota/* ota/debug/* ota/master/* ota/debug/master/* ota/beta/* ota/debug/beta/* webflash/*' + file_pattern: 'ota/* ota/master/* ota/beta/* webflash/*' branch: binary skip_dirty_check: true skip_fetch: true diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index d1765e0..3c8b957 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -13,7 +13,7 @@ jobs: fail-fast: false matrix: board: [esp32, esp32-s3, esp32-s3-oct, esp32-c3, esp32-c6, esp32-h2, esp32-solo1] - build: [release, debug] + build: [release] env: BOARD: ${{ matrix.board }} VARIANT: ${{ matrix.name || matrix.board }} @@ -109,11 +109,6 @@ jobs: with: path: release pattern: '*-release-assets' - - name: Download debug assets - uses: actions/download-artifact@v4 - with: - path: debug - pattern: '*-debug-assets' - name: Build zip archives id: zip env: @@ -171,14 +166,11 @@ jobs: Version: ${{ github.run_id }}.${{ github.run_number }}.${{ github.run_attempt }} run: | mkdir -p ota/beta/ - mkdir -p ota/debug/beta/ mkdir -p ota/master/ - mkdir -p ota/debug/master/ mkdir -p webflash/ mkdir -p resources/ mkdir -p src/ cp -vf release/*/nuki_hub_*.bin ota/ - cp -vf debug/*/nuki_hub_*.bin ota/debug/ cp -vf release/*/webflash_nuki_hub_*.bin webflash/ cp -vf master/resources/ota_manifest.py resources/ota_manifest.py cp -vf master/src/Config.h src/Config.h @@ -186,18 +178,15 @@ jobs: python3 resources/ota_manifest.py beta none find * -not -path "ota*" -not -path "webflash*" -delete rm -rf ota/beta/*.bin - rm -rf ota/debug/beta/*.bin rm -rf .github .gitignore .gitmodules touch ota/beta/empty touch ota/master/empty - touch ota/debug/beta/empty - touch ota/debug/master/empty touch webflash/empty - name: Commit binaries to binary uses: stefanzweifel/git-auto-commit-action@v5 with: commit_message: "Update binaries" - file_pattern: 'ota/* ota/debug/* ota/master/* ota/debug/master/* ota/beta/* ota/debug/beta/* webflash/*' + file_pattern: 'ota/* ota/master/* ota/beta/* webflash/*' branch: binary skip_dirty_check: true skip_fetch: true diff --git a/README.md b/README.md index 3a02e5f..a9235b8 100644 --- a/README.md +++ b/README.md @@ -312,7 +312,7 @@ In a browser navigate to the IP address assigned to the ESP32. - User: Pick a username to enable HTTP authentication for the Web Configuration, Set to "#" to disable authentication. - Password/Retype password: Pick a password to enable HTTP authentication for the Web Configuration. -- Use Digest Authentication (more secure): Enable to use HTTP Digest Authentication instead of HTTP Basic Authentication. Digest authentication is more secure, especially over unencrypted (HTTP) connections. +- HTTP Authentication type: Select from Basic, Digest or Form based authentication. Digest authentication is more secure than Basic or Form based authentication, especially over unencrypted (HTTP) connections. Form based authentication works best with password managers. Note: Firefox seems to have issues with basic authentication. #### Nuki Lock PIN / Nuki Opener PIN diff --git a/lib/PsychicHttp/src/PsychicRequest.cpp b/lib/PsychicHttp/src/PsychicRequest.cpp index e4272d0..9f0b921 100644 --- a/lib/PsychicHttp/src/PsychicRequest.cpp +++ b/lib/PsychicHttp/src/PsychicRequest.cpp @@ -303,7 +303,7 @@ String PsychicRequest::getCookie(const char* key) // how big is our cookie? size_t size; - if (!hasCookie("counter", &size)) + if (!hasCookie(key, &size)) return cookie; // allocate cookie buffer... keep it on the stack diff --git a/lib/nuki_ble b/lib/nuki_ble index a579d58..cf762e7 160000 --- a/lib/nuki_ble +++ b/lib/nuki_ble @@ -1 +1 @@ -Subproject commit a579d58a1c0d3d6119a780f4376b77046795cc16 +Subproject commit cf762e7dbd6ed501e9b719a7a4b9590ec85c7f99 diff --git a/sdkconfig.defaults b/sdkconfig.defaults index c3091fa..50c7787 100644 --- a/sdkconfig.defaults +++ b/sdkconfig.defaults @@ -120,4 +120,5 @@ CONFIG_ESP_WIFI_IRAM_OPT=n CONFIG_ESP_WIFI_RX_IRAM_OPT=n CONFIG_MBEDTLS_DYNAMIC_BUFFER=y CONFIG_LWIP_DHCP_GET_NTP_SRV=y -CONFIG_LWIP_SNTP_UPDATE_DELAY=43200000 \ No newline at end of file +CONFIG_LWIP_SNTP_UPDATE_DELAY=43200000 +CONFIG_LWIP_SNTP_MAX_SERVERS=3 \ No newline at end of file diff --git a/src/Config.h b/src/Config.h index ec1e067..de392ab 100644 --- a/src/Config.h +++ b/src/Config.h @@ -5,7 +5,7 @@ #define NUKI_HUB_VERSION "9.08" #define NUKI_HUB_VERSION_INT (uint32_t)908 #define NUKI_HUB_BUILD "unknownbuildnr" -#define NUKI_HUB_DATE "2025-01-15" +#define NUKI_HUB_DATE "2025-01-17" #define GITHUB_LATEST_RELEASE_URL (char*)"https://github.com/technyon/nuki_hub/releases/latest" #define GITHUB_OTA_MANIFEST_URL (char*)"https://raw.githubusercontent.com/technyon/nuki_hub/binary/ota/manifest.json" diff --git a/src/PreferencesKeys.h b/src/PreferencesKeys.h index e99f3cb..1b0b60f 100644 --- a/src/PreferencesKeys.h +++ b/src/PreferencesKeys.h @@ -226,6 +226,7 @@ inline void initPreferences(Preferences* preferences) preferences->putInt(preference_query_interval_configuration, 3600); preferences->putInt(preference_query_interval_battery, 1800); preferences->putInt(preference_query_interval_keypad, 1800); + preferences->putInt(preference_http_auth_type, 0); preferences->putBool(preference_debug_connect, false); preferences->putBool(preference_debug_communication, false); @@ -233,7 +234,6 @@ inline void initPreferences(Preferences* preferences) preferences->putBool(preference_debug_hex_data, false); preferences->putBool(preference_debug_command, false); preferences->putBool(preference_connect_mode, true); - preferences->putBool(preference_http_auth_type, false); preferences->putBool(preference_retain_gpio, false); preferences->putBool(preference_enable_debug_mode, false); @@ -508,7 +508,7 @@ private: preference_publish_authdata, preference_publish_debug_info, preference_official_hybrid_enabled, preference_mqtt_hass_enabled, preference_retain_gpio, preference_official_hybrid_actions, preference_official_hybrid_retry, preference_conf_info_enabled, preference_disable_non_json, preference_update_from_mqtt, preference_auth_control_enabled, preference_auth_topic_per_entry, preference_auth_info_enabled, preference_webserial_enabled, preference_hass_device_discovery, - preference_ntw_reconfigure, preference_keypad_check_code_enabled, preference_disable_network_not_connected, preference_find_best_rssi, preference_http_auth_type, + preference_ntw_reconfigure, preference_keypad_check_code_enabled, preference_disable_network_not_connected, preference_find_best_rssi, preference_debug_connect, preference_debug_communication, preference_debug_readable_data, preference_debug_hex_data, preference_debug_command, preference_connect_mode, preference_lock_force_id, preference_lock_force_doorsensor, preference_lock_force_keypad, preference_opener_force_id, preference_opener_force_keypad, preference_mqtt_ssl_enabled, preference_hybrid_reboot_on_disconnect, preference_lock_gemini_enabled, preference_enable_debug_mode @@ -528,7 +528,7 @@ private: preference_task_size_network, preference_task_size_nuki, preference_authlog_max_entries, preference_keypad_max_entries, preference_timecontrol_max_entries, preference_ble_tx_power, preference_network_custom_mdc, preference_network_custom_clk, preference_network_custom_phy, preference_network_custom_addr, preference_network_custom_irq, preference_network_custom_rst, preference_network_custom_cs, preference_network_custom_sck, preference_network_custom_miso, - preference_network_custom_mosi, preference_network_custom_pwr, preference_network_custom_mdio + preference_network_custom_mosi, preference_network_custom_pwr, preference_network_custom_mdio, preference_http_auth_type }; std::vector _uintPrefs = { diff --git a/src/WebCfgServer.cpp b/src/WebCfgServer.cpp index d3ac43c..8f91996 100644 --- a/src/WebCfgServer.cpp +++ b/src/WebCfgServer.cpp @@ -56,8 +56,12 @@ WebCfgServer::WebCfgServer(NukiNetwork* network, Preferences* preferences, bool str = _preferences->getString(preference_cred_password, ""); const char *pass = str.c_str(); memcpy(&_credPassword, pass, str.length()); - } + if (_preferences->getInt(preference_http_auth_type, 0) == 2) + { + loadSessions(); + } + } _confirmCode = generateConfirmCode(); #ifndef NUKI_HUB_UPDATER @@ -76,41 +80,115 @@ WebCfgServer::WebCfgServer(NukiNetwork* network, Preferences* preferences, bool #endif } -void WebCfgServer::initialize() +bool WebCfgServer::isAuthenticated(PsychicRequest *request) { - _psychicServer->onOpen([&](PsychicClient* client) { Log->printf("[http] connection #%u connected from %s\n", client->socket(), client->localIP().toString().c_str()); }); - _psychicServer->onClose([&](PsychicClient* client) { Log->printf("[http] connection #%u closed from %s\n", client->socket(), client->localIP().toString().c_str()); }); + if (request->hasCookie("sessionId")) { + String cookie = request->getCookie("sessionId"); - HTTPAuthMethod auth_type = BASIC_AUTH; - if (_preferences->getBool(preference_http_auth_type, false)) + if (_httpSessions[cookie].is()) + { + struct timeval time; + gettimeofday(&time, NULL); + int64_t time_us = (int64_t)time.tv_sec * 1000000L + (int64_t)time.tv_usec; + + if (_httpSessions[cookie].as() > time_us) + { + return true; + } + else + { + Log->println("Cookie found, but not valid anymore"); + } + } + } + Log->println("Authentication Failed"); + return false; +} + +esp_err_t WebCfgServer::logoutSession(PsychicRequest *request, PsychicResponse* resp) +{ + Log->print("Logging out"); + resp->setCookie("sessionId", "", 0, "HttpOnly"); + + if (request->hasCookie("sessionId")) { + String cookie = request->getCookie("sessionId"); + _httpSessions.remove(cookie); + saveSessions(); + } + else { - auth_type = DIGEST_AUTH; + Log->print("No session cookie found"); } - _psychicServer->on("/", HTTP_GET, [&](PsychicRequest *request, PsychicResponse* resp) - { - if(strlen(_credUser) > 0 && strlen(_credPassword) > 0 && !request->authenticate(_credUser, _credPassword)) - { - return request->requestAuthentication(auth_type, "Nuki Hub", "You must log in."); - } + return buildConfirmHtml(request, resp, "Logging out", 3, true); +} -#ifndef CONFIG_IDF_TARGET_ESP32H2 - if(!_network->isApOpen()) - { -#endif -#ifndef NUKI_HUB_UPDATER - return buildHtml(request, resp); -#else - return buildOtaHtml(request, resp); -#endif -#ifndef CONFIG_IDF_TARGET_ESP32H2 +void WebCfgServer::saveSessions() +{ + if(_preferences->getBool(preference_update_time, false)) + { + if (!SPIFFS.begin(true)) { + Log->println("SPIFFS Mount Failed"); + } + else + { + File file = SPIFFS.open("/sessions.json", "w"); + serializeJson(_httpSessions, file); + file.close(); + } + } +} +void WebCfgServer::loadSessions() +{ + if(_preferences->getBool(preference_update_time, false)) + { + if (!SPIFFS.begin(true)) { + Log->println("SPIFFS Mount Failed"); } else { - return buildWifiConnectHtml(request, resp); + File file = SPIFFS.open("/sessions.json", "r"); + + if (!file || file.isDirectory()) { + Log->println("sessions.json not found"); + } + else + { + deserializeJson(_httpSessions, file); + } + file.close(); } -#endif - }); + } +} + +int WebCfgServer::doAuthentication(PsychicRequest *request) +{ + if(strlen(_credUser) > 0 && strlen(_credPassword) > 0) + { + int savedAuthType = _preferences->getInt(preference_http_auth_type, 0); + if (savedAuthType == 2) + { + if (!isAuthenticated(request)) { + return savedAuthType; + } + } + else + { + if (!request->authenticate(_credUser, _credPassword)) + { + return savedAuthType; + } + } + } + + return 4; +} + +void WebCfgServer::initialize() +{ + //_psychicServer->onOpen([&](PsychicClient* client) { Log->printf("[http] connection #%u connected from %s\n", client->socket(), client->localIP().toString().c_str()); }); + //_psychicServer->onClose([&](PsychicClient* client) { Log->printf("[http] connection #%u closed from %s\n", client->socket(), client->localIP().toString().c_str()); }); + _psychicServer->on("/style.css", HTTP_GET, [&](PsychicRequest *request, PsychicResponse* resp) { return sendCss(request, resp); @@ -129,9 +207,23 @@ void WebCfgServer::initialize() }); _psychicServer->on("/savewifi", HTTP_POST, [&](PsychicRequest *request, PsychicResponse* resp) { - if(strlen(_credUser) > 0 && strlen(_credPassword) > 0 && !request->authenticate(_credUser, _credPassword)) + int authReq = doAuthentication(request); + + switch (authReq) { - return request->requestAuthentication(auth_type, "Nuki Hub", "You must log in."); + case 0: + return request->requestAuthentication(BASIC_AUTH, "Nuki Hub", "You must log in."); + break; + case 1: + return request->requestAuthentication(DIGEST_AUTH, "Nuki Hub", "You must log in."); + break; + case 2: + resp->setCode(302); + resp->addHeader("Cache-Control", "no-cache"); + return resp->redirect("/get?page=login"); + case 4: + default: + break; } String message = ""; @@ -148,9 +240,23 @@ void WebCfgServer::initialize() }); _psychicServer->on("/reboot", HTTP_GET, [&](PsychicRequest *request, PsychicResponse* resp) { - if(strlen(_credUser) > 0 && strlen(_credPassword) > 0 && !request->authenticate(_credUser, _credPassword)) + int authReq = doAuthentication(request); + + switch (authReq) { - return request->requestAuthentication(auth_type, "Nuki Hub", "You must log in."); + case 0: + return request->requestAuthentication(BASIC_AUTH, "Nuki Hub", "You must log in."); + break; + case 1: + return request->requestAuthentication(DIGEST_AUTH, "Nuki Hub", "You must log in."); + break; + case 2: + resp->setCode(302); + resp->addHeader("Cache-Control", "no-cache"); + return resp->redirect("/get?page=login"); + case 4: + default: + break; } String value = ""; @@ -169,6 +275,8 @@ void WebCfgServer::initialize() if(value != _confirmCode) { + resp->setCode(302); + resp->addHeader("Cache-Control", "no-cache"); return resp->redirect("/"); } esp_err_t res = buildConfirmHtml(request, resp, "Rebooting...", 2, true); @@ -191,16 +299,44 @@ void WebCfgServer::initialize() value = p->value(); } } - - if (value != "status") + int authReq = doAuthentication(request); + + if (value != "status" && value != "login" && value != "logout") { - if(strlen(_credUser) > 0 && strlen(_credPassword) > 0 && !request->authenticate(_credUser, _credPassword)) + switch (authReq) { - return request->requestAuthentication(auth_type, "Nuki Hub", "You must log in."); + case 0: + return request->requestAuthentication(BASIC_AUTH, "Nuki Hub", "You must log in."); + break; + case 1: + return request->requestAuthentication(DIGEST_AUTH, "Nuki Hub", "You must log in."); + break; + case 2: + resp->setCode(302); + resp->addHeader("Cache-Control", "no-cache"); + return resp->redirect("/get?page=login"); + case 4: + default: + break; } } + else if (value == "status" && authReq != 4) + { + resp->setCode(200); + resp->setContentType("application/json"); + resp->setContent("{}"); + return resp->send(); + } - if (value == "reboot") + if (value == "login") + { + return buildLoginHtml(request, resp); + } + else if (value == "logout") + { + return logoutSession(request, resp); + } + else if (value == "reboot") { String value = ""; if(request->hasParam("CONFIRMTOKEN")) @@ -218,6 +354,8 @@ void WebCfgServer::initialize() if(value != _confirmCode) { + resp->setCode(302); + resp->addHeader("Cache-Control", "no-cache"); return resp->redirect("/"); } esp_err_t res = buildConfirmHtml(request, resp, "Rebooting...", 2, true); @@ -250,18 +388,7 @@ void WebCfgServer::initialize() } else if (value == "status") { - if(request->hasParam("token")) - { - const PsychicWebParameter* p2 = request->getParam("token"); - if(p2->value().toInt() == _randomInt) - { - return buildStatusHtml(request, resp); - } - } - resp->setCode(200); - resp->setContentType("text/html"); - resp->setContent(""); - return resp->send(); + return buildStatusHtml(request, resp); } else if (value == "acclvl") { @@ -337,6 +464,8 @@ void WebCfgServer::initialize() } if(value != _confirmCode) { + resp->setCode(302); + resp->addHeader("Cache-Control", "no-cache"); return resp->redirect("/"); } if(!_allowRestartToPortal) @@ -356,7 +485,8 @@ void WebCfgServer::initialize() } else if (value == "otadebug") { - return buildOtaHtml(request, resp, true); + return buildOtaHtml(request, resp); + //return buildOtaHtml(request, resp, true); } else if (value == "reboottoota") { @@ -376,6 +506,8 @@ void WebCfgServer::initialize() if(value != _confirmCode) { + resp->setCode(302); + resp->addHeader("Cache-Control", "no-cache"); return resp->redirect("/"); } esp_err_t res = buildConfirmHtml(request, resp, "Rebooting to other partition...", 2, true); @@ -389,36 +521,21 @@ void WebCfgServer::initialize() #ifndef NUKI_HUB_UPDATER return processUpdate(request, resp); #else + resp->setCode(302); + resp->addHeader("Cache-Control", "no-cache"); return resp->redirect("/"); #endif } else { - #ifndef CONFIG_IDF_TARGET_ESP32H2 - if(!_network->isApOpen()) - { - #endif - #ifndef NUKI_HUB_UPDATER - return buildHtml(request, resp); - #else - return buildOtaHtml(request, resp); - #endif - #ifndef CONFIG_IDF_TARGET_ESP32H2 - } - else - { - return buildWifiConnectHtml(request, resp); - } - #endif + Log->println("Page not found, loading index"); + resp->setCode(302); + resp->addHeader("Cache-Control", "no-cache"); + return resp->redirect("/"); } }); _psychicServer->on("/post", HTTP_POST, [&](PsychicRequest *request, PsychicResponse* resp) { - if(strlen(_credUser) > 0 && strlen(_credPassword) > 0 && !request->authenticate(_credUser, _credPassword)) - { - return request->requestAuthentication(auth_type, "Nuki Hub", "You must log in."); - } - String value = ""; if(request->hasParam("page")) { @@ -429,8 +546,46 @@ void WebCfgServer::initialize() } } + if (value != "login") + { + int authReq = doAuthentication(request); + + switch (authReq) + { + case 0: + return request->requestAuthentication(BASIC_AUTH, "Nuki Hub", "You must log in."); + break; + case 1: + return request->requestAuthentication(DIGEST_AUTH, "Nuki Hub", "You must log in."); + break; + case 2: + resp->setCode(302); + resp->addHeader("Cache-Control", "no-cache"); + return resp->redirect("/get?page=login"); + case 4: + default: + break; + } + } + + if (value == "login") + { + bool loggedIn = processLogin(request, resp); + if (loggedIn) + { + resp->setCode(302); + resp->addHeader("Cache-Control", "no-cache"); + return resp->redirect("/"); + } + else + { + resp->setCode(302); + resp->addHeader("Cache-Control", "no-cache"); + return resp->redirect("/get?page=login"); + } + } #ifndef NUKI_HUB_UPDATER - if (value == "savecfg") + else if (value == "savecfg") { String message = ""; bool restart = processArgs(request, resp, message); @@ -474,10 +629,8 @@ void WebCfgServer::initialize() bool restart = processImport(request, resp, message); return buildConfirmHtml(request, resp, message, 3, true); } - else - #else - if (1 == 1) #endif + else { #ifndef CONFIG_IDF_TARGET_ESP32H2 if(!_network->isApOpen()) @@ -501,19 +654,44 @@ void WebCfgServer::initialize() PsychicUploadHandler *updateHandler = new PsychicUploadHandler(); updateHandler->onUpload([&](PsychicRequest *request, const String& filename, uint64_t index, uint8_t *data, size_t len, bool last) { - if(strlen(_credUser) > 0 && strlen(_credPassword) > 0 && !request->authenticate(_credUser, _credPassword)) - { - return request->requestAuthentication(auth_type, "Nuki Hub", "You must log in."); - } + int authReq = doAuthentication(request); + switch (authReq) + { + case 0: + return request->requestAuthentication(BASIC_AUTH, "Nuki Hub", "You must log in."); + break; + case 1: + return request->requestAuthentication(DIGEST_AUTH, "Nuki Hub", "You must log in."); + break; + case 2: + return ESP_FAIL; + case 4: + default: + break; + } return handleOtaUpload(request, filename, index, data, len, last); }); updateHandler->onRequest([&](PsychicRequest* request, PsychicResponse* resp) { - if(strlen(_credUser) > 0 && strlen(_credPassword) > 0 && !request->authenticate(_credUser, _credPassword)) + int authReq = doAuthentication(request); + + switch (authReq) { - return request->requestAuthentication(auth_type, "Nuki Hub", "You must log in."); + case 0: + return request->requestAuthentication(BASIC_AUTH, "Nuki Hub", "You must log in."); + break; + case 1: + return request->requestAuthentication(DIGEST_AUTH, "Nuki Hub", "You must log in."); + break; + case 2: + resp->setCode(302); + resp->addHeader("Cache-Control", "no-cache"); + return resp->redirect("/get?page=login"); + case 4: + default: + break; } String result; @@ -546,6 +724,47 @@ void WebCfgServer::initialize() _psychicServer->on("/uploadota", HTTP_POST, updateHandler); //Update.onProgress(printProgress); } + + _psychicServer->on("/", HTTP_GET, [&](PsychicRequest *request, PsychicResponse* resp) + { + int authReq = doAuthentication(request); + + switch (authReq) + { + case 0: + return request->requestAuthentication(BASIC_AUTH, "Nuki Hub", "You must log in."); + break; + case 1: + return request->requestAuthentication(DIGEST_AUTH, "Nuki Hub", "You must log in."); + break; + case 2: + resp->setCode(302); + resp->addHeader("Cache-Control", "no-cache"); + return resp->redirect("/get?page=login"); + break; + case 4: + default: + break; + } + +#ifndef CONFIG_IDF_TARGET_ESP32H2 + if(!_network->isApOpen()) + { +#endif +#ifndef NUKI_HUB_UPDATER + return buildHtml(request, resp); +#else + return buildOtaHtml(request, resp); +#endif +#ifndef CONFIG_IDF_TARGET_ESP32H2 + } + else + { + return buildWifiConnectHtml(request, resp); + } +#endif + }); + } void WebCfgServer::printCheckBox(PsychicStreamResponse *response, const char *token, const char *description, const bool value, const char *htmlClass) @@ -1266,6 +1485,54 @@ void WebCfgServer::printInputField(PsychicStreamResponse *response, printInputField(response, token, description, valueStr, maxLength, args); } +esp_err_t WebCfgServer::buildLoginHtml(PsychicRequest *request, PsychicResponse* resp) +{ + PsychicStreamResponse response(resp, "text/html"); + response.beginSend(); + response.print(""); + response.print(""); + response.print(""); + response.print("

NukiHub login

"); + response.print("
"); + response.print(""); + response.print("
"); + response.print("
"); + return response.endSend(); +} + +bool WebCfgServer::processLogin(PsychicRequest *request, PsychicResponse* resp) +{ + if(request->hasParam("username") && request->hasParam("password")) + { + const PsychicWebParameter* user = request->getParam("username"); + const PsychicWebParameter* pass = request->getParam("password"); + if(user->value() != "" && pass->value() != "") + { + if (user->value() == _preferences->getString(preference_cred_user, "") && pass->value() == _preferences->getString(preference_cred_password, "")) + { + char buffer[33]; + int i; + int64_t durationLength = 60*60*24*30; + for (i = 0; i < 4; i++) { + sprintf(buffer + (i * 8), "%08lx", (unsigned long int)esp_random()); + } + if(!request->hasParam("remember")) { + durationLength = 60*60; + } + resp->setCookie("sessionId", buffer, durationLength, "HttpOnly"); + + struct timeval time; + gettimeofday(&time, NULL); + int64_t time_us = (int64_t)time.tv_sec * 1000000L + (int64_t)time.tv_usec; + _httpSessions[buffer] = time_us + (durationLength*1000000L); + saveSessions(); + return true; + } + } + } + return false; +} + #ifndef NUKI_HUB_UPDATER esp_err_t WebCfgServer::sendSettings(PsychicRequest *request, PsychicResponse* resp) { @@ -2788,9 +3055,9 @@ bool WebCfgServer::processArgs(PsychicRequest *request, PsychicResponse* resp, S } else if(key == "CREDDIGEST") { - if(_preferences->getBool(preference_http_auth_type, false) != (value == "1")) + if(_preferences->getInt(preference_http_auth_type, 0) != value.toInt()) { - _preferences->putBool(preference_http_auth_type, (value == "1")); + _preferences->putInt(preference_http_auth_type, value.toInt()); Log->print(("Setting changed: ")); Log->println(key); configChanged = true; @@ -3914,8 +4181,7 @@ esp_err_t WebCfgServer::buildCustomNetworkConfigHtml(PsychicRequest *request, Ps esp_err_t WebCfgServer::buildHtml(PsychicRequest *request, PsychicResponse* resp) { - _randomInt = esp_random(); - String header = (String)""; + String header = (String)""; PsychicStreamResponse response(resp, "text/html"); response.beginSend(); buildHtmlHeader(&response, header); @@ -4008,6 +4274,10 @@ esp_err_t WebCfgServer::buildHtml(PsychicRequest *request, PsychicResponse* resp #endif String rebooturl = "/get?page=reboot&CONFIRMTOKEN=" + _confirmCode; buildNavigationMenuEntry(&response, "Reboot Nuki Hub", rebooturl.c_str()); + if (_preferences->getInt(preference_http_auth_type, 0) == 2) + { + buildNavigationMenuEntry(&response, "Logout", "/get?page=logout"); + } response.print(""); return response.endSend(); } @@ -4024,7 +4294,13 @@ esp_err_t WebCfgServer::buildCredHtml(PsychicRequest *request, PsychicResponse* printInputField(&response, "CREDUSER", "User (# to clear)", _preferences->getString(preference_cred_user).c_str(), 30, "id=\"inputuser\"", false, true); printInputField(&response, "CREDPASS", "Password", "*", 30, "id=\"inputpass\"", true, true); printInputField(&response, "CREDPASSRE", "Retype password", "*", 30, "id=\"inputpass2\"", true); - printCheckBox(&response, "CREDDIGEST", "Use Digest Authentication (more secure)", _preferences->getBool(preference_http_auth_type, false), ""); + + std::vector> httpAuthOptions; + httpAuthOptions.push_back(std::make_pair("0", "Basic")); + httpAuthOptions.push_back(std::make_pair("1", "Digest")); + httpAuthOptions.push_back(std::make_pair("2", "Form")); + + printDropDown(&response, "CREDDIGEST", "HTTP Authentication type", String(_preferences->getInt(preference_http_auth_type, 0)), httpAuthOptions, ""); response.print(""); response.print("
"); response.print(""); @@ -4964,7 +5240,7 @@ esp_err_t WebCfgServer::buildInfoHtml(PsychicRequest *request, PsychicResponse* response.print("\nWeb configurator password: "); response.print(_preferences->getString(preference_cred_password, "").length() > 0 ? "***" : "Not set"); response.print("\nWeb configurator authentication: "); - response.print(_preferences->getBool(preference_http_auth_type, false) ? "Digest" : "Basic"); + response.print(_preferences->getInt(preference_http_auth_type, 0) == 0 ? "Basic" : _preferences->getInt(preference_http_auth_type, 0) == 1 ? "Digest" : "Form"); response.print("\nWeb configurator enabled: "); response.print(_preferences->getBool(preference_webserver_enabled, true) ? "Yes" : "No"); response.print("\nHTTP SSL: "); @@ -5652,6 +5928,7 @@ esp_err_t WebCfgServer::processUpdate(PsychicRequest *request, PsychicResponse* if(request->hasParam("beta")) { + /* if(request->hasParam("debug")) { res = buildConfirmHtml(request, resp, "Rebooting to update Nuki Hub and Nuki Hub updater
Updating to latest DEBUG BETA version", 2, true); @@ -5660,13 +5937,15 @@ esp_err_t WebCfgServer::processUpdate(PsychicRequest *request, PsychicResponse* } else { - res = buildConfirmHtml(request, resp, "Rebooting to update Nuki Hub and Nuki Hub updater
Updating to latest BETA version", 2, true); - _preferences->putString(preference_ota_updater_url, GITHUB_BETA_UPDATER_BINARY_URL); - _preferences->putString(preference_ota_main_url, GITHUB_BETA_RELEASE_BINARY_URL); - } + */ + res = buildConfirmHtml(request, resp, "Rebooting to update Nuki Hub and Nuki Hub updater
Updating to latest BETA version", 2, true); + _preferences->putString(preference_ota_updater_url, GITHUB_BETA_UPDATER_BINARY_URL); + _preferences->putString(preference_ota_main_url, GITHUB_BETA_RELEASE_BINARY_URL); + //} } else if(request->hasParam("master")) { + /* if(request->hasParam("debug")) { res = buildConfirmHtml(request, resp, "Rebooting to update Nuki Hub and Nuki Hub updater
Updating to latest DEBUG DEVELOPMENT version", 2, true); @@ -5675,10 +5954,11 @@ esp_err_t WebCfgServer::processUpdate(PsychicRequest *request, PsychicResponse* } else { - res = buildConfirmHtml(request, resp, "Rebooting to update Nuki Hub and Nuki Hub updater
Updating to latest DEVELOPMENT version", 2, true); - _preferences->putString(preference_ota_updater_url, GITHUB_MASTER_UPDATER_BINARY_URL); - _preferences->putString(preference_ota_main_url, GITHUB_MASTER_RELEASE_BINARY_URL); - } + */ + res = buildConfirmHtml(request, resp, "Rebooting to update Nuki Hub and Nuki Hub updater
Updating to latest DEVELOPMENT version", 2, true); + _preferences->putString(preference_ota_updater_url, GITHUB_MASTER_UPDATER_BINARY_URL); + _preferences->putString(preference_ota_main_url, GITHUB_MASTER_RELEASE_BINARY_URL); + //} } #if defined(CONFIG_IDF_TARGET_ESP32S3) else if(request->hasParam("other")) @@ -5690,6 +5970,7 @@ esp_err_t WebCfgServer::processUpdate(PsychicRequest *request, PsychicResponse* #endif else { + /* if(request->hasParam("debug")) { res = buildConfirmHtml(request, resp, "Rebooting to update Nuki Hub and Nuki Hub updater
Updating to latest DEBUG RELEASE version", 2, true); @@ -5698,10 +5979,11 @@ esp_err_t WebCfgServer::processUpdate(PsychicRequest *request, PsychicResponse* } else { - res = buildConfirmHtml(request, resp, "Rebooting to update Nuki Hub and Nuki Hub updater
Updating to latest RELEASE version", 2, true); - _preferences->putString(preference_ota_updater_url, GITHUB_LATEST_UPDATER_BINARY_URL); - _preferences->putString(preference_ota_main_url, GITHUB_LATEST_RELEASE_BINARY_URL); - } + */ + res = buildConfirmHtml(request, resp, "Rebooting to update Nuki Hub and Nuki Hub updater
Updating to latest RELEASE version", 2, true); + _preferences->putString(preference_ota_updater_url, GITHUB_LATEST_UPDATER_BINARY_URL); + _preferences->putString(preference_ota_main_url, GITHUB_LATEST_RELEASE_BINARY_URL); + //} } waitAndProcess(true, 1000); restartEsp(RestartReason::OTAReboot); diff --git a/src/WebCfgServer.h b/src/WebCfgServer.h index 67ed731..a5c6a1d 100644 --- a/src/WebCfgServer.h +++ b/src/WebCfgServer.h @@ -100,7 +100,14 @@ private: std::vector _rssiList; String generateConfirmCode(); String _confirmCode = "----"; - + + void saveSessions(); + void loadSessions(); + esp_err_t logoutSession(PsychicRequest *request, PsychicResponse* resp); + bool isAuthenticated(PsychicRequest *request); + bool processLogin(PsychicRequest *request, PsychicResponse* resp); + int doAuthentication(PsychicRequest *request); + esp_err_t buildLoginHtml(PsychicRequest *request, PsychicResponse* resp); esp_err_t buildSSIDListHtml(PsychicRequest *request, PsychicResponse* resp); esp_err_t buildConfirmHtml(PsychicRequest *request, PsychicResponse* resp, const String &message, uint32_t redirectDelay = 5, bool redirect = false, String redirectTo = "/"); esp_err_t buildOtaHtml(PsychicRequest *request, PsychicResponse* resp, bool debug = false); @@ -128,6 +135,6 @@ private: bool _allowRestartToPortal = false; uint8_t _partitionType = 0; size_t _otaContentLen = 0; - uint32_t _randomInt = 0; String _hostname; + JsonDocument _httpSessions; }; diff --git a/src/main.cpp b/src/main.cpp index 4509efa..dde42f7 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -10,6 +10,7 @@ #include "esp32-hal-log.h" #include "hal/wdt_hal.h" #include "esp_chip_info.h" +#include "esp_netif_sntp.h" #ifdef CONFIG_SOC_SPIRAM_SUPPORTED #include "esp_psram.h" #include "FS.h" @@ -29,7 +30,6 @@ #include "RestartReason.h" #include "EspMillis.h" #include "NimBLEDevice.h" -#include "esp_netif_sntp.h" /* #ifdef DEBUG_NUKIHUB @@ -38,8 +38,6 @@ #endif */ -char log_print_buffer[1024]; - NukiNetworkLock* networkLock = nullptr; NukiNetworkOpener* networkOpener = nullptr; BleScanner::Scanner* bleScanner = nullptr; @@ -71,6 +69,8 @@ int64_t restartTs = 10 * 60 * 1000; #endif +char log_print_buffer[1024]; + PsychicHttpServer* psychicServer = nullptr; PsychicHttpsServer* psychicSSLServer = nullptr; NukiNetwork* network = nullptr; @@ -97,7 +97,6 @@ RestartReason currentRestartReason = RestartReason::NotApplicable; TaskHandle_t otaTaskHandle = nullptr; TaskHandle_t networkTaskHandle = nullptr; -#ifndef NUKI_HUB_UPDATER ssize_t write_fn(void* cookie, const char* buf, ssize_t size) { Log->write((uint8_t *)buf, (size_t)size); @@ -157,7 +156,6 @@ void setReroute() } } -#endif uint8_t checkPartition() { @@ -181,6 +179,10 @@ uint8_t checkPartition() } } +void cbSyncTime(struct timeval *tv) { + Log->println("NTP time synched"); +} + void networkTask(void *pvParameters) { int64_t networkLoopTs = 0; @@ -204,13 +206,15 @@ void networkTask(void *pvParameters) network->update(); bool connected = network->isConnected(); - #ifndef NUKI_HUB_UPDATER if(connected && reroute) { + if(preferences->getBool(preference_update_time, false)) + { + esp_netif_sntp_start(); + } reroute = false; setReroute(); } - #endif #ifndef NUKI_HUB_UPDATER wifiConnected = network->wifiConnected(); @@ -503,10 +507,6 @@ void setupTasks(bool ota) } } -void cbSyncTime(struct timeval *tv) { - Log->println(("NTP time synched")); -} - void setup() { //Set Log level to error for all TAGS @@ -822,16 +822,14 @@ void setup() } #endif */ - } - if(preferences->getBool(preference_update_time, false)) - { - esp_sntp_config_t config = ESP_NETIF_SNTP_DEFAULT_CONFIG(preferences->getString(preference_time_server, "pool.ntp.org").c_str()); + String timeserver = preferences->getString(preference_time_server, "pool.ntp.org"); + esp_sntp_config_t config = ESP_NETIF_SNTP_DEFAULT_CONFIG(timeserver.c_str()); config.start = false; config.server_from_dhcp = true; config.renew_servers_after_new_IP = true; config.index_of_first_server = 1; - + if (network->networkDeviceType() == NetworkDeviceType::WiFi) { config.ip_event_to_renew = IP_EVENT_STA_GOT_IP;