From d2087065307ab8600c245dcad6e13d23e55e5089 Mon Sep 17 00:00:00 2001 From: Joel Samson Date: Mon, 7 Sep 2026 20:02:41 -0400 Subject: [PATCH 01/26] Delete merlin-sha256.txt --- merlin-sha256.txt | 21 --------------------- 1 file changed, 21 deletions(-) delete mode 100644 merlin-sha256.txt diff --git a/merlin-sha256.txt b/merlin-sha256.txt deleted file mode 100644 index 2eed75ce..00000000 --- a/merlin-sha256.txt +++ /dev/null @@ -1,21 +0,0 @@ -a04d65e566b70a2533f1dcde9e0e1e66133438268f66165caeb973a2aaa8e065 GT-AX11000_3004_388.12_2_rog_ubi.w -24f8679af3726386321aea8ae49af2e7a560f46a8f1fca5a93e31a31d72e43d2 GT-AX11000_3004_388.12_2_ubi.w -b78b365ec8060351c5143782ad9a438f67ab39f37b454fbe06113e99a62c6c25 GT-AXE11000_3004_388.12_2_pureubi.w -c4ef1675ecd772f381c6e5a696d643db89e28f64451d140f53bb8fea62d0089d GT-AXE11000_3004_388.12_2_rog_pureubi.w -352f4a89e86f02deb89d5569db0e354c89956f28225458f8e14f4fe6bacc2123 RT-AX58U_3004_388.12_2_puresqubi.w -936e04cccc6da05b94c19c3cb9c6d8b9fbb0a6542adc7b953aea3c0c7c068903 RT-AX68U_3004_388.12_2_pureubi.w -864cb94504bd0f036f730089c779aafbf5827721af10d5533924f94d3c20982a RT-AX86U_3004_388.12_2_pureubi.w -a902d77915c609cf5ee6ac6069c52e3ab460cd4b915f0febcb269175cc842a6d RT-AX88U_3004_388.12_2_ubi.w -5dc2284ac99bcb280b8f57c98d817ae17c1ae5e60102b8cb581680894739f467 GT-AX11000_PRO_3006_102.8_4_nand_squashfs.pkgtb -9497ab9a5956da9b6c9b86a1c0172e0eef9d8e02c9531598332f0a911a92e925 GT-AX6000_3006_102.8_4_nand_squashfs.pkgtb -ea266310a61dc9018fed850e1dba38b37335740fd5e0053683a1133d69837f4a GT-AXE16000_3006_102.8_4_nand_squashfs.pkgtb -f9e896c3b46a2913e60ea2ac638b4ca624d4d239b3a5416864da95f8b4ed1873 GT-BE19000AI_3006_102.8_4_emmc_squashfs.pkgtb -11932c048a9ecc2be8de93a19af985d685bb21999091d70a1b946fbfa57b7960 GT-BE98_PRO_3006_102.8_4_nand_squashfs.pkgtb -78f296e30bde73b842f3e211b3205bdab7e9b947f1b0b40d43a248cc86badaa7 RT-AX86U_PRO_3006_102.8_4_nand_squashfs.pkgtb -8eca0813db27ef8518535cd00060475c58c7928d8878e87d34df4476dde05550 RT-AX88U_PRO_3006_102.8_4_nand_squashfs.pkgtb -566bb3a9a6331293d7a28b2dbb0676dfa846a5f11bafa3a99c848b894c087606 RT-BE58_GO_3006_102.8_4_nand_squashfs.pkgtb -7cf298f161aca6de3ad4ff3840f08eae3c11a39c067da825e444439aac67dabc RT-BE86U_3006_102.8_4_nand_squashfs.pkgtb -28954e19157261fe97adcb266591de839c202d188d485484a87089149c61dea3 RT-BE88U_3006_102.8_4_nand_squashfs.pkgtb -1fc818b504f51a378fcbd1b8acde0d1e7950d28527c364cc8c4937c9a4072fbe RT-BE92U_3006_102.8_4_nand_squashfs.pkgtb -f4483a27071d4bc074b2e42760e736a8d079a5f0fb9cab1684553de11ce5f453 RT-BE96U_3006_102.8_4_nand_squashfs.pkgtb -12b00d614e5073e78c8e87ae84c688c2d93e7afcb4d0f36d479731b1cbb9f54e XT12_3006_102.8_4_nand_squashfs.pkgtb From ee06d2ab9f162debaed99df1d8f57c7d45ba32ad Mon Sep 17 00:00:00 2001 From: ExtremeFiretop Date: Wed, 16 Sep 2026 01:40:56 -0400 Subject: [PATCH 02/26] Prevent mesh firmware checks from interrupting node updates MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Fix a new bug identified and introduced by commit: https://github.com/ExtremeFiretop/MerlinAutoUpdate-Router/commit/bae364025176113bcedff624fc665087954a7f8a And PR: https://github.com/ExtremeFiretop/MerlinAutoUpdate-Router/pull/539 This bug appears to be a concurrent firmware management collision, essentially a race while flashing a node while checking it for updates from the primary at the same time. I accidently ran into this, the seems to be that the node had prepared for the update, authenticated to its own WebUI, and was just about to hand the beta1 image to /upgrade.cgi When at that exact time, I started an upgrade from the primary, which logged into the node to check for updates, triggered start_webs_update, and resulted in putting the node’s ASUS firmware update system into a competing update/check state and ultimately MerlinAU rebooted the node without flashing any firmware. This may also be related to a report from JimbobJay here: https://www.snbforums.com/threads/merlinau-v1-6-8-the-ultimate-firmware-auto-updater.96306/post-999444 --- MerlinAU.sh | 103 +++++++++++++++++++++++++++++++++++++++++++--------- 1 file changed, 86 insertions(+), 17 deletions(-) diff --git a/MerlinAU.sh b/MerlinAU.sh index 1d3eaabb..ffdc1ce3 100644 --- a/MerlinAU.sh +++ b/MerlinAU.sh @@ -19,11 +19,11 @@ set -u ## Set version for each Production Release ## -readonly SCRIPT_VERSION=1.6.8 -readonly SCRIPT_VERSTAG="26090718" +readonly SCRIPT_VERSION=1.6.9 +readonly SCRIPT_VERSTAG="26091601" readonly SCRIPT_NAME="MerlinAU" ## Set to "master" for Production Releases ## -SCRIPT_BRANCH="master" +SCRIPT_BRANCH="dev" ##----------------------------------------## ## Modified by Martinski W. [2024-Jul-03] ## @@ -4335,7 +4335,7 @@ _ReEnableAsusTrendMicroProcesses_() } ##------------------------------------------## -## Modified by ExtremeFiretop [2024-Jan-26] ## +## Modified by ExtremeFiretop [2025-Sep-16] ## ##------------------------------------------## _DoCleanUp_() { @@ -4353,6 +4353,10 @@ _DoCleanUp_() [ $# -gt 1 ] && [ "$2" -eq 1 ] && keepZIPfile=true [ $# -gt 2 ] && [ "$3" -eq 1 ] && keepWfile=true + # Clear the volatile F/W-update guard used by AiMesh primaries. # + # This value is intentionally never committed to NVRAM. # + nvram unset merlinau_fw_update 2>/dev/null + # Stop the LEDs blinking # _Reset_LEDs_ 1 @@ -5429,9 +5433,9 @@ _DoMeshNodeLogin_() return "$?" } -##----------------------------------------## -## Modified by Martinski W. [2026-Jan-01] ## -##----------------------------------------## +##------------------------------------------## +## Modified by ExtremeFiretop [2026-Sep-16] ## +##------------------------------------------## # Trigger the node "Check for updates" (no waiting here) _MeshNodeTriggerFWCheck_() { @@ -5443,6 +5447,7 @@ _MeshNodeTriggerFWCheck_() local safeID="$(_MeshSafeID_ "$nodeIPv4addr")" local nodeURL="$(_GetNodeURL_ "$nodeIPv4addr")" local cookieFile="/tmp/${runID}.${safeID}.cookie" + local nodeBusy nodeBusyRC # Check for Login Credentials # credsENC="$(Get_Custom_Setting credentials_base64)" @@ -5463,6 +5468,30 @@ _MeshNodeTriggerFWCheck_() return 1 fi + # Check if the AiMesh node is already performing a MerlinAU F/W update + # before triggering the built-in firmware update check. + nodeBusy="$(curl -s -k "${nodeURL}/appGet.cgi?hook=nvram_get(merlinau_fw_update)" \ + -H 'User-Agent: Mozilla/5.0 (X11; Linux x86_64; rv:109.0) Gecko/20100101 Firefox/115.0' \ + -H 'Accept: application/json,text/plain,*/*' \ + -H 'Accept-Language: en-US,en;q=0.5' \ + -H 'Connection: keep-alive' \ + -H "Referer: ${nodeURL}/index.asp" \ + --cookie "$cookieFile" \ + --max-time 2 2>/dev/null)" + nodeBusyRC="$?" + + if [ "$nodeBusyRC" -eq 0 ] && echo "$nodeBusy" | grep -Eq '"merlinau_fw_update"[[:space:]]*:[[:space:]]*"1"' + then + Say "AiMesh Node [$nodeIPv4addr] entered an active MerlinAU F/W update before start_webs_update. Skipping firmware check." + + # Best-effort logout of this primary-router session, then remove its cookie. + curl -s -k "${nodeURL}/Logout.asp" \ + --cookie "$cookieFile" \ + --max-time 2 >/dev/null 2>&1 + rm -f "$cookieFile" + return 0 + fi + # Trigger firmware check (mimic WebUI "Check" button) # curl -s -k "${nodeURL}/start_apply.htm" \ --referer "${nodeURL}/Advanced_FirmwareUpgrade_Content.asp" \ @@ -9755,11 +9784,15 @@ _Unmount_Eject_USB_Drives_() "$ejectUSB_OK" && return 0 || return 1 } -##----------------------------------------## -## Modified by Martinski W. [2026-Jan-01] ## -##----------------------------------------## +##------------------------------------------## +## Modified by ExtremeFiretop [2026-Sep-16] ## +##------------------------------------------## _RunFirmwareUpdateNow_() { + local fwUploadResponseFile="/tmp/upload_response.txt" + local fwUploadDiagFile="${SETTINGS_DIR}/last_fw_upload_response.txt" + local curlRC=0 uploadHTTPcode="" + # Double-check the directory exists before using it # [ ! -d "$FW_LOG_DIR" ] && mkdir -p -m 755 "$FW_LOG_DIR" @@ -10219,6 +10252,15 @@ Please manually update to version ${GRNct}${MinSupportedFirmwareVers}${NOct} or fi fi + #------------------------------------------------------------------------# + # A volatile guard before restarting/logging into the WebGUI. + # Primary routers running MerlinAU can query this nvram value with appGet.cgi + # and avoid triggering start_webs_update on this router mid-flash. + # Do not commit this value since a reboot should clear it automatically. + #------------------------------------------------------------------------# + nvram set merlinau_fw_update=1 + rm -f "$fwUploadResponseFile" "$fwUploadDiagFile" + #------------------------------------------------------------# # Restart the WebGUI to make sure nobody else is logged in # so that the F/W Update can start without interruptions. @@ -10311,7 +10353,8 @@ Please manually update to version ${GRNct}${MinSupportedFirmwareVers}${NOct} or Say "Flashing ${GRNct}${firmware_file}${NOct}...\n${REDct}Please wait for reboot in about 4 minutes or less.${NOct}" echo - # *WARNING*: NO MORE logging at this point & beyond # + # Avoid persistent logging from this point during the normal flash path. # + # Failure diagnostics are written only if the router does not reboot. # sync ; sleep 2 ; echo 3 > /proc/sys/vm/drop_caches ; sleep 3 ##-------------------------------------## @@ -10327,7 +10370,7 @@ Please manually update to version ${GRNct}${MinSupportedFirmwareVers}${NOct} or # the following 'Curl' command MUST always be the last step in this block. # Do NOT insert any commands after it! (unless you understand the implications). #----------------------------------------------------------------------------------# - nohup curl -k "${routerURL}/upgrade.cgi" \ + nohup curl -sS -k "${routerURL}/upgrade.cgi" \ --referer "${routerURL}/Advanced_FirmwareUpgrade_Content.asp" \ --user-agent 'Mozilla/5.0 (X11; Linux x86_64; rv:109.0) Gecko/20100101 Firefox/115.0' \ -H 'Accept-Language: en-US,en;q=0.5' \ @@ -10340,7 +10383,9 @@ Please manually update to version ${GRNct}${MinSupportedFirmwareVers}${NOct} or -F 'preferred_lang=EN' \ -F "firmver=${dottedVersion}" \ -F "file=@${firmware_file}" \ - --cookie "$cookieFile" > /tmp/upload_response.txt 2>&1 & + --cookie "$cookieFile" \ + --write-out '\nMERLINAU_HTTP_CODE:%{http_code}\n' \ + > "$fwUploadResponseFile" 2>&1 & curlPID=$! #----------------------------------------------------------# @@ -10356,16 +10401,40 @@ Please manually update to version ${GRNct}${MinSupportedFirmwareVers}${NOct} or sleep 180 if [ "$curlPID" -gt 0 ] then - kill -EXIT $curlPID 2>/dev/null || return - kill -TERM $curlPID 2>/dev/null + kill -EXIT "$curlPID" 2>/dev/null || return + kill -TERM "$curlPID" 2>/dev/null fi ) & - wait $curlPID ; curlPID=0 + + # Preserve Curl's actual result instead of discarding it. # + wait "$curlPID" + curlRC=$? + curlPID=0 + uploadHTTPcode="$(sed -n 's/^MERLINAU_HTTP_CODE://p' "$fwUploadResponseFile" 2>/dev/null | tail -n 1)" + #----------------------------------------------------------# # Let's wait for 3 minutes here. If the router does not - # reboot by itself after the process returns, do it now. + # reboot by itself after the process returns, + # preserve any diagnostics then reboot. + # A successful flash reboots before this step. #----------------------------------------------------------# sleep 180 + + { + echo "MerlinAU v$SCRIPT_VERSION firmware upload diagnostics" + echo "Timestamp: $(date '+%Y-%m-%d %H:%M:%S %Z')" + echo "Router: $MODEL_ID" + echo "Firmware image: $firmware_file" + echo "Curl exit code: $curlRC" + echo "HTTP status: ${uploadHTTPcode:-UNKNOWN}" + echo "------------------------------------------------------------" + [ -s "$fwUploadResponseFile" ] && cat "$fwUploadResponseFile" + } > "$fwUploadDiagFile" 2>/dev/null + chmod 600 "$fwUploadDiagFile" 2>/dev/null + + _MsgToSysLog_ "F/W upload did not cause the router to reboot within 180 seconds. Curl exit code [$curlRC], HTTP status [${uploadHTTPcode:-UNKNOWN}]." + _MsgToSysLog_ "F/W upload diagnostics saved to [$fwUploadDiagFile]." + _ReleaseLock_ /sbin/service reboot else From 6212bf4a7f3fbefb6a58ae9709ba21833638c529 Mon Sep 17 00:00:00 2001 From: ExtremeFiretop Date: Wed, 16 Sep 2026 02:24:14 -0400 Subject: [PATCH 03/26] Stop logging out early, allow _GetNodeInfo_ to logout Stop logging out early, allow _GetNodeInfo_ to logout --- MerlinAU.sh | 6 ------ 1 file changed, 6 deletions(-) diff --git a/MerlinAU.sh b/MerlinAU.sh index ffdc1ce3..22ba8786 100644 --- a/MerlinAU.sh +++ b/MerlinAU.sh @@ -5483,12 +5483,6 @@ _MeshNodeTriggerFWCheck_() if [ "$nodeBusyRC" -eq 0 ] && echo "$nodeBusy" | grep -Eq '"merlinau_fw_update"[[:space:]]*:[[:space:]]*"1"' then Say "AiMesh Node [$nodeIPv4addr] entered an active MerlinAU F/W update before start_webs_update. Skipping firmware check." - - # Best-effort logout of this primary-router session, then remove its cookie. - curl -s -k "${nodeURL}/Logout.asp" \ - --cookie "$cookieFile" \ - --max-time 2 >/dev/null 2>&1 - rm -f "$cookieFile" return 0 fi From c2c2e463fd68ba6857525a4686b0eb783b097960 Mon Sep 17 00:00:00 2001 From: ExtremeFiretop Date: Wed, 16 Sep 2026 02:56:36 -0400 Subject: [PATCH 04/26] Tightening up the Flash Order Tightening up the Flash Order Adjusting the flash order so we delete cron jobs before unloading Entware. (Incase a cron tries to fire for an entware script that is unloaded) This may also be related to a report from JimbobJay here: https://www.snbforums.com/threads/merlinau-v1-6-8-the-ultimate-firmware-auto-updater.96306/post-999444 Also adjusted the WebUI restart to be RIGHT before we login to the router to start the flash. The vulnerable window was the gap between the WebUI restart, unloading the USB, and flashing. Also adjusted the order so we say flashing right before we actually restart the WebUI and flash --- MerlinAU.sh | 29 +++++++++++++++-------------- 1 file changed, 15 insertions(+), 14 deletions(-) diff --git a/MerlinAU.sh b/MerlinAU.sh index 22ba8786..044e7986 100644 --- a/MerlinAU.sh +++ b/MerlinAU.sh @@ -10255,14 +10255,6 @@ Please manually update to version ${GRNct}${MinSupportedFirmwareVers}${NOct} or nvram set merlinau_fw_update=1 rm -f "$fwUploadResponseFile" "$fwUploadDiagFile" - #------------------------------------------------------------# - # Restart the WebGUI to make sure nobody else is logged in - # so that the F/W Update can start without interruptions. - #------------------------------------------------------------# - "$isInteractive" && printf "\nRestarting web server... Please wait.\n" - /sbin/service restart_httpd >/dev/null 2>&1 & - sleep 4 - # Send last email notification before F/W flash # _SendEMailNotification_ START_FW_UPDATE_STATUS @@ -10333,19 +10325,16 @@ Please manually update to version ${GRNct}${MinSupportedFirmwareVers}${NOct} or # Remove SIGHUP to allow script to continue # trap '' HUP - # Stop Entware services WITHOUT exceptions BEFORE the F/W flash # - _EntwareServicesHandler_ stop -noskip - ##-------------------------------------## ## Added by Martinski W. [2024-Sep-15] ## ##-------------------------------------## # Remove cron jobs from 3rd-party Add-Ons # _RemoveCronJobsFromAddOns_ + # Stop Entware services WITHOUT exceptions BEFORE the F/W flash # + _EntwareServicesHandler_ stop -noskip + _Do_PostReboot_FWUpdate_Setup_ - echo - Say "Flashing ${GRNct}${firmware_file}${NOct}...\n${REDct}Please wait for reboot in about 4 minutes or less.${NOct}" - echo # Avoid persistent logging from this point during the normal flash path. # # Failure diagnostics are written only if the router does not reboot. # @@ -10358,6 +10347,18 @@ Please manually update to version ${GRNct}${MinSupportedFirmwareVers}${NOct} or #------------------------------------------------------------------# _Unmount_Eject_USB_Drives_ + echo + Say "Flashing ${GRNct}${firmware_file}${NOct}...\n${REDct}Please wait for reboot in about 4 minutes or less.${NOct}" + echo + + #------------------------------------------------------------# + # Restart the WebGUI to make sure nobody else is logged in + # so that the F/W Update can start without interruptions. + #------------------------------------------------------------# + "$isInteractive" && printf "\nRestarting web server... Please wait.\n" + /sbin/service restart_httpd >/dev/null 2>&1 & + sleep 3 + #----------------------------------------------------------------------------------# # **IMPORTANT NOTE**: # Due to the nature of 'nohup' and the specific behavior of this 'Curl' request, From 1089887ed3bc09302a0e2af2ec481521e9f6c0f3 Mon Sep 17 00:00:00 2001 From: Martinski4GitHub <119833648+Martinski4GitHub@users.noreply.github.com> Date: Thu, 17 Sep 2026 01:34:04 -0700 Subject: [PATCH 05/26] Fix for Copy&Paste into Password Entry Fixed issue in the CLI menu, where user was unable to perform a "Copy&Paste" operation when entering the password string. --- MerlinAU.sh | 22 +++++++++++++--------- README.md | 4 ++-- version.txt | 2 +- 3 files changed, 16 insertions(+), 12 deletions(-) diff --git a/MerlinAU.sh b/MerlinAU.sh index 1d3eaabb..629e22e8 100644 --- a/MerlinAU.sh +++ b/MerlinAU.sh @@ -4,7 +4,7 @@ # # Project Created: 2023-Oct-01 by @ExtremeFiretop # Official Co-Author: @Martinski W. since 2023-Nov-01 -# Last Modified: 2026-Sep-03 +# Last Modified: 2026-Sep-17 # # MerlinAU™ / MerlinAutoUpdate™ # Official project: https://github.com/ExtremeFiretop/MerlinAutoUpdate-Router @@ -19,11 +19,11 @@ set -u ## Set version for each Production Release ## -readonly SCRIPT_VERSION=1.6.8 -readonly SCRIPT_VERSTAG="26090718" +readonly SCRIPT_VERSION=1.6.9 +readonly SCRIPT_VERSTAG="26091700" readonly SCRIPT_NAME="MerlinAU" ## Set to "master" for Production Releases ## -SCRIPT_BRANCH="master" +SCRIPT_BRANCH="dev" ##----------------------------------------## ## Modified by Martinski W. [2024-Jul-03] ## @@ -4647,7 +4647,7 @@ _GetRawKeypress_() fi local savedSettings="$(stty -g)" stty -icanon -echo - dd bs=4 count=1 2>/dev/null + dd bs=64 count=1 2>/dev/null stty "$savedSettings" stty -echo } @@ -4774,6 +4774,7 @@ _GetKeypressInput_() inputString="" inputStrLen=0 keypressCnt=0 + keypressLen=0 prevxStrLen=0 _ClearKeySeqState_ _ShowInputString_ @@ -4782,7 +4783,8 @@ _GetKeypressInput_() do theChar="$(_GetRawKeypress_)" charNum="$(printf "%d" "'$theChar")" - keypressCnt="$((keypressCnt + 1))" + keypressLen="${#theChar}" + keypressCnt="$((keypressCnt + keypressLen))" #### if echo "$charNum" | grep -qE "^(0|10|13)$" @@ -4980,6 +4982,7 @@ _GetPasswordInput_() charNum="" showPSWD=0 keypressCnt=0 + keypressLen=0 prevxStrLen=0 newPSWDstring="$thePWSDstring" newPSWDlength="${#newPSWDstring}" @@ -4990,7 +4993,8 @@ _GetPasswordInput_() do theChar="$(_GetRawKeypress_)" charNum="$(printf "%d" "'$theChar")" - keypressCnt="$((keypressCnt + 1))" + keypressLen="${#theChar}" + keypressCnt="$((keypressCnt + keypressLen))" #### if echo "$charNum" | grep -qE "^(0|10|13)$" @@ -6140,8 +6144,8 @@ _CheckOnlineFirmwareSHA256_() return 1 fi - checksumSource="FwUpdate VPS mirror" - Say "${MGNTct}*WARNING*${NOct}: Using the FwUpdate VPS checksum mirror for verification." + checksumSource="RMerlin's F/W Update VPS mirror" + Say "${MGNTct}*WARNING*${NOct}: Using RMerlin's F/W Update VPS checksum mirror for verification." fi #--------------------------------------------------------------------------# diff --git a/README.md b/README.md index 814a3d5b..f98791ae 100644 --- a/README.md +++ b/README.md @@ -1,7 +1,7 @@ # MerlinAU - AsusWRT-Merlin Firmware Auto Updater -## v1.6.8 -## 2026-Sep-07 +## v1.6.9 +## 2026-Sep-17 ## WebUI: image diff --git a/version.txt b/version.txt index d8c5e721..15d45d4b 100644 --- a/version.txt +++ b/version.txt @@ -1 +1 @@ -1.6.8 +1.6.9 From 22cc42468c7b4690adf891ca97d1d0ad0d218ad9 Mon Sep 17 00:00:00 2001 From: Martinski4GitHub <119833648+Martinski4GitHub@users.noreply.github.com> Date: Sun, 20 Sep 2026 17:02:40 -0700 Subject: [PATCH 06/26] Fixes and Improvements - Fixed bug due to restarting the HTTP daemon right before starting to flash the F/W image. This was causing the previously authenticated login session token/cookie to become invalid. - Modified and improved functions used to login to the primary router and AiMesh nodes. - Added code to double-check that the current login session token/cookie is still valid before proceeding to flash the F/W image. If it's not valid, a 2nd login is attempted. If that fails, the router is rebooted to make sure the router goes back to a fresh state. - Miscellaneous improvements and fine-tuning. --- MerlinAU.sh | 441 +++++++++++++++++++++++++++++++++++++--------------- README.md | 2 +- 2 files changed, 315 insertions(+), 128 deletions(-) diff --git a/MerlinAU.sh b/MerlinAU.sh index 3ca78831..1fd8697e 100644 --- a/MerlinAU.sh +++ b/MerlinAU.sh @@ -4,7 +4,7 @@ # # Project Created: 2023-Oct-01 by @ExtremeFiretop # Official Co-Author: @Martinski W. since 2023-Nov-01 -# Last Modified: 2026-Sep-17 +# Last Modified: 2026-Sep-20 # # MerlinAU™ / MerlinAutoUpdate™ # Official project: https://github.com/ExtremeFiretop/MerlinAutoUpdate-Router @@ -20,7 +20,7 @@ set -u ## Set version for each Production Release ## readonly SCRIPT_VERSION=1.6.9 -readonly SCRIPT_VERSTAG="26091700" +readonly SCRIPT_VERSTAG="26092015" readonly SCRIPT_NAME="MerlinAU" ## Set to "master" for Production Releases ## SCRIPT_BRANCH="dev" @@ -89,6 +89,10 @@ readonly InvBGRNct="\e[30;102m" readonly InvBYLWct="\e[30;103m" readonly InvBMGNct="\e[30;105m" +readonly pLogERROR=3 +readonly pLogWARNG=4 +readonly logTagSTR="${SCRIPT_NAME}_[$$]" + readonly ScriptFileName="${0##*/}" readonly ScriptFNameTag="${ScriptFileName%%.*}" readonly ScriptDirNameD="${ScriptFNameTag}.d" @@ -124,9 +128,13 @@ readonly TEMPFILE="/tmp/MerlinAU_settings_$$.txt" readonly webPageFileRegExp="user([1-9]|[1-2][0-9])[.]asp" readonly webPageLineTabExp="\{url: \"$webPageFileRegExp\", tabName: " readonly webPageLineRegExp="${webPageLineTabExp}\"$SCRIPT_NAME\"\}," -readonly curlHTTPstatusStr="HTTP/S_Status_Code" -readonly curlTmpLogFile="${TEMP_DIR}/tmpCurl_${ScriptFNameTag}_$$.TMP.LOG" -readonly curlErrLogFile="${TEMP_DIR}/tmpCurl_${ScriptFNameTag}_$$.ERR.LOG" +readonly curlHTTPstatusStr="HTTP_Status_Code" +readonly curlTmpLogFPath="${TEMP_DIR}/tmpCurl_${ScriptFNameTag}_$$.TMP.LOG" +readonly curlErrLogFPath="${TEMP_DIR}/tmpCurl_${ScriptFNameTag}_$$.ERR.LOG" +readonly curlTmpRespFile="${TEMP_DIR}/tmpCurl_${ScriptFNameTag}_$$.RESP.TXT" + +# Temporary NVRAM key to indicate when a F/W Update is in progress # +readonly nvramTempFWupdateKey="merlinau_fw_update" # Give FIRST priority to built-in binaries over any other # export PATH="/bin:/usr/bin:/sbin:/usr/sbin:$PATH" @@ -207,7 +215,7 @@ fi if [ "$isInteractive" = "false" ] && { [ $# -eq 0 ] || [ -z "$1" ] ; } then - logger -st "${SCRIPT_NAME}_[$$]" -p 3 "**ERROR**: CLI Menu is NOT available in a non-interactive shell" + logger -st "$logTagSTR" -p "$pLogERROR" "**ERROR**: CLI Menu is NOT available in a non-interactive shell" exit 1 fi @@ -374,7 +382,22 @@ Say() logMsg="$(echo "$1" | \ sed 's/\\e\[[0-1]m//g; s/\\e\[[3-4][0-9]m//g; s/\\e\[[0-1];[3-4][0-9]m//g; s/\\e\[30;10[1-9]m//g; s/\\n/ /g')" _UserLogMsg_ "$logMsg" - printf "$logMsg" | logger -t "${SCRIPT_NAME}_[$$]" + printf "$logMsg" | logger -t "$logTagSTR" +} + +##----------------------------------------## +## Modified by Martinski W. [2026-Sep-20] ## +##----------------------------------------## +_MsgToSysLog_() +{ + local logPrioNum + + if [ $# -gt 1 ] && [ -n "$2" ] && \ + echo "$2" | grep -qE '^[1-6]$' + then logPrioNum="$2" + else logPrioNum="$pLogWARNG" + fi + logger -st "$logTagSTR" -p "$logPrioNum" "$1" } ##----------------------------------------## @@ -2923,17 +2946,18 @@ _CurlFileDownload_() local curlRetCode returnCODE statusSTRx httpStatusSTR rm -f "$tempFilePathDL" - printf '' > "$curlErrLogFile" ; printf '' > "$curlTmpLogFile" + printf '' > "$curlErrLogFPath" + printf '' > "$curlTmpLogFPath" curl -LSs --retry 3 --retry-delay 5 --retry-connrefused \ --connect-timeout 30 --max-time 60 \ - -w "${curlHTTPstatusStr}: %{http_code}\n" --stderr "$curlErrLogFile" \ - "$srceFilePathURL" --output "$tempFilePathDL" >> "$curlTmpLogFile" + -w "${curlHTTPstatusStr}: %{http_code}\n" --stderr "$curlErrLogFPath" \ + "$srceFilePathURL" --output "$tempFilePathDL" >> "$curlTmpLogFPath" curlRetCode="$?" returnCODE="$curlRetCode" statusSTRx="Curl Status Code: $curlRetCode" - httpStatusSTR="$(grep -oE "${curlHTTPstatusStr}: [4-5][0-9]{2,}" "$curlTmpLogFile")" + httpStatusSTR="$(grep -oE "${curlHTTPstatusStr}: [4-5][0-9]{2,}" "$curlTmpLogFPath")" if [ "$curlRetCode" -eq 0 ] && \ [ -z "$httpStatusSTR" ] && [ -s "$tempFilePathDL" ] @@ -2953,16 +2977,16 @@ _CurlFileDownload_() if [ "$curlRetCode" -eq 0 ] && [ -n "$httpStatusSTR" ] then returnCODE="$(echo "$httpStatusSTR" | awk -F' ' '{print $2}')" - statusSTRx="HTTP/S Status Code: $returnCODE" + statusSTRx="HTTP Status Code: $returnCODE" fi - if [ -s "$curlErrLogFile" ] && "$isInteractive" - then echo ; cat "$curlErrLogFile" + if [ -s "$curlErrLogFPath" ] && "$isInteractive" + then echo ; cat "$curlErrLogFPath" fi Say "${REDct}**ERROR**${NOct}: Unable to download the file [$2] [${MGNTct}${statusSTRx}${NOct}]" rm -f "$tempFilePathDL" fi - rm -f "$curlErrLogFile" "$curlTmpLogFile" + rm -f "$curlErrLogFPath" "$curlTmpLogFPath" return "$returnCODE" } @@ -3703,22 +3727,16 @@ _CreateEMailContent_() ! "$isEMailFormatHTML" && sed -i 's/[<]b[>]//g ; s/[<]\/b[>]//g' "$tempEMailBodyMsg" - if [ -n "$CC_NAME" ] && [ -n "$CC_ADDRESS" ] - then - CC_ADDRESS_ARG="--mail-rcpt $CC_ADDRESS" - CC_ADDRESS_STR="\"${CC_NAME}\" <$CC_ADDRESS>" - fi - - ## Header-1 ## + ## Header-1a ## cat < "$tempEMailContent" From: "$FROM_NAME" <$FROM_ADDRESS> To: "$TO_NAME" <$TO_ADDRESS> EOF - [ -n "$CC_ADDRESS_STR" ] && \ - printf "Cc: %s\n" "$CC_ADDRESS_STR" >> "$tempEMailContent" + [ -n "$CC_ADDRESS_OK" ] && \ + printf "Cc: \"$CC_NAME\" <$CC_ADDRESS>\n" >> "$tempEMailContent" - ## Header-2 ## + ## Header-1b ## cat <> "$tempEMailContent" Subject: $subjectStr Date: $(date -R) @@ -3729,6 +3747,7 @@ EOF cat <> "$tempEMailContent" MIME-Version: 1.0 Content-Type: text/html; charset="UTF-8" +Content-Transfer-Encoding: 8bit Content-Disposition: inline @@ -3738,6 +3757,7 @@ Content-Disposition: inline EOF else cat <> "$tempEMailContent" +MIME-Version: 1.0 Content-Type: text/plain; charset="UTF-8" Content-Transfer-Encoding: quoted-printable Content-Disposition: inline @@ -3747,7 +3767,7 @@ EOF printf "%s\n\n" "$emailBodyTitle" >> "$tempEMailContent" fi - ## Body ## + ## Email Body ## cat "$tempEMailBodyMsg" >> "$tempEMailContent" ## Footer ## @@ -3755,7 +3775,7 @@ EOF then cat <> "$tempEMailContent" -Sent by the "${ScriptFNameTag}" utility. +Sent by the "${ScriptFNameTag}" script. From the "${FRIENDLY_ROUTER_NAME}" router. $(date +"$theEMailDateTimeFormat") @@ -3764,7 +3784,7 @@ EOF else cat <> "$tempEMailContent" -Sent by the "${ScriptFNameTag}" utility. +Sent by the "${ScriptFNameTag}" script. From the "${FRIENDLY_ROUTER_NAME}" router. $(date +"$theEMailDateTimeFormat") @@ -3846,39 +3866,78 @@ _SendEMailNotification_() ! _CheckEMailConfigFileFromAMTM_ 1 then return 1 ; fi - local CC_ADDRESS_STR="" CC_ADDRESS_ARG="" + local CC_ADDRESS_OK="" mailpswd + local curlRetCode statusCODE statusSTRx httpStatusSTR [ -z "$FROM_NAME" ] && FROM_NAME="$ScriptFNameTag" [ -z "$FRIENDLY_ROUTER_NAME" ] && FRIENDLY_ROUTER_NAME="$MODEL_ID" + if [ -n "$CC_NAME" ] && [ -n "$CC_ADDRESS" ] + then CC_ADDRESS_OK=TRUE + fi + ! _CreateEMailContent_ "$1" && return 1 if "$isInteractive" then - printf "\nSending email notification [$1]." + printf "\nSending email notification [${GRNct}${1}${NOct}]." printf "\nPlease wait...\n" fi - _UserTraceLog_ "SENDING email notification..." + _UserTraceLog_ "SENDING email notification [$1]..." + + mailpswd="$(openssl aes-256-cbc "$emailPwEnc" -d -in "$amtmMailPswdFile" -pass pass:ditbabot,isoi)" + if [ -z "$mailpswd" ] + then + Say "${REDct}**ERROR**${NOct}: Failure to extract email password." + return 1 + fi + + printf '' > "$curlErrLogFPath" + printf '' > "$curlTmpLogFPath" - curl -Lv --retry 4 --retry-delay 5 --url "${PROTOCOL}://${SMTP}:${PORT}" \ - --mail-from "$FROM_ADDRESS" --mail-rcpt "$TO_ADDRESS" $CC_ADDRESS_ARG \ - --user "${USERNAME}:$(/usr/sbin/openssl aes-256-cbc "$emailPwEnc" -d -in "$amtmMailPswdFile" -pass pass:ditbabot,isoi)" \ + curl -vLSs --retry 3 --retry-delay 5 --retry-connrefused \ + --connect-timeout 30 --max-time 60 \ + -w "${curlHTTPstatusStr}: %{http_code}\n" \ + --output /dev/null --stderr "$curlErrLogFPath" \ + --url "${PROTOCOL}://${SMTP}:${PORT}" \ + --user "${USERNAME}:$mailpswd" \ + --mail-from "$FROM_ADDRESS" --mail-rcpt "$TO_ADDRESS" \ + ${CC_ADDRESS_OK:+--mail-rcpt "$CC_ADDRESS"} \ --upload-file "$tempEMailContent" \ - $SSL_FLAG --ssl-reqd --crlf >> "$userTraceFile" 2>&1 - curlCode="$?" + $SSL_FLAG --ssl-reqd --crlf >> "$curlTmpLogFPath" + curlRetCode="$?" - if [ "$curlCode" -eq 0 ] + statusCODE="$curlRetCode" + statusSTRx="Curl Status Code: $curlRetCode" + httpStatusSTR="$(grep -oE "${curlHTTPstatusStr}: [4-5][0-9]{2,}" "$curlTmpLogFPath")" + + if [ "$curlRetCode" -eq 0 ] && [ -z "$httpStatusSTR" ] then - sleep 2 rm -f "$userTraceFile" - Say "The email notification was sent successfully [$1]." + Say "The email notification [${GRNct}${1}${NOct}] was sent successfully." else - Say "${REDct}**ERROR**${NOct}: Failure to send email notification [Code: $curlCode][$1]." + if [ "$curlRetCode" -eq 0 ] && [ -n "$httpStatusSTR" ] + then + statusCODE="$(echo "$httpStatusSTR" | awk -F' ' '{print $2}')" + statusSTRx="HTTP Status Code: $statusCODE" + cat "$curlTmpLogFPath" >> "$userTraceFile" + fi + Say "${REDct}**ERROR**${NOct}: Failure to send email notification [${MGNTct}${1}${NOct}] [${REDct}${statusSTRx}${NOct}]." + + if [ -s "$curlErrLogFPath" ] && "$isInteractive" + then + echo "=======================================================" + cat "$curlErrLogFPath" + echo "=======================================================" + fi fi - rm -f "$tempEMailContent" + mailpswd='XXXXXXXXXXXXXXX' ; unset mailpswd + sleep 2 - return "$curlCode" + rm -f "$tempEMailContent" + rm -f "$curlErrLogFPath" "$curlTmpLogFPath" + return "$statusCODE" } ##----------------------------------------## @@ -4353,9 +4412,9 @@ _DoCleanUp_() [ $# -gt 1 ] && [ "$2" -eq 1 ] && keepZIPfile=true [ $# -gt 2 ] && [ "$3" -eq 1 ] && keepWfile=true - # Clear the volatile F/W-update guard used by AiMesh primaries. # - # This value is intentionally never committed to NVRAM. # - nvram unset merlinau_fw_update 2>/dev/null + # Clear the NVRAM F/W-update guard used by AiMesh nodes # + # This value is intentionally never committed to NVRAM # + nvram unset "$nvramTempFWupdateKey" 2>/dev/null # Stop the LEDs blinking # _Reset_LEDs_ 1 @@ -4533,21 +4592,24 @@ _CheckForMinimumModelSupport_() "$routerModelCheckFailed" && return 1 || return 0 } -##------------------------------------------## -## Modified by ExtremeFiretop [2026-Jul-30] ## -##------------------------------------------## +##----------------------------------------## +## Modified by Martinski W. [2026-Sep-20] ## +##----------------------------------------## _DoMainRouterLogin_() { if [ $# -lt 3 ] || [ -z "$1" ] || [ -z "$2" ] || [ -z "$3" ] - then - echo ; return 1 + then echo ; return 1 fi - local routerURL="$1" - local credsENC="$2" - local cookieFile="$3" - local curlCode curlResponse + local routerURL="$1" credsENC="$2" cookieFile="$3" + local responseFPath="${curlTmpRespFile}.MAIN.LOGIN" + local curlRetCode statusCODE statusSTRx httpStatusSTR + + printf '' > "$responseFPath" + printf '' > "$curlErrLogFPath" + printf '' > "$curlTmpLogFPath" - curlResponse="$(curl -k "${routerURL}/login.cgi" \ + curl -kiLSs "${routerURL}/login.cgi" \ + --connect-timeout 10 --max-time 15 \ --referer "${routerURL}/Main_Login.asp" \ --user-agent 'Mozilla/5.0 (X11; Linux x86_64; rv:109.0) Gecko/20100101 Firefox/115.0' \ -H 'Accept-Language: en-US,en;q=0.5' \ @@ -4556,11 +4618,34 @@ _DoMainRouterLogin_() -H 'Connection: keep-alive' \ --data-raw "group_id=&action_mode=&action_script=&action_wait=5¤t_page=Main_Login.asp&next_page=index.asp" \ --data-urlencode "login_authorization=$credsENC" \ - --cookie-jar "$cookieFile")" - curlCode="$?" + --cookie-jar "$cookieFile" \ + -w "${curlHTTPstatusStr}: %{http_code}\n" --stderr "$curlErrLogFPath" \ + --output "$responseFPath" >> "$curlTmpLogFPath" + curlRetCode="$?" - echo "$curlResponse" - return "$curlCode" + statusCODE="$curlRetCode" + statusSTRx="Curl Status Code: $curlRetCode" + httpStatusSTR="$(grep -oE "${curlHTTPstatusStr}: [4-5][0-9]{2,}" "$curlTmpLogFPath")" + + if [ "$curlRetCode" -eq 0 ] && \ + [ -z "$httpStatusSTR" ] && [ -s "$responseFPath" ] + then + if ! grep -qE 'url=index[.]asp|url=GameDashboard[.]asp' "$responseFPath" + then + statusCODE=777 + statusSTRx="Login Failure Code: $statusCODE" + fi + else + if [ "$curlRetCode" -eq 0 ] && [ -n "$httpStatusSTR" ] + then + statusCODE="$(echo "$httpStatusSTR" | awk -F' ' '{print $2}')" + statusSTRx="HTTP Status Code: $statusCODE" + fi + fi + + rm -f "$curlErrLogFPath" "$curlTmpLogFPath" "$responseFPath" + echo "$statusSTRx" + return "$statusCODE" } ##----------------------------------------## @@ -4568,7 +4653,7 @@ _DoMainRouterLogin_() ##----------------------------------------## _TestLoginCredentials_() { - local credsENC routerURL cookieFile curlResponse retCode + local credsENC routerURL cookieFile curlStatus retCode if [ $# -gt 0 ] && [ -n "$1" ] then credsENC="$1" @@ -4582,8 +4667,7 @@ _TestLoginCredentials_() /sbin/service restart_httpd >/dev/null 2>&1 sleep 4 - if curlResponse="$(_DoMainRouterLogin_ "$routerURL" "$credsENC" "$cookieFile")" && \ - echo "$curlResponse" | grep -Eq 'url=index\.asp|url=GameDashboard\.asp' + if curlStatus="$(_DoMainRouterLogin_ "$routerURL" "$credsENC" "$cookieFile")" then _UpdateLoginPswdCheckHelper_ SUCCESS printf "\n${GRNct}Router Login test passed.${NOct}" @@ -4593,7 +4677,7 @@ _TestLoginCredentials_() retCode=0 else _UpdateLoginPswdCheckHelper_ FAILURE - printf "\n${REDct}**ERROR**${NOct}: Router Login test failed.\n" + printf "\n${REDct}**ERROR**${NOct}: Router Login test failed [$curlStatus].\n" printf "\n${routerLoginFailureMsg}\n\n" if _WaitForYESorNO_ "Would you like to try again?" then retCode=1 # Indicates failure but with intent to retry # @@ -5415,14 +5499,18 @@ _GetNodeURL_() _DoMeshNodeLogin_() { if [ $# -lt 3 ] || [ -z "$1" ] || [ -z "$2" ] || [ -z "$3" ] - then - return 1 + then echo ; return 1 fi - local nodeURL="$1" - local credsENC="$2" - local cookieFile="$3" + local nodeURL="$1" credsENC="$2" cookieFile="$3" + local responseFPath="${curlTmpRespFile}.NODE.LOGIN" + local curlRetCode statusCODE statusSTRx httpStatusSTR + + printf '' > "$responseFPath" + printf '' > "$curlErrLogFPath" + printf '' > "$curlTmpLogFPath" - curl -s -k "${nodeURL}/login.cgi" \ + curl -kiLSs "${nodeURL}/login.cgi" \ + --connect-timeout 10 --max-time 15 \ --referer "${nodeURL}/Main_Login.asp" \ --user-agent 'Mozilla/5.0 (X11; Linux x86_64; rv:109.0) Gecko/20100101 Firefox/115.0' \ -H 'Accept-Language: en-US,en;q=0.5' \ @@ -5432,15 +5520,97 @@ _DoMeshNodeLogin_() --data-raw "group_id=&action_mode=&action_script=&action_wait=5¤t_page=Main_Login.asp&next_page=index.asp" \ --data-urlencode "login_authorization=$credsENC" \ --cookie-jar "$cookieFile" \ - --max-time 3 >/dev/null 2>&1 + -w "${curlHTTPstatusStr}: %{http_code}\n" --stderr "$curlErrLogFPath" \ + --output "$responseFPath" >> "$curlTmpLogFPath" + curlRetCode="$?" - return "$?" + statusCODE="$curlRetCode" + statusSTRx="Curl Status Code: $curlRetCode" + httpStatusSTR="$(grep -oE "${curlHTTPstatusStr}: [4-5][0-9]{2,}" "$curlTmpLogFPath")" + + if [ "$curlRetCode" -eq 0 ] && \ + [ -z "$httpStatusSTR" ] && [ -s "$responseFPath" ] + then + ## MUST check & verify *IF* this is TRUE for AiMesh Nodes ## + if ! grep -qE 'url=index[.]asp|url=GameDashboard[.]asp' "$responseFPath" + then + statusCODE=788 + statusSTRx="Login Failure Code: $statusCODE" + fi + else + if [ "$curlRetCode" -eq 0 ] && [ -n "$httpStatusSTR" ] + then + statusCODE="$(echo "$httpStatusSTR" | awk -F' ' '{print $2}')" + statusSTRx="HTTP Status Code: $statusCODE" + fi + fi + + rm -f "$curlErrLogFPath" "$curlTmpLogFPath" "$responseFPath" + echo "$statusSTRx" + return "$statusCODE" } -##------------------------------------------## -## Modified by ExtremeFiretop [2026-Sep-16] ## -##------------------------------------------## -# Trigger the node "Check for updates" (no waiting here) +##-------------------------------------## +## Added by Martinski W. [2026-Sep-20] ## +##-------------------------------------## +_GetNVRAM_FromWebUI_() +{ + if [ $# -lt 3 ] || [ -z "$1" ] || [ -z "$2" ] || [ -z "$3" ] + then echo ; return 1 + fi + local webUIcURL="$1" cookieFile="$2" nvramKey="$3" + local responseFPath="${curlTmpRespFile}.NVRAM.TMP" + local curlRetCode statusCODE statusSTRx httpStatusSTR + local nvramKeyValPair="" + + printf '' > "$responseFPath" + printf '' > "$curlErrLogFPath" + printf '' > "$curlTmpLogFPath" + + curl -kiLSs "${webUIcURL}/appGet.cgi?hook=nvram_get($nvramKey)" \ + --connect-timeout 10 --max-time 15 \ + -H 'User-Agent: Mozilla/5.0 (X11; Linux x86_64; rv:109.0) Gecko/20100101 Firefox/115.0' \ + -H 'Accept: application/json,text/plain,*/*' \ + -H 'Accept-Language: en-US,en;q=0.5' \ + -H 'Connection: keep-alive' \ + -H "Referer: ${webUIcURL}/index.asp" \ + --cookie "$cookieFile" \ + -w "${curlHTTPstatusStr}: %{http_code}\n" --stderr "$curlErrLogFPath" \ + --output "$responseFPath" >> "$curlTmpLogFPath" + curlRetCode="$?" + + statusCODE="$curlRetCode" + statusSTRx="Curl Status Code: $curlRetCode" + httpStatusSTR="$(grep -oE "${curlHTTPstatusStr}: [4-5][0-9]{2,}" "$curlTmpLogFPath")" + + if [ "$curlRetCode" -eq 0 ] && \ + [ -z "$httpStatusSTR" ] && [ -s "$responseFPath" ] + then + if grep -Eq "href='/Main_Login[.]asp'|login[.]cgi" "$responseFPath" + then + statusCODE=799 + statusSTRx="WebUI Session Failure Code: $statusCODE" + else + nvramKeyValPair="$(grep -oE "\"$nvramKey\":\"[^\"]*\"" "$responseFPath")" + fi + else + if [ "$curlRetCode" -eq 0 ] && [ -n "$httpStatusSTR" ] + then + statusCODE="$(echo "$httpStatusSTR" | awk -F' ' '{print $2}')" + statusSTRx="HTTP Status Code: $statusCODE" + fi + fi + [ -z "$nvramKeyValPair" ] && nvramKeyValPair="$statusSTRx" + + rm -f "$curlErrLogFPath" "$curlTmpLogFPath" "$responseFPath" + echo "$nvramKeyValPair" + return "$statusCODE" +} + +##----------------------------------------## +## Modified by Martinski W. [2026-Sep-20] ## +##----------------------------------------## +# Trigger the node "Check for updates" (no waiting here) # _MeshNodeTriggerFWCheck_() { if [ $# -lt 2 ] || [ -z "$1" ] || [ -z "$2" ] @@ -5451,7 +5621,7 @@ _MeshNodeTriggerFWCheck_() local safeID="$(_MeshSafeID_ "$nodeIPv4addr")" local nodeURL="$(_GetNodeURL_ "$nodeIPv4addr")" local cookieFile="/tmp/${runID}.${safeID}.cookie" - local nodeBusy nodeBusyRC + local curlStatus nvramKeyPair # Check for Login Credentials # credsENC="$(Get_Custom_Setting credentials_base64)" @@ -5463,35 +5633,32 @@ _MeshNodeTriggerFWCheck_() return 1 fi - if _DoMeshNodeLogin_ "$nodeURL" "$credsENC" "$cookieFile" + if curlStatus="$(_DoMeshNodeLogin_ "$nodeURL" "$credsENC" "$cookieFile")" then Say "${GRNct}Successful Login for AiMesh Node [$nodeIPv4addr].${NOct}" else rm -f "$cookieFile" - Say "${REDct}Failed Login for AiMesh Node [$nodeIPv4addr].${NOct}" + Say "${REDct}Failed Login for AiMesh Node [$nodeIPv4addr] [$curlStatus].${NOct}" return 1 fi + #-----------------------------------------------------------------------# # Check if the AiMesh node is already performing a MerlinAU F/W update - # before triggering the built-in firmware update check. - nodeBusy="$(curl -s -k "${nodeURL}/appGet.cgi?hook=nvram_get(merlinau_fw_update)" \ - -H 'User-Agent: Mozilla/5.0 (X11; Linux x86_64; rv:109.0) Gecko/20100101 Firefox/115.0' \ - -H 'Accept: application/json,text/plain,*/*' \ - -H 'Accept-Language: en-US,en;q=0.5' \ - -H 'Connection: keep-alive' \ - -H "Referer: ${nodeURL}/index.asp" \ - --cookie "$cookieFile" \ - --max-time 2 2>/dev/null)" - nodeBusyRC="$?" - - if [ "$nodeBusyRC" -eq 0 ] && echo "$nodeBusy" | grep -Eq '"merlinau_fw_update"[[:space:]]*:[[:space:]]*"1"' + # *BEFORE* triggering the built-in firmware update check. + #-----------------------------------------------------------------------# + if nvramKeyPair="$(_GetNVRAM_FromWebUI_ "$nodeURL" "$cookieFile" "$nvramTempFWupdateKey")" then - Say "AiMesh Node [$nodeIPv4addr] entered an active MerlinAU F/W update before start_webs_update. Skipping firmware check." - return 0 + if echo "$nvramKeyPair" | grep -qE "\"$nvramTempFWupdateKey\"[[:blank:]]*:[[:blank:]]*\"1\"" + then + Say "AiMesh Node [$nodeIPv4addr] entered an active MerlinAU F/W update before start_webs_update. Skipping firmware check." + return 0 + fi fi + #-----------------------------------------------------# # Trigger firmware check (mimic WebUI "Check" button) # - curl -s -k "${nodeURL}/start_apply.htm" \ + #-----------------------------------------------------# + curl -sk "${nodeURL}/start_apply.htm" \ --referer "${nodeURL}/Advanced_FirmwareUpgrade_Content.asp" \ --user-agent 'Mozilla/5.0 (X11; Linux x86_64; rv:109.0) Gecko/20100101 Firefox/115.0' \ -H 'Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8' \ @@ -5520,7 +5687,7 @@ _GetNodeInfo_() then echo "**ERROR** **NO_PARAMS**" ; return 1 fi - local curlCode htmlContent + local curlCode htmlContent curlStatus='' local nodeIPv4addr="$1" runID="$2" local safeID="$(_MeshSafeID_ "$nodeIPv4addr")" local nodeURL="$(_GetNodeURL_ "$nodeIPv4addr")" @@ -5557,10 +5724,10 @@ _GetNodeInfo_() # If already created a cookie, reuse it (skip login), else perform login request # if [ ! -s "$cookieFile" ] && \ - ! _DoMeshNodeLogin_ "$nodeURL" "$credsENC" "$cookieFile" + ! curlStatus="$(_DoMeshNodeLogin_ "$nodeURL" "$credsENC" "$cookieFile")" then rm -f "$cookieFile" - Say "${REDct}Failed Login for AiMesh Node [$nodeIPv4addr].${NOct}" + Say "${REDct}Failed Login for AiMesh Node [$nodeIPv4addr] [$curlStatus].${NOct}" return 1 fi @@ -6069,17 +6236,18 @@ _GetChecksumsFromRMerlinWebsite_() theChecksums="" rm -f "$outTempFPathDL" - printf '' > "$curlErrLogFile" ; printf '' > "$curlTmpLogFile" + printf '' > "$curlErrLogFPath" + printf '' > "$curlTmpLogFPath" curl -LSs --retry 3 --retry-delay 5 --retry-connrefused \ --connect-timeout 30 --max-time 60 \ - -w "${curlHTTPstatusStr}: %{http_code}\n" --stderr "$curlErrLogFile" \ - "$FW_SHA256_URL" --output "$outTempFPathDL" >> "$curlTmpLogFile" + -w "${curlHTTPstatusStr}: %{http_code}\n" --stderr "$curlErrLogFPath" \ + "$FW_SHA256_URL" --output "$outTempFPathDL" >> "$curlTmpLogFPath" curlRetCode="$?" returnCODE="$curlRetCode" statusSTRx="Curl Status Code: $curlRetCode" - httpStatusSTR="$(grep -oE "${curlHTTPstatusStr}: [4-5][0-9]{2,}" "$curlTmpLogFile")" + httpStatusSTR="$(grep -oE "${curlHTTPstatusStr}: [4-5][0-9]{2,}" "$curlTmpLogFPath")" if [ "$curlRetCode" -eq 0 ] && \ [ -z "$httpStatusSTR" ] && [ -s "$outTempFPathDL" ] @@ -6095,15 +6263,15 @@ _GetChecksumsFromRMerlinWebsite_() if [ "$curlRetCode" -eq 0 ] && [ -n "$httpStatusSTR" ] then returnCODE="$(echo "$httpStatusSTR" | awk -F' ' '{print $2}')" - statusSTRx="HTTP/S Status Code: $returnCODE" + statusSTRx="HTTP Status Code: $returnCODE" fi - if [ -s "$curlErrLogFile" ] && "$isInteractive" - then echo ; cat "$curlErrLogFile" + if [ -s "$curlErrLogFPath" ] && "$isInteractive" + then echo ; cat "$curlErrLogFPath" fi Say "${MGNTct}*WARNING*${NOct}: Unable to download the SHA256 checksum signature list from the ASUSWRT-Merlin website [${MGNTct}${statusSTRx}${NOct}]" fi - rm -f "$curlErrLogFile" "$curlTmpLogFile" "$outTempFPathDL" + rm -f "$curlErrLogFPath" "$curlTmpLogFPath" "$outTempFPathDL" return "$returnCODE" } @@ -9715,8 +9883,6 @@ _Unmount_Eject_USB_Drives_() local ejectUSB_OK=false ejectUSB_PID="" usbMountPoint="" local logMsg="Unmount/Eject USB Drive" - _MsgToSysLog_() { logger -st "${SCRIPT_NAME}_[$$]" -p 4 "$1" ; } - _MsgToSysLog_ "START of ${logMsg}..." /sbin/ejusb -1 0 -u 1 2>/dev/null & ejectUSB_PID=$! @@ -10235,7 +10401,7 @@ Please manually update to version ${GRNct}${MinSupportedFirmwareVers}${NOct} or routerURL="$(_GetRouterURL_)" Say "Router Web URL is: ${routerURL}" - cookieFile="/tmp/FW_UpgradeCookie.txt" + cookieFile="/tmp/MerlinAU_FW_UpgradeCookie.txt" if "$isInteractive" then @@ -10250,13 +10416,13 @@ Please manually update to version ${GRNct}${MinSupportedFirmwareVers}${NOct} or fi fi - #------------------------------------------------------------------------# - # A volatile guard before restarting/logging into the WebGUI. - # Primary routers running MerlinAU can query this nvram value with appGet.cgi - # and avoid triggering start_webs_update on this router mid-flash. - # Do not commit this value since a reboot should clear it automatically. - #------------------------------------------------------------------------# - nvram set merlinau_fw_update=1 + #-------------------------------------------------------------------# + # NVRAM key guard set BEFORE restarting/logging into the WebGUI. + # Primary routers running MerlinAU can query this NVRAM value and + # avoid triggering 'start_webs_update' on AiMesh nodes mid-flash. + # Do *NOT* commit this key value since a reboot must clear it. + #-------------------------------------------------------------------# + nvram set "$nvramTempFWupdateKey"=1 rm -f "$fwUploadResponseFile" "$fwUploadDiagFile" # Send last email notification before F/W flash # @@ -10292,11 +10458,18 @@ Please manually update to version ${GRNct}${MinSupportedFirmwareVers}${NOct} or requiredDIVER_version="$(_ScriptVersionStrToNum_ "5.2.0")" fi + #------------------------------------------------------------# + # Restart the WebGUI to make sure nobody else is logged in + # so that the F/W Update can start *without* interruptions. + #------------------------------------------------------------# + "$isInteractive" && printf "\nRestarting web server... Please wait.\n" + /sbin/service restart_httpd >/dev/null 2>&1 & + sleep 3 + ##----------------------------------------## ## Modified by Martinski W. [2026-Jan-01] ## ##----------------------------------------## - if curlResponse="$(_DoMainRouterLogin_ "$routerURL" "$credsENC" "$cookieFile")" && \ - echo "$curlResponse" | grep -Eq 'url=index\.asp|url=GameDashboard\.asp' + if curlStatus="$(_DoMainRouterLogin_ "$routerURL" "$credsENC" "$cookieFile")" then _UpdateLoginPswdCheckHelper_ SUCCESS @@ -10355,13 +10528,27 @@ Please manually update to version ${GRNct}${MinSupportedFirmwareVers}${NOct} or Say "Flashing ${GRNct}${firmware_file}${NOct}...\n${REDct}Please wait for reboot in about 4 minutes or less.${NOct}" echo - #------------------------------------------------------------# - # Restart the WebGUI to make sure nobody else is logged in - # so that the F/W Update can start without interruptions. - #------------------------------------------------------------# - "$isInteractive" && printf "\nRestarting web server... Please wait.\n" - /sbin/service restart_httpd >/dev/null 2>&1 & - sleep 3 + #-------------------------------------------------------------------# + # Double-check IF the existing Cookie is still valid. If it's not, + # attempt to get a NEW login session Cookie by logging in again. + # If this login fails now then we have to abort here and reboot. + # Added by Martinski W. [2026-Sep-20] + #-------------------------------------------------------------------# + if ! nvramKeyPair="$(_GetNVRAM_FromWebUI_ "$routerURL" "$cookieFile" "$nvramTempFWupdateKey")" + then + rm -f "$cookieFile" + if ! curlStatus="$(_DoMainRouterLogin_ "$routerURL" "$credsENC" "$cookieFile")" + then + rm -f "$cookieFile" + _MsgToSysLog_ "**ERROR**: Router Login 2nd Attempt Failed [$curlStatus]." "$pLogERROR" + _MsgToSysLog_ "*WARNING*: Router will be rebooted at this point." + _SendEMailNotification_ FAILED_FW_UPDATE_STATUS + _DoCleanUp_ 1 "$keepZIPfile" "$keepWfile" + _ReleaseLock_ ; sleep 2 + /sbin/service reboot + return 1 + fi + fi #----------------------------------------------------------------------------------# # **IMPORTANT NOTE**: @@ -10428,8 +10615,8 @@ Please manually update to version ${GRNct}${MinSupportedFirmwareVers}${NOct} or echo "HTTP status: ${uploadHTTPcode:-UNKNOWN}" echo "------------------------------------------------------------" [ -s "$fwUploadResponseFile" ] && cat "$fwUploadResponseFile" - } > "$fwUploadDiagFile" 2>/dev/null - chmod 600 "$fwUploadDiagFile" 2>/dev/null + } > "$fwUploadDiagFile" + chmod 600 "$fwUploadDiagFile" _MsgToSysLog_ "F/W upload did not cause the router to reboot within 180 seconds. Curl exit code [$curlRC], HTTP status [${uploadHTTPcode:-UNKNOWN}]." _MsgToSysLog_ "F/W upload diagnostics saved to [$fwUploadDiagFile]." @@ -10438,7 +10625,7 @@ Please manually update to version ${GRNct}${MinSupportedFirmwareVers}${NOct} or /sbin/service reboot else _UpdateLoginPswdCheckHelper_ FAILURE - Say "${REDct}**ERROR**${NOct}: Router Login failed." + Say "${REDct}**ERROR**${NOct}: Router Login failed [$curlStatus]." if "$inMenuMode" || "$isInteractive" then printf "\n${routerLoginFailureMsg}\n\n" @@ -10459,7 +10646,7 @@ Please manually update to version ${GRNct}${MinSupportedFirmwareVers}${NOct} or AllowVPN="$(Get_Custom_Setting Allow_Updates_OverVPN)" if [ "$AllowVPN" = "DISABLED" ] then - Say "Unable to Restart Diversion. Please reboot to restart entware services." + Say "Unable to Restart Diversion. Please reboot to restart Entware services." fi fi sleep 5 diff --git a/README.md b/README.md index f98791ae..81cf4e46 100644 --- a/README.md +++ b/README.md @@ -1,7 +1,7 @@ # MerlinAU - AsusWRT-Merlin Firmware Auto Updater ## v1.6.9 -## 2026-Sep-17 +## 2026-Sep-20 ## WebUI: image From e9d61896f57a75d8f125a1eca5632575dbd602f3 Mon Sep 17 00:00:00 2001 From: Martinski4GitHub <119833648+Martinski4GitHub@users.noreply.github.com> Date: Mon, 21 Sep 2026 21:20:00 -0700 Subject: [PATCH 07/26] Account for Concurrent AiMesh Node Operations Adjusted code to account for concurrent AiMesh Node operations. --- MerlinAU.sh | 70 +++++++++++++++++++++++++++++------------------------ 1 file changed, 38 insertions(+), 32 deletions(-) diff --git a/MerlinAU.sh b/MerlinAU.sh index 1fd8697e..3c5e92a7 100644 --- a/MerlinAU.sh +++ b/MerlinAU.sh @@ -4,7 +4,7 @@ # # Project Created: 2023-Oct-01 by @ExtremeFiretop # Official Co-Author: @Martinski W. since 2023-Nov-01 -# Last Modified: 2026-Sep-20 +# Last Modified: 2026-Sep-21 # # MerlinAU™ / MerlinAutoUpdate™ # Official project: https://github.com/ExtremeFiretop/MerlinAutoUpdate-Router @@ -20,7 +20,7 @@ set -u ## Set version for each Production Release ## readonly SCRIPT_VERSION=1.6.9 -readonly SCRIPT_VERSTAG="26092015" +readonly SCRIPT_VERSTAG="26092121" readonly SCRIPT_NAME="MerlinAU" ## Set to "master" for Production Releases ## SCRIPT_BRANCH="dev" @@ -5493,21 +5493,24 @@ _GetNodeURL_() echo "${urlProto}://${nodeIPv4addr}${urlPort}" } -##-------------------------------------## -## Added by Martinski W. [2026-Jan-01] ## -##-------------------------------------## +##----------------------------------------## +## Modified by Martinski W. [2026-Sep-21] ## +##----------------------------------------## _DoMeshNodeLogin_() { - if [ $# -lt 3 ] || [ -z "$1" ] || [ -z "$2" ] || [ -z "$3" ] + if [ $# -lt 4 ] || [ -z "$1" ] || \ + [ -z "$2" ] || [ -z "$3" ] | [ -z "$4" ] then echo ; return 1 fi local nodeURL="$1" credsENC="$2" cookieFile="$3" - local responseFPath="${curlTmpRespFile}.NODE.LOGIN" + local responseFPath="${curlTmpRespFile}.${4}.NODE.LOGIN" + local curlErrLogFile="${curlErrLogFPath}.${4}.NODE.LOGIN" + local curlTmpLogFile="${curlTmpLogFPath}.${4}.NONE.LOGIN" local curlRetCode statusCODE statusSTRx httpStatusSTR printf '' > "$responseFPath" - printf '' > "$curlErrLogFPath" - printf '' > "$curlTmpLogFPath" + printf '' > "$curlErrLogFile" + printf '' > "$curlTmpLogFile" curl -kiLSs "${nodeURL}/login.cgi" \ --connect-timeout 10 --max-time 15 \ @@ -5520,13 +5523,13 @@ _DoMeshNodeLogin_() --data-raw "group_id=&action_mode=&action_script=&action_wait=5¤t_page=Main_Login.asp&next_page=index.asp" \ --data-urlencode "login_authorization=$credsENC" \ --cookie-jar "$cookieFile" \ - -w "${curlHTTPstatusStr}: %{http_code}\n" --stderr "$curlErrLogFPath" \ - --output "$responseFPath" >> "$curlTmpLogFPath" + -w "${curlHTTPstatusStr}: %{http_code}\n" --stderr "$curlErrLogFile" \ + --output "$responseFPath" >> "$curlTmpLogFile" curlRetCode="$?" statusCODE="$curlRetCode" statusSTRx="Curl Status Code: $curlRetCode" - httpStatusSTR="$(grep -oE "${curlHTTPstatusStr}: [4-5][0-9]{2,}" "$curlTmpLogFPath")" + httpStatusSTR="$(grep -oE "${curlHTTPstatusStr}: [4-5][0-9]{2,}" "$curlTmpLogFile")" if [ "$curlRetCode" -eq 0 ] && \ [ -z "$httpStatusSTR" ] && [ -s "$responseFPath" ] @@ -5545,27 +5548,30 @@ _DoMeshNodeLogin_() fi fi - rm -f "$curlErrLogFPath" "$curlTmpLogFPath" "$responseFPath" + rm -f "$curlErrLogFile" "$curlTmpLogFile" "$responseFPath" echo "$statusSTRx" return "$statusCODE" } -##-------------------------------------## -## Added by Martinski W. [2026-Sep-20] ## -##-------------------------------------## +##----------------------------------------## +## Modified by Martinski W. [2026-Sep-21] ## +##----------------------------------------## _GetNVRAM_FromWebUI_() { - if [ $# -lt 3 ] || [ -z "$1" ] || [ -z "$2" ] || [ -z "$3" ] + if [ $# -lt 4 ] || [ -z "$1" ] || \ + [ -z "$2" ] || [ -z "$3" ] || [ -z "$4" ] then echo ; return 1 fi local webUIcURL="$1" cookieFile="$2" nvramKey="$3" - local responseFPath="${curlTmpRespFile}.NVRAM.TMP" + local responseFPath="${curlTmpRespFile}.${4}.NVRAM.TMP" + local curlErrLogFile="${curlErrLogFPath}.${4}.NVRAM.TMP" + local curlTmpLogFile="${curlTmpLogFPath}.${4}.NVRAM.TMP" local curlRetCode statusCODE statusSTRx httpStatusSTR local nvramKeyValPair="" printf '' > "$responseFPath" - printf '' > "$curlErrLogFPath" - printf '' > "$curlTmpLogFPath" + printf '' > "$curlErrLogFile" + printf '' > "$curlTmpLogFile" curl -kiLSs "${webUIcURL}/appGet.cgi?hook=nvram_get($nvramKey)" \ --connect-timeout 10 --max-time 15 \ @@ -5575,13 +5581,13 @@ _GetNVRAM_FromWebUI_() -H 'Connection: keep-alive' \ -H "Referer: ${webUIcURL}/index.asp" \ --cookie "$cookieFile" \ - -w "${curlHTTPstatusStr}: %{http_code}\n" --stderr "$curlErrLogFPath" \ - --output "$responseFPath" >> "$curlTmpLogFPath" + -w "${curlHTTPstatusStr}: %{http_code}\n" --stderr "$curlErrLogFile" \ + --output "$responseFPath" >> "$curlTmpLogFile" curlRetCode="$?" statusCODE="$curlRetCode" statusSTRx="Curl Status Code: $curlRetCode" - httpStatusSTR="$(grep -oE "${curlHTTPstatusStr}: [4-5][0-9]{2,}" "$curlTmpLogFPath")" + httpStatusSTR="$(grep -oE "${curlHTTPstatusStr}: [4-5][0-9]{2,}" "$curlTmpLogFile")" if [ "$curlRetCode" -eq 0 ] && \ [ -z "$httpStatusSTR" ] && [ -s "$responseFPath" ] @@ -5602,13 +5608,13 @@ _GetNVRAM_FromWebUI_() fi [ -z "$nvramKeyValPair" ] && nvramKeyValPair="$statusSTRx" - rm -f "$curlErrLogFPath" "$curlTmpLogFPath" "$responseFPath" + rm -f "$curlErrLogFile" "$curlTmpLogFile" "$responseFPath" echo "$nvramKeyValPair" return "$statusCODE" } ##----------------------------------------## -## Modified by Martinski W. [2026-Sep-20] ## +## Modified by Martinski W. [2026-Sep-21] ## ##----------------------------------------## # Trigger the node "Check for updates" (no waiting here) # _MeshNodeTriggerFWCheck_() @@ -5633,7 +5639,7 @@ _MeshNodeTriggerFWCheck_() return 1 fi - if curlStatus="$(_DoMeshNodeLogin_ "$nodeURL" "$credsENC" "$cookieFile")" + if curlStatus="$(_DoMeshNodeLogin_ "$nodeURL" "$credsENC" "$cookieFile" "${runID}.${safeID}")" then Say "${GRNct}Successful Login for AiMesh Node [$nodeIPv4addr].${NOct}" else @@ -5646,7 +5652,7 @@ _MeshNodeTriggerFWCheck_() # Check if the AiMesh node is already performing a MerlinAU F/W update # *BEFORE* triggering the built-in firmware update check. #-----------------------------------------------------------------------# - if nvramKeyPair="$(_GetNVRAM_FromWebUI_ "$nodeURL" "$cookieFile" "$nvramTempFWupdateKey")" + if nvramKeyPair="$(_GetNVRAM_FromWebUI_ "$nodeURL" "$cookieFile" "$nvramTempFWupdateKey" "${runID}.${safeID}")" then if echo "$nvramKeyPair" | grep -qE "\"$nvramTempFWupdateKey\"[[:blank:]]*:[[:blank:]]*\"1\"" then @@ -5724,7 +5730,7 @@ _GetNodeInfo_() # If already created a cookie, reuse it (skip login), else perform login request # if [ ! -s "$cookieFile" ] && \ - ! curlStatus="$(_DoMeshNodeLogin_ "$nodeURL" "$credsENC" "$cookieFile")" + ! curlStatus="$(_DoMeshNodeLogin_ "$nodeURL" "$credsENC" "$cookieFile" "${runID}.${safeID}")" then rm -f "$cookieFile" Say "${REDct}Failed Login for AiMesh Node [$nodeIPv4addr] [$curlStatus].${NOct}" @@ -9948,9 +9954,9 @@ _Unmount_Eject_USB_Drives_() "$ejectUSB_OK" && return 0 || return 1 } -##------------------------------------------## -## Modified by ExtremeFiretop [2026-Sep-16] ## -##------------------------------------------## +##----------------------------------------## +## Modified by Martinski W. [2026-Sep-21] ## +##----------------------------------------## _RunFirmwareUpdateNow_() { local fwUploadResponseFile="/tmp/upload_response.txt" @@ -10534,7 +10540,7 @@ Please manually update to version ${GRNct}${MinSupportedFirmwareVers}${NOct} or # If this login fails now then we have to abort here and reboot. # Added by Martinski W. [2026-Sep-20] #-------------------------------------------------------------------# - if ! nvramKeyPair="$(_GetNVRAM_FromWebUI_ "$routerURL" "$cookieFile" "$nvramTempFWupdateKey")" + if ! nvramKeyPair="$(_GetNVRAM_FromWebUI_ "$routerURL" "$cookieFile" "$nvramTempFWupdateKey" "$$")" then rm -f "$cookieFile" if ! curlStatus="$(_DoMainRouterLogin_ "$routerURL" "$credsENC" "$cookieFile")" From 190015cc95d08683e3f3c243f225585f63b99cf0 Mon Sep 17 00:00:00 2001 From: Martinski4GitHub <119833648+Martinski4GitHub@users.noreply.github.com> Date: Tue, 22 Sep 2026 01:22:19 -0700 Subject: [PATCH 08/26] Fixed Typo Fixed a typo which would have cause a bug!! --- MerlinAU.sh | 6 +++--- README.md | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/MerlinAU.sh b/MerlinAU.sh index 3c5e92a7..470358d2 100644 --- a/MerlinAU.sh +++ b/MerlinAU.sh @@ -4,7 +4,7 @@ # # Project Created: 2023-Oct-01 by @ExtremeFiretop # Official Co-Author: @Martinski W. since 2023-Nov-01 -# Last Modified: 2026-Sep-21 +# Last Modified: 2026-Sep-22 # # MerlinAU™ / MerlinAutoUpdate™ # Official project: https://github.com/ExtremeFiretop/MerlinAutoUpdate-Router @@ -20,7 +20,7 @@ set -u ## Set version for each Production Release ## readonly SCRIPT_VERSION=1.6.9 -readonly SCRIPT_VERSTAG="26092121" +readonly SCRIPT_VERSTAG="26092200" readonly SCRIPT_NAME="MerlinAU" ## Set to "master" for Production Releases ## SCRIPT_BRANCH="dev" @@ -5499,7 +5499,7 @@ _GetNodeURL_() _DoMeshNodeLogin_() { if [ $# -lt 4 ] || [ -z "$1" ] || \ - [ -z "$2" ] || [ -z "$3" ] | [ -z "$4" ] + [ -z "$2" ] || [ -z "$3" ] || [ -z "$4" ] then echo ; return 1 fi local nodeURL="$1" credsENC="$2" cookieFile="$3" diff --git a/README.md b/README.md index 81cf4e46..516257e6 100644 --- a/README.md +++ b/README.md @@ -1,7 +1,7 @@ # MerlinAU - AsusWRT-Merlin Firmware Auto Updater ## v1.6.9 -## 2026-Sep-20 +## 2026-Sep-22 ## WebUI: image From c94caaac61323f1d69f61b1b3169001ef7954396 Mon Sep 17 00:00:00 2001 From: Martinski4GitHub <119833648+Martinski4GitHub@users.noreply.github.com> Date: Tue, 22 Sep 2026 16:38:28 -0700 Subject: [PATCH 09/26] Fixed Typo Fixed another typo, LOL!!!! --- MerlinAU.sh | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/MerlinAU.sh b/MerlinAU.sh index 470358d2..7a17e739 100644 --- a/MerlinAU.sh +++ b/MerlinAU.sh @@ -20,7 +20,7 @@ set -u ## Set version for each Production Release ## readonly SCRIPT_VERSION=1.6.9 -readonly SCRIPT_VERSTAG="26092200" +readonly SCRIPT_VERSTAG="26092216" readonly SCRIPT_NAME="MerlinAU" ## Set to "master" for Production Releases ## SCRIPT_BRANCH="dev" @@ -5505,7 +5505,7 @@ _DoMeshNodeLogin_() local nodeURL="$1" credsENC="$2" cookieFile="$3" local responseFPath="${curlTmpRespFile}.${4}.NODE.LOGIN" local curlErrLogFile="${curlErrLogFPath}.${4}.NODE.LOGIN" - local curlTmpLogFile="${curlTmpLogFPath}.${4}.NONE.LOGIN" + local curlTmpLogFile="${curlTmpLogFPath}.${4}.NODE.LOGIN" local curlRetCode statusCODE statusSTRx httpStatusSTR printf '' > "$responseFPath" From 627f35e3d23edf25a9d2647987ed0e9eb54c8499 Mon Sep 17 00:00:00 2001 From: Maghuro <1546139+maghuro@users.noreply.github.com> Date: Wed, 23 Sep 2026 22:59:12 +0100 Subject: [PATCH 10/26] Render downloaded changelog as text --- MerlinAU.asp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/MerlinAU.asp b/MerlinAU.asp index ec4b2d66..81bce796 100644 --- a/MerlinAU.asp +++ b/MerlinAU.asp @@ -1228,7 +1228,7 @@ function FetchChangelog(startTime) timeout: 1500, // each attempt times out after 9 seconds // success: function(data) { - $('#changelogData').html('
' + data + '
'); + $('#changelogData').empty().append($('
').text(data));
         },
         error: function()
         {

From b4226ceb53bb8bf023df708d06bd2303ad9f42ed Mon Sep 17 00:00:00 2001
From: Maghuro <1546139+maghuro@users.noreply.github.com>
Date: Wed, 23 Sep 2026 23:47:43 +0100
Subject: [PATCH 11/26] Harden pull request source check

---
 .github/workflows/Check-PRsource.yml | 12 +++++++-----
 1 file changed, 7 insertions(+), 5 deletions(-)

diff --git a/.github/workflows/Check-PRsource.yml b/.github/workflows/Check-PRsource.yml
index 4f6babf8..fadb9e99 100644
--- a/.github/workflows/Check-PRsource.yml
+++ b/.github/workflows/Check-PRsource.yml
@@ -6,13 +6,15 @@ on:
       - reopened
       - synchronize
       - edited
+
+permissions: {}
+
 jobs:
   check-branches:
     runs-on: ubuntu-latest
     steps:
-      - name: Check branches
+      - name: Reject non-dev pull requests to main
+        if: github.base_ref == 'main' && github.head_ref != 'dev'
         run: |
-          if [ ${{ github.head_ref }} != "dev" ] && [ ${{ github.base_ref }} == "main" ]; then
-            echo "Merge requests to main branch are only allowed from dev branch."
-            exit 1
-          fi
+          echo "Merge requests to main branch are only allowed from dev branch."
+          exit 1

From 63e4df3652821b13a58bccf3b9da63aed46e703c Mon Sep 17 00:00:00 2001
From: Maghuro <1546139+maghuro@users.noreply.github.com>
Date: Thu, 24 Sep 2026 01:48:31 +0100
Subject: [PATCH 12/26] Preserve failures during MerlinAU self-update

---
 MerlinAU.sh | 8 +++-----
 1 file changed, 3 insertions(+), 5 deletions(-)

diff --git a/MerlinAU.sh b/MerlinAU.sh
index 7a17e739..ae13338f 100644
--- a/MerlinAU.sh
+++ b/MerlinAU.sh
@@ -2991,11 +2991,11 @@ _CurlFileDownload_()
 }
 
 ##----------------------------------------##
