diff --git a/.github/workflows/update-merlin-sha256.yml b/.github/workflows/update-merlin-sha256.yml new file mode 100644 index 00000000..7bf939be --- /dev/null +++ b/.github/workflows/update-merlin-sha256.yml @@ -0,0 +1,121 @@ +name: Update Merlin SHA256 Checksums + +on: + schedule: + # Every 15 minutes, offset from the top of the hour to reduce scheduler congestion. + - cron: '7,22,37,52 * * * *' + workflow_dispatch: + +permissions: + contents: write + +concurrency: + group: update-merlin-sha256 + cancel-in-progress: false + +jobs: + scrape-and-commit: + runs-on: ubuntu-latest + timeout-minutes: 5 + + steps: + - name: Checkout repository + uses: actions/checkout@v7 + with: + ssh-key: ${{ secrets.DEPLOY_KEY }} + + - name: Fetch, parse, and validate SHA256 signatures + shell: bash + run: | + set -euo pipefail + + readonly SOURCE_URL='https://www.asuswrt-merlin.net/download' + readonly TARGET_FILE='merlin-sha256.txt' + readonly MIN_EXPECTED_ENTRIES=5 + + page_file="$(mktemp)" + candidate_file="$(mktemp)" + trap 'rm -f "$page_file" "$candidate_file"' EXIT + + echo "Fetching SHA256 signatures from ${SOURCE_URL}..." + curl --fail --location --silent --show-error \ + --retry 4 --retry-delay 5 --retry-connrefused \ + --connect-timeout 15 --max-time 60 \ + --user-agent 'MerlinAutoUpdate checksum mirror (+https://github.com/ExtremeFiretop/MerlinAutoUpdate-Router)' \ + --output "$page_file" \ + "$SOURCE_URL" + + # Keep the same source section MerlinAU consumes today, but write to a + # temporary candidate so a scrape/parser failure cannot destroy the + # last-known-good mirror in the repository. + sed -n '/<.*>SHA256 signatures:<\/.*>/,/<\/pre>/p' "$page_file" | \ + sed -n '/
]*>/,/<\/pre>/p' | \
+ sed -e 's/^.*]*>//' \
+ -e 's/<[^>]*>//g' \
+ -e 's/^[[:space:]]*//' \
+ -e 's/[[:space:]]*$//' | \
+ tr -d '\r' | \
+ sed '/^[[:space:]]*$/d' > "$candidate_file"
+
+ echo "Validating candidate checksum list..."
+ awk -v min_entries="$MIN_EXPECTED_ENTRIES" '
+ BEGIN {
+ valid = 1
+ count = 0
+ }
+ {
+ count++
+
+ if (NF != 2) {
+ printf "Invalid field count on line %d: %s\n", NR, $0 > "/dev/stderr"
+ valid = 0
+ next
+ }
+
+ if (length($1) != 64 || $1 ~ /[^0-9A-Fa-f]/) {
+ printf "Invalid SHA256 on line %d: %s\n", NR, $1 > "/dev/stderr"
+ valid = 0
+ }
+
+ if (seen[$2]++) {
+ printf "Duplicate firmware filename on line %d: %s\n", NR, $2 > "/dev/stderr"
+ valid = 0
+ }
+ }
+ END {
+ if (count < min_entries) {
+ printf "Only %d checksum entries were parsed; expected at least %d.\n", count, min_entries > "/dev/stderr"
+ valid = 0
+ }
+
+ if (!valid)
+ exit 1
+ }
+ ' "$candidate_file"
+
+ echo "Validated $(wc -l < "$candidate_file") checksum entries."
+ echo "Candidate preview:"
+ head -n 5 "$candidate_file"
+
+ # Replace the working-tree copy only after the candidate has passed
+ # every validation check. A failed run therefore leaves the repository
+ # and its last-known-good checksum mirror unchanged.
+ mv -f "$candidate_file" "$TARGET_FILE"
+
+ - name: Commit and push changes
+ shell: bash
+ run: |
+ set -euo pipefail
+
+ git config user.name 'github-actions[bot]'
+ git config user.email '41898282+github-actions[bot]@users.noreply.github.com'
+
+ git add merlin-sha256.txt
+
+ if git diff --cached --quiet; then
+ echo 'No checksum changes detected. Nothing to commit.'
+ exit 0
+ fi
+
+ git commit -m 'Automated update: refresh Merlin SHA256 checksums'
+ git push
diff --git a/MerlinAU.sh b/MerlinAU.sh
index 6237db63..6bc3fbc1 100644
--- a/MerlinAU.sh
+++ b/MerlinAU.sh
@@ -9,11 +9,11 @@
set -u
## Set version for each Production Release ##
-readonly SCRIPT_VERSION=1.6.6
-readonly SCRIPT_VERSTAG="26081603"
+readonly SCRIPT_VERSION=1.6.7
+readonly SCRIPT_VERSTAG="26082409"
readonly SCRIPT_NAME="MerlinAU"
## Set to "master" for Production Releases ##
-SCRIPT_BRANCH="master"
+SCRIPT_BRANCH="dev"
##----------------------------------------##
## Modified by Martinski W. [2024-Jul-03] ##
@@ -26,6 +26,9 @@ SCRIPT_URL_REPO="${SCRIPT_URL_BASE}/$SCRIPT_BRANCH"
readonly FW_SFURL_BASE="https://sourceforge.net/projects/asuswrt-merlin/files"
readonly FW_SFURL_RELEASE_SUFFIX="Release"
readonly FW_GITURL_RELEASE="https://api.github.com/repos/gnuton/asuswrt-merlin.ng/releases/latest"
+readonly FW_SHA256_URL="https://www.asuswrt-merlin.net/download"
+# The scheduled checksum mirror is maintained on the repository's default branch. #
+readonly FW_SHA256_MIRROR_URL="${SCRIPT_URL_BASE}/main/merlin-sha256.txt"
##----------------------------------------##
## Modified by Martinski W. [2024-May-31] ##
@@ -5971,45 +5974,103 @@ _CopyGnutonFiles_()
return 0
}
-##----------------------------------------##
-## Modified by Martinski W. [2025-Feb-17] ##
-##----------------------------------------##
+##------------------------------------------##
+## Modified by ExtremeFiretop [2026-Aug-24] ##
+##------------------------------------------##
+##------------------------------------------##
+## Modified by ExtremeFiretop [2026-Aug-24] ##
+##------------------------------------------##
+_GetFirmwareSHA256FromList_()
+{
+ local checksumList="$1"
+ local firmwareName="$2"
+
+ # Return a checksum only when there is exactly one exact filename match
+ # and its digest is a syntactically valid SHA256 value. Using awk avoids
+ # treating firmware filenames as regular expressions.
+ printf '%s\n' "$checksumList" | awk -v firmwareName="$firmwareName" '
+ $2 == firmwareName && length($1) == 64 && $1 !~ /[^0-9A-Fa-f]/ {
+ matchCount++
+ checksum = tolower($1)
+ }
+ END {
+ if (matchCount == 1)
+ print checksum
+ }'
+}
+
_CheckOnlineFirmwareSHA256_()
{
- # Fetch the latest SHA256 checksums from ASUSWRT-Merlin website #
- checksums="$(curl -Ls --retry 4 --retry-delay 5 --retry-connrefused \
- https://www.asuswrt-merlin.net/download |
- sed -n '/<.*>SHA256 signatures:<\/.*>/,/<\/pre>/p' |
- sed -n '/].*>/,/<\/pre>/p' |
- sed -e 's/<[^>].*>//g; s/^[[:space:]]*//; s/[[:space:]]*$//')"
+ local checksums=""
+ local dl_sig=""
+ local fw_name=""
+ local fw_sig=""
+ local checksumSource=""
- if [ -z "$checksums" ]
+ if [ ! -f "$firmware_file" ]
then
- Say "${REDct}**ERROR**${NOct}: Could not download the firmware SHA256 signatures from the website."
+ Say "${REDct}**ERROR**${NOct}: Firmware image file NOT found!"
_DoCleanUp_ 1
return 1
fi
- if [ -f "$firmware_file" ]
+ fw_name="$(basename "$firmware_file")"
+ fw_sig="$(openssl sha256 "$firmware_file" | awk -F ' ' '{print tolower($2)}')"
+
+ # PRIMARY: Fetch the checksum directly from the ASUSWRT-Merlin website.
+ checksums="$(curl -Lfs --retry 4 --retry-delay 5 --retry-connrefused \
+ "$FW_SHA256_URL" 2>/dev/null |
+ sed -n '/<.*>SHA256 signatures:<\/.*>/,/<\/pre>/p' |
+ sed -n '/]*>/,/<\/pre>/p' |
+ sed -e 's/<[^>]*>//g; s/^[[:space:]]*//; s/[[:space:]]*$//')"
+
+ if [ -n "$checksums" ]
then
- fw_sig="$(openssl sha256 "$firmware_file" | awk -F ' ' '{print $2}')"
- # Extract the corresponding signature for the firmware file from the fetched checksums #
- dl_sig="$(echo "$checksums" | grep "$(basename "$firmware_file")" | awk -F ' ' '{print $1}')"
- if [ "$fw_sig" != "$dl_sig" ]
+ dl_sig="$(_GetFirmwareSHA256FromList_ "$checksums" "$fw_name")"
+ fi
+
+ if [ -n "$dl_sig" ]
+ then
+ checksumSource="ASUSWRT-Merlin website"
+ else
+ Say "${YLWct}**WARNING**${NOct}: Independently published checksum could not be retrieved from the ASUSWRT-Merlin website."
+
+ # SECONDARY: Use the repository mirror only when the official source
+ # did not yield a usable checksum. Never use the checksum bundled in
+ # the firmware archive for an online update.
+ checksums="$(curl -Lfs --retry 4 --retry-delay 5 --retry-connrefused \
+ "$FW_SHA256_MIRROR_URL" 2>/dev/null)"
+
+ if [ -n "$checksums" ]
+ then
+ dl_sig="$(_GetFirmwareSHA256FromList_ "$checksums" "$fw_name")"
+ fi
+
+ if [ -z "$dl_sig" ]
then
- Say "${REDct}**ERROR**${NOct}: SHA256 signature from extracted firmware file does not match the SHA256 signature from the website."
+ Say "${REDct}**ERROR**${NOct}: No unique valid SHA256 signature for ${fw_name} was available from either independent online source."
+ Say "Online firmware update was aborted; bundled archive checksum fallback is intentionally disabled."
_DoCleanUp_ 1
_SendEMailNotification_ FAILED_FW_CHECKSUM_STATUS
return 1
- else
- Say "SHA256 signature check for firmware image file passed successfully."
- return 0
fi
- else
- Say "${REDct}**ERROR**${NOct}: Firmware image file NOT found!"
+
+ checksumSource="MerlinAU GitHub checksum mirror"
+ Say "${YLWct}**WARNING**${NOct}: Using the MerlinAU GitHub checksum mirror for verification. (15 MINUTE DELAY!)"
+ fi
+
+ # If the primary source supplied a checksum but it mismatches, this fails
+ # immediately. The mirror is never used to bypass a checksum mismatch.
+ if [ "$fw_sig" != "$dl_sig" ]
+ then
+ Say "${REDct}**ERROR**${NOct}: SHA256 signature from extracted firmware file does not match the SHA256 signature from the ${checksumSource}."
_DoCleanUp_ 1
+ _SendEMailNotification_ FAILED_FW_CHECKSUM_STATUS
return 1
fi
+
+ Say "SHA256 signature check for firmware image file passed successfully using the ${checksumSource}."
+ return 0
}
##----------------------------------------##
@@ -10371,9 +10432,9 @@ _DelFWAutoUpdateHook_()
fi
}
-##----------------------------------------##
-## Modified by Martinski W. [2024-May-17] ##
-##----------------------------------------##
+##------------------------------------------##
+## Modified by ExtremeFiretop [2026-Aug-24] ##
+##------------------------------------------##
_AddFWAutoUpdateHook_()
{
local hookScriptFile jobHookAdded=false
@@ -10398,12 +10459,12 @@ _AddFWAutoUpdateHook_()
if "$jobHookAdded"
then Say "Cron job hook was added successfully to '$hookScriptFile' script."
- else Say "Cron job hook already exists in '$hookScriptFile' script."
+ else DoPrintf "Cron job hook already exists in '$hookScriptFile' script.\n"
fi
}
##------------------------------------------##
-## Modified by ExtremeFiretop [2024-Nov-18] ##
+## Modified by ExtremeFiretop [2026-Aug-24] ##
##------------------------------------------##
_AddScriptAutoUpdateHook_()
{
@@ -10429,7 +10490,7 @@ _AddScriptAutoUpdateHook_()
if "$jobHookAdded"
then Say "Cron job hook was added successfully to '$hookScriptFile' script."
- else Say "Cron job hook already exists in '$hookScriptFile' script."
+ else DoPrintf "Cron job hook already exists in '$hookScriptFile' script.\n"
fi
}
diff --git a/merlin-sha256.txt b/merlin-sha256.txt
new file mode 100644
index 00000000..2eed75ce
--- /dev/null
+++ b/merlin-sha256.txt
@@ -0,0 +1,21 @@
+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