-## Modified by Martinski W. [2025-Mar-27] ##
+## Modified by maghuro [2026-Sep-24]     ##
 ##----------------------------------------##
 _DownloadScriptFiles_()
 {
-   local retCode  isUpdateAction  updatedWebUIPage  theWebPage
+   local retCode=0  isUpdateAction  updatedWebUIPage  theWebPage
 
    if [ $# -gt 0 ] && [ "$1" = "update" ]
    then isUpdateAction=true
@@ -3005,7 +3005,7 @@ _DownloadScriptFiles_()
 
    if _CurlFileDownload_ "version.txt" "$SCRIPT_VERPATH"
    then
-       retCode=0 ; chmod 664 "$SCRIPT_VERPATH"
+       chmod 664 "$SCRIPT_VERPATH"
    else
        retCode=1
        Say "${REDct}**ERROR**${NOct}: Unable to download latest version file for $SCRIPT_NAME."
@@ -3014,7 +3014,6 @@ _DownloadScriptFiles_()
    if "$mountWebGUI_OK" && \
       _CurlFileDownload_ "$SCRIPT_WEB_ASP_FILE" "$SCRIPT_WEB_ASP_PATH"
    then
-       retCode=0
        dos2unix "$SCRIPT_WEB_ASP_PATH"
        chmod 664 "$SCRIPT_WEB_ASP_PATH"
        if "$updatedWebUIPage"
@@ -3036,7 +3035,6 @@ _DownloadScriptFiles_()
 
    if _CurlFileDownload_ "${SCRIPT_NAME}.sh" "$ScriptFilePath"
    then
-       retCode=0
        dos2unix "$ScriptFilePath"
        chmod 755 "$ScriptFilePath"
    else

From 190edc6527e2d9bced3ad6a984b63831529fce1b Mon Sep 17 00:00:00 2001
From: Maghuro <1546139+maghuro@users.noreply.github.com>
Date: Thu, 24 Sep 2026 01:48:45 +0100
Subject: [PATCH 13/26] Safely escape values written through sed

---
 MerlinAU.sh | 8 ++++----
 1 file changed, 4 insertions(+), 4 deletions(-)

diff --git a/MerlinAU.sh b/MerlinAU.sh
index 7a17e739..5fe6325e 100644
--- a/MerlinAU.sh
+++ b/MerlinAU.sh
@@ -1685,7 +1685,7 @@ Get_Custom_Setting()
 }
 
 ##----------------------------------------##
-## Modified by Martinski W. [2026-Aug-16] ##
+## Modified by maghuro [2026-Sep-24]     ##
 ##----------------------------------------##
 Update_Custom_Settings()
 {
@@ -1715,7 +1715,7 @@ Update_Custom_Settings()
                 then
                     if [ "$setting_value" != "$(grep "^$setting_type" "$CONFIG_FILE" | cut -f2 -d' ')" ]
                     then
-                        fixedVal="$(echo "$setting_value" | sed 's/[\/&]/\\&/g')"
+                        fixedVal="$(printf '%s' "$setting_value" | sed 's/[\/&\\]/\\&/g')"
                         sed -i "s/^${setting_type}.*/$setting_type $fixedVal/" "$CONFIG_FILE"
                     fi
                 else
@@ -1743,7 +1743,7 @@ Update_Custom_Settings()
                     oldVal="$(grep "^${setting_type}=" "$CONFIG_FILE" | awk -F '=' '{print $2}' | sed "s/['\"]//g")"
                     if [ -z "$oldVal" ] || [ "$oldVal" != "$setting_value" ]
                     then
-                        fixedVal="$(echo "$setting_value" | sed 's/[\/.,*-]/\\&/g')"
+                        fixedVal="$(printf '%s' "$setting_value" | sed 's/[\/&\\]/\\&/g')"
                         sed -i "s/${setting_type}=.*/${setting_type}=\"${fixedVal}\"/" "$CONFIG_FILE"
                     fi
                 else
@@ -1801,7 +1801,7 @@ Update_Custom_Settings()
                 oldVal="$(grep "^${setting_type}=" "$CONFIG_FILE" | awk -F '=' '{print $2}' | sed "s/['\"]//g")"
                 if [ -z "$oldVal" ] || [ "$oldVal" != "$setting_value" ]
                 then
-                    fixedVal="$(echo "$setting_value" | sed 's/[\/&]/\\&/g')"
+                    fixedVal="$(printf '%s' "$setting_value" | sed 's/[\/&\\]/\\&/g')"
                     sed -i "s/^${setting_type}=.*/${setting_type}=\"${fixedVal}\"/" "$CONFIG_FILE"
                 fi
             else

From 58d93df4a8010c39109abfa118e74de5857c0873 Mon Sep 17 00:00:00 2001
From: ExtremeFiretop 
Date: Wed, 23 Sep 2026 22:22:41 -0400
Subject: [PATCH 14/26] Fix dependabot

This makes sure that dependabot always checks and submits against dev.
Checking against main works against our other workflows and architecture such as Check-PRsource.yml
---
 .github/dependabot.yml | 11 -----------
 1 file changed, 11 deletions(-)

diff --git a/.github/dependabot.yml b/.github/dependabot.yml
index 35446561..5eb2c0b2 100644
--- a/.github/dependabot.yml
+++ b/.github/dependabot.yml
@@ -3,17 +3,6 @@
 # Please see the documentation for all configuration options:
 # https://docs.github.com/code-security/dependabot/dependabot-version-updates/configuration-options-for-the-dependabot.yml-file
 
-version: 2
-updates:
-  - package-ecosystem: "github-actions" # See documentation for possible values
-    directory: "/" # Location of package manifests
-    schedule:
-      interval: "weekly"
-    groups:
-      all-actions:
-        patterns: [ "*" ]
-    target-branch: "main"
-
 version: 2
 updates:
   - package-ecosystem: "github-actions" # See documentation for possible values

From 4e779dbaf5e3835d2df3a748a94835a677fef3bb Mon Sep 17 00:00:00 2001
From: ExtremeFiretop 
Date: Thu, 24 Sep 2026 00:13:12 -0400
Subject: [PATCH 15/26] Extension of PR: #590 and #610

Extension of PR: #590 and #610

Fix settings values that contain an = character being cut off when MerlinAU reads them back... For example, foo=bar@example.com would previously be read as foo.
---
 MerlinAU.sh | 8 ++++----
 1 file changed, 4 insertions(+), 4 deletions(-)

diff --git a/MerlinAU.sh b/MerlinAU.sh
index d50a01d0..1b567ab1 100644
--- a/MerlinAU.sh
+++ b/MerlinAU.sh
@@ -1669,7 +1669,7 @@ Get_Custom_Setting()
             "FW_New_Update_EMail_CC_Name" | \
             "FW_New_Update_EMail_CC_Address")
                 grep -q "^${setting_type}=" "$CONFIG_FILE" && \
-                setting_value="$(grep "^${setting_type}=" "$CONFIG_FILE" | awk -F '=' '{print $2}' | sed "s/['\"]//g")"
+                setting_value="$(grep "^${setting_type}=" "$CONFIG_FILE" | cut -f2- -d'=' | sed "s/['\"]//g")"
                 ;;
             *)
                 setting_value="**ERROR**"
@@ -1740,7 +1740,7 @@ Update_Custom_Settings()
             then
                 if grep -q "^${setting_type}=" "$CONFIG_FILE"
                 then
-                    oldVal="$(grep "^${setting_type}=" "$CONFIG_FILE" | awk -F '=' '{print $2}' | sed "s/['\"]//g")"
+                    oldVal="$(grep "^${setting_type}=" "$CONFIG_FILE" | cut -f2- -d'=' | sed "s/['\"]//g")"
                     if [ -z "$oldVal" ] || [ "$oldVal" != "$setting_value" ]
                     then
                         fixedVal="$(printf '%s' "$setting_value" | sed 's/[\/&\\]/\\&/g')"
@@ -1798,7 +1798,7 @@ Update_Custom_Settings()
             # Generic handling for arbitrary settings #
             if grep -q "^${setting_type}=" "$CONFIG_FILE"
             then
-                oldVal="$(grep "^${setting_type}=" "$CONFIG_FILE" | awk -F '=' '{print $2}' | sed "s/['\"]//g")"
+                oldVal="$(grep "^${setting_type}=" "$CONFIG_FILE" | cut -f2- -d'=' | sed "s/['\"]//g")"
                 if [ -z "$oldVal" ] || [ "$oldVal" != "$setting_value" ]
                 then
                     fixedVal="$(printf '%s' "$setting_value" | sed 's/[\/&\\]/\\&/g')"
@@ -1846,7 +1846,7 @@ _GetAllNodeSettings_()
         if [ -n "$matched_lines" ]
         then
             # Extract the value from the first matched line #
-            setting_value="$(echo "$matched_lines" | head -n 1 | awk -F '=' '{print $2}' | tr -d '"')"
+            setting_value="$(echo "$matched_lines" | head -n 1 | cut -f2- -d'=' | tr -d '"')"
         fi
     fi
     echo "$setting_value"

From a2fd184b8d5c72fed50a828c6d2086d82d92edb5 Mon Sep 17 00:00:00 2001
From: Martinski4GitHub <119833648+Martinski4GitHub@users.noreply.github.com>
Date: Fri, 25 Sep 2026 02:34:48 -0700
Subject: [PATCH 16/26] Fixes and Code Improvements

- Fixed issue where setting the email format to "Plain Text" in the SSH CLI menu, the WebUI would show the selection box as empty instead of reflecting the "Plain Text" setting made previously by the user.

- Fixed parsing issues when handling unusual but valid user input for the "Secondary Email Address" option.

- Improved functions that set and get configuration settings, also making sure values stored as assignment statements are defined as literal strings (e.g. KeyName='KeyValue').

- Included fix in the WebUI from @maghuro to properly handle the colon separator in login credentials (i.e. username:password).

- Miscellaneous code improvements.
---
 MerlinAU.asp |  15 ++++---
 MerlinAU.sh  | 115 +++++++++++++++++++++++++++++++--------------------
 README.md    |   2 +-
 3 files changed, 81 insertions(+), 51 deletions(-)

diff --git a/MerlinAU.asp b/MerlinAU.asp
index 81bce796..e2ed2795 100644
--- a/MerlinAU.asp
+++ b/MerlinAU.asp
@@ -304,7 +304,7 @@ input[value="Uninstall"] {