diff options
Diffstat (limited to 'global-functions.rsc')
-rw-r--r-- | global-functions.rsc | 822 |
1 files changed, 544 insertions, 278 deletions
diff --git a/global-functions.rsc b/global-functions.rsc index 17ccda8..829cbf2 100644 --- a/global-functions.rsc +++ b/global-functions.rsc @@ -1,18 +1,21 @@ #!rsc by RouterOS # RouterOS script: global-functions -# Copyright (c) 2013-2024 Christian Hesse <mail@eworm.de> +# Copyright (c) 2013-2025 Christian Hesse <mail@eworm.de> # Michael Gisbers <michael@gisbers.de> -# https://git.eworm.de/cgit/routeros-scripts/about/COPYING.md +# https://rsc.eworm.de/COPYING.md # -# requires RouterOS, version=7.12 +# requires RouterOS, version=7.15 +# requires device-mode, fetch, scheduler # # global functions -# https://git.eworm.de/cgit/routeros-scripts/about/ +# https://rsc.eworm.de/ :local ScriptName [ :jobname ]; -# expected configuration version -:global ExpectedConfigVersion 124; +# Git commit id & info, expected configuration version +:global CommitId "unknown"; +:global CommitInfo "unknown"; +:global ExpectedConfigVersion 138; # global variables not to be changed by user :global GlobalFunctionsReady false; @@ -32,7 +35,11 @@ :global DownloadPackage; :global EitherOr; :global EscapeForRegEx; -:global FetchUserAgent; +:global ExitError; +:global FetchHuge; +:global FetchUserAgentStr; +:global FileExists; +:global FileGet; :global FormatLine; :global FormatMultiLines; :global GetMacVendor; @@ -49,18 +56,20 @@ :global IsMacLocallyAdministered; :global IsTimeSync; :global LogPrint; -:global LogPrintExit2; :global LogPrintOnce; +:global LogPrintVerbose; :global MAX; :global MIN; :global MkDir; :global NotificationFunctions; :global ParseDate; -:global ParseJson; :global ParseKeyValueStore; :global PrettyPrint; +:global ProtocolStrip; :global RandomDelay; :global RequiredRouterOS; +:global RmDir; +:global RmFile; :global ScriptFromTerminal; :global ScriptInstallUpdate; :global ScriptLock; @@ -107,8 +116,18 @@ "is configured to download certificate CRLs to system!"); } + :if ([ :len $CommonName ] = 0) do={ + $LogPrint warning $0 ("No CommonName given!"); + :return false; + } + + :if (([ /certificate/settings/get ]->"builtin-trust-anchors") = "trusted" && \ + [[ :parse (":return [ :len [ /certificate/builtin/find where common-name=\"" . $CommonName . "\" ] ]") ]] > 0) do={ + :return true; + } + :if ([ :len [ /certificate/find where common-name=$CommonName ] ] = 0) do={ - $LogPrint info $0 ("Certificate with CommonName \"" . $CommonName . "\" not available."); + $LogPrint info $0 ("Certificate with CommonName '" . $CommonName . "' not available."); :if ([ $CertificateDownload $CommonName ] = false) do={ :return false; } @@ -117,8 +136,8 @@ :local CertVal [ /certificate/get [ find where common-name=$CommonName ] ]; :while (($CertVal->"akid") != "" && ($CertVal->"akid") != ($CertVal->"skid")) do={ :if ([ :len [ /certificate/find where skid=($CertVal->"akid") ] ] = 0) do={ - $LogPrint info $0 ("Certificate chain for \"" . $CommonName . \ - "\" is incomplete, missing \"" . ([ $ParseKeyValueStore ($CertVal->"issuer") ]->"CN") . "\"."); + $LogPrint info $0 ("Certificate chain for '" . $CommonName . \ + "' is incomplete, missing '" . ([ $ParseKeyValueStore ($CertVal->"issuer") ]->"CN") . "'."); :if ([ $CertificateDownload $CommonName ] = false) do={ :return false; } @@ -135,42 +154,75 @@ :global ScriptUpdatesBaseUrl; :global ScriptUpdatesUrlSuffix; + :global CertificateAvailable; :global CertificateNameByCN; :global CleanName; - :global FetchUserAgent; + :global FetchUserAgentStr; :global LogPrint; + :global RmFile; :global WaitForFile; $LogPrint info $0 ("Downloading and importing certificate with " . \ - "CommonName \"" . $CommonName . "\"."); + "CommonName '" . $CommonName . "'."); + :local FileName ([ $CleanName $CommonName ] . ".pem"); :do { - :local FileName ([ $CleanName $CommonName ] . ".pem"); - /tool/fetch check-certificate=yes-without-crl http-header-field=({ [ $FetchUserAgent $0 ] }) \ + /tool/fetch check-certificate=yes-without-crl http-header-field=({ [ $FetchUserAgentStr $0 ] }) \ ($ScriptUpdatesBaseUrl . "certs/" . $FileName . $ScriptUpdatesUrlSuffix) \ dst-path=$FileName as-value; $WaitForFile $FileName; - /certificate/import file-name=$FileName passphrase="" as-value; - :delay 1s; - /file/remove $FileName; - - :foreach Cert in=[ /certificate/find where name~("^" . $FileName . "_[0-9]+\$") ] do={ - $CertificateNameByCN [ /certificate/get $Cert common-name ]; - } } on-error={ - $LogPrint warning $0 ("Failed importing certificate with CommonName \"" . $CommonName . "\"!"); + $LogPrint warning $0 ("Failed downloading certificate with CommonName '" . $CommonName . \ + "' from repository! Trying fallback to mkcert.org..."); + :do { + :if ([ :len [ /certificate/find where common-name="ISRG Root X1" ] ] = 0) do={ + $LogPrint error $0 ("Required certificate is not available."); + :return false; + } + /tool/fetch check-certificate=yes-without-crl http-header-field=({ [ $FetchUserAgentStr $0 ] }) \ + "https://mkcert.org/generate/" http-data=[ :serialize to=json ({ $CommonName }) ] \ + dst-path=$FileName as-value; + $WaitForFile $FileName; + :if ([ /file/get $FileName size ] = 0) do={ + $RmFile $FileName; + :error false; + } + } on-error={ + $LogPrint warning $0 ("Failed downloading certificate with CommonName '" . $CommonName . "'!"); + :return false; + } + } + + /certificate/import file-name=$FileName passphrase="" as-value; + :delay 1s; + $RmFile $FileName; + + :if ([ :len [ /certificate/find where common-name=$CommonName ] ] = 0) do={ + /certificate/remove [ find where name~("^" . $FileName . "_[0-9]+\$") ]; + $LogPrint warning $0 ("Certificate with CommonName '" . $CommonName . "' still unavailable!"); :return false; } + + :foreach Cert in=[ /certificate/find where name~("^" . $FileName . "_[0-9]+\$") ] do={ + $CertificateNameByCN [ /certificate/get $Cert common-name ]; + } :return true; } # name a certificate by its common-name :set CertificateNameByCN do={ - :local CommonName [ :tostr $1 ]; + :local Match [ :tostr $1 ]; :global CleanName; + :global LogPrint; - :local Cert [ /certificate/find where common-name=$CommonName ]; + :local Cert ([ /certificate/find where (common-name=$Match or fingerprint=$Match or name=$Match) ]->0); + :if ([ :len $Cert ] = 0) do={ + $LogPrint warning $0 ("No matching certificate found."); + :return false; + } + :local CommonName [ /certificate/get $Cert common-name ]; /certificate/set $Cert name=[ $CleanName $CommonName ]; + :return true; } # multiply given character(s) @@ -229,18 +281,28 @@ :for I from=0 to=([ :len $Input ] - 1) do={ :local Char [ :pick $Input $I ]; - :if ([ :typeof [ find "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-" $Char ] ] = "nil") do={ - :set Char "-"; - } - :if ($Char != "-" || [ :pick $Return ([ :len $Return ] - 1) ] != "-") do={ - :set Return ($Return . $Char); + :if ([ :typeof [ find "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789" $Char ] ] = "nil") do={ + :do { + :if ([ :len $Return ] = 0) do={ + :error true; + } + :if ([ :pick $Return ([ :len $Return ] - 1) ] = "-") do={ + :error true; + } + :set Char "-"; + } on-error={ + :set Char ""; + } } + :set Return ($Return . $Char); } :return $Return; } # get readable device info :set DeviceInfo do={ + :global CommitId; + :global CommitInfo; :global ExpectedConfigVersion; :global Identity; @@ -262,16 +324,19 @@ ([ $FormatLine "Location" ($Snmp->"location") ] . "\n") ] . \ [ $IfThenElse ([ :len ($Snmp->"contact") ] > 0) \ ([ $FormatLine "Contact" ($Snmp->"contact") ] . "\n") ] . \ - [ $FormatLine "Board name" ($Resource->"board-name") ] . "\n" . \ - [ $FormatLine "Architecture" ($Resource->"architecture-name") ] . "\n" . \ + "Hardware:\n" . \ + [ $FormatLine " Board" ($Resource->"board-name") ] . "\n" . \ + [ $FormatLine " Arch" ($Resource->"architecture-name") ] . "\n" . \ [ $IfThenElse ($RouterBoard->"routerboard" = true) \ - ([ $FormatLine "Model" ($RouterBoard->"model") ] . \ + ([ $FormatLine " Model" ($RouterBoard->"model") ] . \ [ $IfThenElse ([ :len ($RouterBoard->"revision") ] > 0) \ (" " . $RouterBoard->"revision") ] . "\n" . \ - [ $FormatLine "Serial number" ($RouterBoard->"serial-number") ] . "\n") ] . \ - [ $IfThenElse ([ :len ($License->"level") ] > 0) \ - ([ $FormatLine "License" ($License->"level") ] . "\n") ] . \ + [ $FormatLine " Serial" ($RouterBoard->"serial-number") ] . "\n") ] . \ + [ $IfThenElse ([ :len ($License->"nlevel") ] > 0) \ + ([ $FormatLine " License" ("level " . ($License->"nlevel")) ] . "\n") ] . \ "RouterOS:\n" . \ + [ $IfThenElse ([ :len ($License->"level") ] > 0) \ + ([ $FormatLine " License" ("level " . ($License->"level")) ] . "\n") ] . \ [ $FormatLine " Channel" ($Update->"channel") ] . "\n" . \ [ $FormatLine " Installed" ($Update->"installed-version") ] . "\n" . \ [ $IfThenElse ([ :typeof ($Update->"latest-version") ] != "nothing" && \ @@ -281,16 +346,14 @@ $RouterBoard->"current-firmware" != $RouterBoard->"upgrade-firmware") \ ([ $FormatLine " Firmware" ($RouterBoard->"current-firmware") ] . "\n") ] . \ "RouterOS-Scripts:\n" . \ + [ $IfThenElse ($CommitId != "unknown") \ + ([ $FormatLine " Commit" ($CommitInfo . "/" . [ :pick $CommitId 0 8 ]) ] . "\n") ] . \ [ $FormatLine " Version" $ExpectedConfigVersion ]); } # convert line endings, DOS -> UNIX :set Dos2Unix do={ - :local Input [ :tostr $1 ]; - - :global CharacterReplace; - - :return [ $CharacterReplace $Input ("\r\n") ("\n") ]; + :return [ :tolf [ :tostr $1 ] ]; } # download package from upgrade server @@ -302,8 +365,10 @@ :global CertificateAvailable; :global CleanFilePath; + :global FileExists; :global LogPrint; :global MkDir; + :global RmFile; :global WaitForFile; :if ([ :len $PkgName ] = 0) do={ :return false; } @@ -321,12 +386,12 @@ :return false; } - :if ([ :len [ /file/find where name=$PkgDest type="package" ] ] > 0) do={ + :if ([ $FileExists $PkgDest "package" ] = true) do={ $LogPrint info $0 ("Package file " . $PkgName . " already exists."); :return true; } - :if ([ $CertificateAvailable "R3" ] = false) do={ + :if ([ $CertificateAvailable "ISRG Root X1" ] = false) do={ $LogPrint error $0 ("Downloading required certificate failed."); :return false; } @@ -334,25 +399,22 @@ :local Url ("https://upgrade.mikrotik.com/routeros/" . $PkgVer . "/" . $PkgFile); $LogPrint info $0 ("Downloading package file '" . $PkgName . "'..."); $LogPrint debug $0 ("... from url: " . $Url); - :local Retry 3; - :while ($Retry > 0) do={ - :do { - /tool/fetch check-certificate=yes-without-crl $Url dst-path=$PkgDest; - $WaitForFile $PkgDest; - :if ([ /file/get [ find where name=$PkgDest ] type ] = "package") do={ - :return true; - } - } on-error={ - $LogPrint debug $0 ("Downloading package file failed."); - } + :onerror Err { + /tool/fetch check-certificate=yes-without-crl $Url dst-path=$PkgDest; + $WaitForFile $PkgDest; + } do={ + $LogPrint warning $0 ("Downloading package file '" . $PkgName . "' failed: " . $Err); + :return false; + } - /file/remove [ find where name=$PkgDest ]; - :set Retry ($Retry - 1); + :if ([ $FileExists $PkgDest "package" ] = false) do={ + $LogPrint warning $0 ("Downloaded file is not a package, removing."); + $RmFile $PkgDest; + :return false; } - $LogPrint warning $0 ("Downloading package file '" . $PkgName . "' failed."); - :return false; + :return true; } # return either first (if "true") or second @@ -365,6 +427,7 @@ :if ([ :typeof $1 ] = "time") do={ :return [ $IfThenElse ($1 > 0s) $1 $2 ]; } + # this works for boolean values, literal ones with parentheses :return [ $IfThenElse ([ :len [ :tostr $1 ] ] > 0) $1 $2 ]; } @@ -390,8 +453,77 @@ :return $Return; } +# simple macro to print error message on unintentional error +:set ExitError do={ + :local ExitOK [ :tostr $1 ]; + :local Name [ :tostr $2 ]; + :local Error [ :tostr $3 ]; + + :global IfThenElse; + :global LogPrint; + + :if ($ExitOK = "false") do={ + $LogPrint error $Name ([ $IfThenElse ([ :pick $Name 0 1 ] = "\$") \ + "Function" "Script" ] . " '" . $Name . "' exited with error" . \ + [ $IfThenElse (!($Error ~ "^(|true|false)\$")) (": " . $Error) "." ]); + } +} + +# fetch huge data to file, read in chunks +:set FetchHuge do={ + :local ScriptName [ :tostr $1 ]; + :local Url [ :tostr $2 ]; + :local CheckCert [ :tostr $3 ]; + + :global CleanName; + :global FetchUserAgentStr; + :global GetRandom20CharAlNum; + :global IfThenElse; + :global LogPrint; + :global MkDir; + :global RmDir; + :global RmFile; + :global WaitForFile; + + :set CheckCert [ $IfThenElse ($CheckCert = "false") "no" "yes-without-crl" ]; + + :local DirName ("tmpfs/" . [ $CleanName $ScriptName ]); + :if ([ $MkDir $DirName ] = false) do={ + $LogPrint error $0 ("Failed creating directory!"); + :return false; + } + + :local FileName ($DirName . "/" . [ $CleanName $0 ] . "-" . [ $GetRandom20CharAlNum ]); + :onerror Err { + /tool/fetch check-certificate=$CheckCert $Url dst-path=$FileName \ + http-header-field=({ [ $FetchUserAgentStr $ScriptName ] }) as-value; + } do={ + :if ([ $WaitForFile $FileName 500ms ] = true) do={ + $RmFile $FileName; + } + $LogPrint debug $0 ("Failed downloading from " . $Url . " - " . $Err); + $RmDir $DirName; + :return false; + } + $WaitForFile $FileName; + + :local FileSize [ /file/get $FileName size ]; + :local Return ""; + :local VarSize 0; + :while ($VarSize != $FileSize) do={ + :set Return ($Return . ([ /file/read offset=$VarSize chunk-size=32768 file=$FileName as-value ]->"data")); + :set FileSize [ /file/get $FileName size ]; + :set VarSize [ :len $Return ]; + :if ($VarSize > $FileSize) do={ + :delay 100ms; + } + } + $RmDir $DirName; + :return $Return; +} + # generate user agent string for fetch -:set FetchUserAgent do={ +:set FetchUserAgentStr do={ :local Caller [ :tostr $1 ]; :local Resource [ /system/resource/get ]; @@ -400,6 +532,47 @@ $Resource->"architecture-name" . " " . $Caller . "/Fetch (https://rsc.eworm.de/)"); } +# check for existence of file, optionally with type +:set FileExists do={ + :local FileName [ :tostr $1 ]; + :local Type [ :tostr $2 ]; + + :global FileGet; + + :local FileVal [ $FileGet $FileName ]; + :if ($FileVal = false) do={ + :return false; + } + + :if ([ :len ($FileVal->"size") ] = 0) do={ + :return false; + } + + :if ([ :len $Type ] = 0 || $FileVal->"type" = $Type) do={ + :return true; + } + + :return false; +} + +# get file properties in array, or false on error +:set FileGet do={ + :local FileName [ :tostr $1 ]; + + :global WaitForFile; + + :if ([ $WaitForFile $FileName 0s ] = false) do={ + :return false; + } + + :local FileVal false; + :do { + :set FileVal [ /file/get $FileName ]; + } on-error={ } + + :return $FileVal; +} + # format a line for output :set FormatLine do={ :local Key [ :tostr $1 ]; @@ -454,7 +627,7 @@ } :do { - :if ([ $CertificateAvailable "GTS CA 1P5" ] = false) do={ + :if ([ $CertificateAvailable "GTS Root R4" ] = false) do={ $LogPrint warning $0 ("Downloading required certificate failed."); :error false; } @@ -462,12 +635,12 @@ ("https://api.macvendors.com/" . [ :pick $Mac 0 8 ]) output=user as-value ]->"data"); :return $Vendor; } on-error={ - :do { + :onerror Err { /tool/fetch check-certificate=yes-without-crl ("https://api.macvendors.com/") \ output=none as-value; $LogPrint debug $0 ("The mac vendor is not known in database."); - } on-error={ - $LogPrint warning $0 ("Failed getting mac vendor."); + } do={ + $LogPrint warning $0 ("Failed getting mac vendor: " . $Err); } :return "unknown vendor"; } @@ -533,11 +706,13 @@ :local Base [ :tonum $2 ]; :global EitherOr; + :global IfThenElse; :local Prefix "kMGTPE"; :local Pow 1; :set Base [ $EitherOr $Base 1024 ]; + :local Bin [ $IfThenElse ($Base = 1024) "i" "" ]; :if ($Input < $Base) do={ :return $Input; @@ -550,9 +725,11 @@ :local Tmp1 ($Input * 100 / $Pow); :local Tmp2 ($Tmp1 / 100); :if ($Tmp2 >= 100) do={ - :return ($Tmp2 . $Prefix); + :return ($Tmp2 . $Prefix . $Bin); } - :return ($Tmp2 . "." . [ :pick $Tmp1 [ :len $Tmp2 ] ([ :len $Tmp1 ] - [ :len $Tmp2 ] + 1) ] . $Prefix); + :return ($Tmp2 . "." . \ + [ :pick $Tmp1 [ :len $Tmp2 ] ([ :len $Tmp1 ] - [ :len $Tmp2 ] + 1) ] . \ + $Prefix . $Bin); } } } @@ -614,7 +791,7 @@ :global IsTimeSyncCached; :global IsTimeSyncResetNtp; - :global LogPrint; + :global LogPrintOnce; :if ($IsTimeSyncCached = true) do={ :return true; @@ -626,14 +803,15 @@ :return true; } + :local Uptime [ /system/resource/get uptime ]; :if ([ :typeof $IsTimeSyncResetNtp ] = "nothing") do={ - :set IsTimeSyncResetNtp 0s; + :set IsTimeSyncResetNtp $Uptime; } - :local Uptime [ /system/resource/get uptime ]; :if ($Uptime - $IsTimeSyncResetNtp < 3m) do={ :return false; } + $LogPrintOnce warning $0 ("The ntp client is configured, but did not sync."); :set IsTimeSyncResetNtp $Uptime; /system/ntp/client/set enabled=no; :delay 20ms; @@ -643,7 +821,7 @@ :if ([ /system/license/get ]->"level" = "free" || \ [ /system/resource/get ]->"board-name" = "x86") do={ - $LogPrint debug $0 ("No ntp client configured, relying on RTC for CHR free license and x86."); + $LogPrintOnce debug $0 ("No ntp client configured, relying on RTC for CHR free license and x86."); :return true; } @@ -655,7 +833,7 @@ :return false; } - $LogPrint debug $0 ("No time source configured! Returning gracefully..."); + $LogPrintOnce debug $0 ("No time source configured! Returning gracefully..."); :return true; } @@ -698,27 +876,6 @@ } } -# log and print with same text, optionally exit -# Deprectated! - TODO: remove later -:set LogPrintExit2 do={ - :local Severity [ :tostr $1 ]; - :local Name [ :tostr $2 ]; - :local Message [ :tostr $3 ]; - :local Exit [ :tostr $4 ]; - - :global LogPrint; - :global LogPrintOnce; - - $LogPrintOnce warning $0 \ - ("This function is deprecated and will be removed. Please make your adjustments!"); - - $LogPrint $1 $2 $3; - - :if ($Exit = "true") do={ - :error ("Hard error to exit."); - } -} - # log and print, once until reboot :set LogPrintOnce do={ :local Severity [ :tostr $1 ]; @@ -747,6 +904,9 @@ :return true; } +# The function $LogPrintVerbose is declared, but has no code, intentionally. +# https://rsc.eworm.de/DEBUG.md#verbose-output + # get max value :set MAX do={ :if ($1 > $2) do={ :return $1; } @@ -764,24 +924,31 @@ :local Path [ :tostr $1 ]; :global CleanFilePath; + :global FileGet; :global LogPrint; + :global RmDir; :global WaitForFile; :local MkTmpfs do={ :global LogPrint; :global WaitForFile; - :if ([ :len [ /disk/find where slot=tmpfs type=tmpfs ] ] = 1) do={ + :local TmpFs [ /disk/find where slot=tmpfs type=tmpfs ]; + :if ([ :len $TmpFs ] = 1) do={ + :if ([ /disk/get $TmpFs disabled ] = true) do={ + $LogPrint info $0 ("The tmpfs is disabled, enabling."); + /disk/enable $TmpFs; + } :return true; } $LogPrint info $0 ("Creating disk of type tmpfs."); - /file/remove [ find where name="tmpfs" type="directory" ]; - :do { + $RmDir "tmpfs"; + :onerror Err { /disk/add slot=tmpfs type=tmpfs tmpfs-max-size=([ /system/resource/get total-memory ] / 3); $WaitForFile "tmpfs"; - } on-error={ - $LogPrint warning $0 ("Creating disk of type tmpfs failed!"); + } do={ + $LogPrint warning $0 ("Creating disk of type tmpfs failed: " . $Err); :return false; } :return true; @@ -793,7 +960,11 @@ :return true; } - :if ([ :len [ /file/find where name=$Path type="directory" ] ] = 1) do={ + $LogPrint debug $0 ("Making directory: " . $Path); + + :local PathVal [ $FileGet $Path ]; + :if ($PathVal->"type" = "directory") do={ + $LogPrint debug $0 ("... which already exists."); :return true; } @@ -803,13 +974,11 @@ } } - :do { - :local File ($Path . "/file"); - /file/add name=$File; - $WaitForFile $File; - /file/remove $File; - } on-error={ - $LogPrint warning $0 ("Making directory '" . $Path . "' failed!"); + :onerror Err { + /file/add type="directory" name=$Path; + $WaitForFile $Path; + } do={ + $LogPrint warning $0 ("Making directory '" . $Path . "' failed: " . $Err); :return false; } @@ -830,77 +999,27 @@ "day"=[ :tonum [ :pick $Date 8 10 ] ] }); } -# parse JSON into array -# Warning: This is not a complete parser! -:set ParseJson do={ - :local Input [ :tostr $1 ]; - - :local InLen; - :local Return ({}); - :local Skip 0; - - :if ([ :pick $Input 0 ] = "{") do={ - :set Input [ :pick $Input 1 ([ :len $Input ] - 1) ]; - } - :set Input [ :toarray $Input ]; - :set InLen [ :len $Input ]; - - :for I from=0 to=$InLen do={ - :if ($Skip > 0 || $Input->$I = "\n" || $Input->$I = "\r\n") do={ - :if ($Skip > 0) do={ - :set $Skip ($Skip - 1); - } - } else={ - :local Done false; - :local Key ($Input->$I); - :local Val1 ($Input->($I + 1)); - :local Val2 ($Input->($I + 2)); - :if ($Val1 = ":") do={ - :set Skip 2; - :set ($Return->$Key) $Val2; - :set Done true; - } - :if ($Done = false && $Val1 = ":[") do={ - :local Last false; - :set Skip 1; - :set ($Return->$Key) ({}); - :do { - :set Skip ($Skip + 1); - :local ValX ($Input->($I + $Skip)); - :if ([ :pick $ValX ([ :len $ValX ] - 1) ] = "]") do={ - :set Last true; - :set ValX [ :pick $ValX 0 ([ :len $ValX ] - 1) ]; - } - :set ($Return->$Key) (($Return->$Key), $ValX); - } while=($Last = false && $I + $Skip < $InLen); - :set Done true; - } - :if ($Done = false && $Val1 = ":[]") do={ - :set Skip 1; - :set ($Return->$Key) ({}); - :set Done true; - } - :if ($Done = false) do={ - :set Skip 1; - :set ($Return->$Key) [ :pick $Val1 1 [ :len $Val1 ] ]; - } - } - } - - :return $Return; -} - # parse key value store :set ParseKeyValueStore do={ :local Source $1; + + :if ([ :pick $Source 0 1 ] = "{") do={ + :do { + :return [ :deserialize from=json $Source ]; + } on-error={ } + } + :if ([ :typeof $Source ] != "array") do={ :set Source [ :tostr $1 ]; } :local Result ({}); :foreach KeyValue in=[ :toarray $Source ] do={ :if ([ :find $KeyValue "=" ]) do={ - :set ($Result->[ :pick $KeyValue 0 [ :find $KeyValue "=" ] ]) \ - [ :pick $KeyValue ([ :find $KeyValue "=" ] + 1) [ :len $KeyValue ] ]; + :local Key [ :pick $KeyValue 0 [ :find $KeyValue "=" ] ]; + :local Value [ :pick $KeyValue ([ :find $KeyValue "=" ] + 1) [ :len $KeyValue ] ]; + :if ($Value="true") do={ :set Value true; } + :if ($Value="false") do={ :set Value false; } + :set ($Result->$Key) $Value; } else={ :set ($Result->$KeyValue) true; } @@ -910,11 +1029,18 @@ # print lines with trailing carriage return :set PrettyPrint do={ - :local Input [ :tostr $1 ]; + :put [ :tocrlf [ :tostr $1 ] ]; +} - :global Unix2Dos; +# strip protocol from from url string +:set ProtocolStrip do={ + :local Input [ :tostr $1 ]; - :put [ $Unix2Dos $Input ]; + :local Pos [ :find $Input "://" ]; + :if ([ :typeof $Pos ] = "nil") do={ + :return $Input; + } + :return [ :pick $Input ($Pos + 3) [ :len $Input ] ]; } # delay a random amount of seconds @@ -958,11 +1084,74 @@ :return true; } +# remove directory +:set RmDir do={ + :local DirName [ :tostr $1 ]; + + :global FileGet; + :global LogPrint; + + $LogPrint debug $0 ("Removing directory: ". $DirName); + + :local DirVal [ $FileGet $DirName ]; + :if ($DirVal = false) do={ + $LogPrint debug $0 ("... which does not exist."); + :return true; + } + + :if ($DirVal->"type" != "directory") do={ + $LogPrint error $0 ("Directory '" . $DirName . "' is not a directory."); + :return false; + } + + :onerror Err { + /file/remove $DirName; + } do={ + $LogPrint error $0 ("Removing directory '" . $DirName . "' failed: " . $Err); + :return false; + } + :return true; +} + +# remove file +:set RmFile do={ + :local FileName [ :tostr $1 ]; + + :global FileGet; + :global LogPrint; + + $LogPrint debug $0 ("Removing file: ". $FileName); + + :local FileVal [ $FileGet $FileName ]; + :if ($FileVal = false) do={ + $LogPrint debug $0 ("... which does not exist."); + :return true; + } + + :if ($FileVal->"type" = "directory" || $FileVal->"type" = "disk") do={ + $LogPrint error $0 ("File '" . $FileName . "' is not a file."); + :return false; + } + + :onerror Err { + /file/remove $FileName; + } do={ + $LogPrint error $0 ("Removing file '" . $FileName . "' failed: " . $Err); + :return false; + } + :return true; +} + # check if script is run from terminal :set ScriptFromTerminal do={ :local Script [ :tostr $1 ]; :global LogPrint; + :global ScriptLock; + + :if ([ $ScriptLock $Script ] = false) do={ + :return false; + } :foreach Job in=[ /system/script/job/find where script=$Script ] do={ :set Job [ /system/script/job/get $Job ]; @@ -974,26 +1163,31 @@ :return true; } } - $LogPrint debug $0 ("Script " . $Script . " NOT started from terminal."); + $LogPrint debug $0 ("Script " . $Script . " NOT started from terminal."); :return false; } # install new scripts, update existing scripts -:set ScriptInstallUpdate do={ +:set ScriptInstallUpdate do={ :onerror Err { :local Scripts [ :toarray $1 ]; :local NewComment [ :tostr $2 ]; + :global CommitId; + :global CommitInfo; :global ExpectedConfigVersion; + :global GlobalConfigReady; + :global GlobalFunctionsReady; :global Identity; :global IDonate; :global NoNewsAndChangesNotification; :global ScriptUpdatesBaseUrl; + :global ScriptUpdatesCRLF; :global ScriptUpdatesUrlSuffix; :global CertificateAvailable; :global EitherOr; - :global FetchUserAgent; + :global FetchUserAgentStr; :global Grep; :global IfThenElse; :global LogPrint; @@ -1004,7 +1198,7 @@ :global SymbolForNotification; :global ValidateSyntax; - :if ([ $CertificateAvailable "E1" ] = false) do={ + :if ([ $CertificateAvailable "ISRG Root X2" ] = false) do={ $LogPrint warning $0 ("Downloading certificate failed, trying without."); } @@ -1015,9 +1209,18 @@ } } + :local CommitIdBefore $CommitId; :local ExpectedConfigVersionBefore $ExpectedConfigVersion; - :local ReloadGlobalFunctions false; - :local ReloadGlobalConfig false; + :local ReloadGlobal false; + :local DeviceMode [ /system/device-mode/get ]; + + :local CheckSums ({}); + :do { + :local Url ($ScriptUpdatesBaseUrl . "checksums.json" . $ScriptUpdatesUrlSuffix); + $LogPrint debug $0 ("Fetching checksums from url: " . $Url); + :set CheckSums [ :deserialize from=json ([ /tool/fetch check-certificate=yes-without-crl \ + http-header-field=({ [ $FetchUserAgentStr $0 ] }) $Url output=user as-value ]->"data") ]; + } on-error={ } :foreach Script in=[ /system/script/find where source~"^#!rsc by RouterOS\r?\n" ] do={ :local ScriptVal [ /system/script/get $Script ]; @@ -1032,79 +1235,113 @@ } } - :if (!($ScriptInfo->"ignore" = true)) do={ - :do { + :do { + :if ($ScriptInfo->"ignore" = true) do={ + $LogPrint debug $0 ("Ignoring script '" . $ScriptVal->"name" . "', as requested."); + :error true; + } + + :local CheckSum ($CheckSums->($ScriptVal->"name")); + :if ([ :len ($ScriptInfo->"base-url") ] = 0 && [ :len ($ScriptInfo->"url-suffix") ] = 0 && \ + [ :convert transform=md5 to=hex [ :tolf ($ScriptVal->"source") ] ] = $CheckSum) do={ + $LogPrint debug $0 ("Checksum for script '" . $ScriptVal->"name" . "' matches, ignoring."); + :error true; + } + + :if ([ :len ($ScriptInfo->"certificate") ] > 0) do={ + :if ([ $CertificateAvailable ($ScriptInfo->"certificate") ] = false) do={ + $LogPrint warning $0 ("Downloading certificate failed, trying without."); + } + } + + :onerror Err { :local BaseUrl [ $EitherOr ($ScriptInfo->"base-url") $ScriptUpdatesBaseUrl ]; :local UrlSuffix [ $EitherOr ($ScriptInfo->"url-suffix") $ScriptUpdatesUrlSuffix ]; :local Url ($BaseUrl . $ScriptVal->"name" . ".rsc" . $UrlSuffix); $LogPrint debug $0 ("Fetching script '" . $ScriptVal->"name" . "' from url: " . $Url); :local Result [ /tool/fetch check-certificate=yes-without-crl \ - http-header-field=({ [ $FetchUserAgent $0 ] }) $Url output=user as-value ]; + http-header-field=({ [ $FetchUserAgentStr $0 ] }) $Url output=user as-value ]; :if ($Result->"status" = "finished") do={ - :set SourceNew ($Result->"data"); + :set SourceNew [ :tolf ($Result->"data") ]; } - } on-error={ + } do={ + $LogPrint warning $0 ("Failed fetching script '" . $ScriptVal->"name" . "': " . $Err); :if ($ScriptVal->"source" = "#!rsc by RouterOS\n") do={ - $LogPrint warning $0 ("Failed fetching script '" . $ScriptVal->"name" . \ - "', removing dummy. Typo on installation?"); + $LogPrint warning $0 ("Removing dummy. Typo on installation?"); /system/script/remove $Script; - } else={ - $LogPrint warning $0 ("Failed fetching script '" . $ScriptVal->"name" . "'!"); } + :error false; } - } - :if ([ :len $SourceNew ] > 0) do={ - :if ($SourceNew != $ScriptVal->"source") do={ - :if ([ :pick $SourceNew 0 18 ] = "#!rsc by RouterOS\n") do={ - :local Required ([ $ParseKeyValueStore [ $Grep $SourceNew ("\23 requires RouterOS, ") ] ]->"version"); - :if ([ $RequiredRouterOS $0 [ $EitherOr $Required "0.0" ] false ] = true) do={ - :if ([ $ValidateSyntax $SourceNew ] = true) do={ - $LogPrint info $0 ("Updating script: " . $ScriptVal->"name"); - /system/script/set owner=($ScriptVal->"name") source=$SourceNew $Script; - :if ($ScriptVal->"name" = "global-config") do={ - :set ReloadGlobalConfig true; - } - :if ($ScriptVal->"name" = "global-functions" || $ScriptVal->"name" ~ ("^mod/.")) do={ - :set ReloadGlobalFunctions true; - } - } else={ - $LogPrint warning $0 ("Syntax validation for script '" . $ScriptVal->"name" . \ - "' failed! Ignoring!"); - } - } else={ - $LogPrintOnce warning $0 ("The script '" . $ScriptVal->"name" . "' requires RouterOS " . \ - $Required . ", which is not met by your installation. Ignoring!"); - } - } else={ - $LogPrint warning $0 ("Looks like new script '" . $ScriptVal->"name" . \ + :if ([ :len $SourceNew ] = 0) do={ + $LogPrint debug $0 ("No update for script '" . $ScriptVal->"name" . "'."); + :error false; + } + + :local SourceCRLF [ :tocrlf $SourceNew ]; + :if ($SourceNew = $ScriptVal->"source" || $SourceCRLF = $ScriptVal->"source") do={ + $LogPrint debug $0 ("Script '" . $ScriptVal->"name" . "' did not change."); + :error false; + } + + :if ([ :pick $SourceNew 0 18 ] != "#!rsc by RouterOS\n") do={ + $LogPrint warning $0 ("Looks like new script '" . $ScriptVal->"name" . \ "' is not valid (missing shebang). Ignoring!"); + :error false; + } + + :local RequiredROS ([ $ParseKeyValueStore [ $Grep $SourceNew ("\23 requires RouterOS, ") ] ]->"version"); + :if ([ $RequiredRouterOS $0 [ $EitherOr $RequiredROS "0.0" ] false ] = false) do={ + $LogPrintOnce warning $0 ("The script '" . $ScriptVal->"name" . "' requires RouterOS " . \ + $RequiredROS . ", which is not met by your installation. Ignoring!"); + :error false; + } + + :local RequiredDM [ $ParseKeyValueStore [ $Grep $SourceNew ("\23 requires device-mode, ") ] ]; + :local MissingDM ({}); + :foreach Feature,Value in=$RequiredDM do={ + :if ([ :typeof ($DeviceMode->$Feature) ] = "bool" && ($DeviceMode->$Feature) = false) do={ + :set MissingDM ($MissingDM, $Feature); } - } else={ - $LogPrint debug $0 ("Script '" . $ScriptVal->"name" . "' did not change."); } - } else={ - $LogPrint debug $0 ("No update for script '" . $ScriptVal->"name" . "'."); - } + :if ([ :len $MissingDM ] > 0) do={ + $LogPrintOnce warning $0 ("The script '" . $ScriptVal->"name" . "' requires disabled " . \ + "device-mode features (" . [ :tostr $MissingDM ] . "). Ignoring!"); + :error false; + } + + :if ([ $ValidateSyntax $SourceNew ] = false) do={ + $LogPrint warning $0 ("Syntax validation for script '" . $ScriptVal->"name" . "' failed! Ignoring!"); + :error false; + } + + $LogPrint info $0 ("Updating script: " . $ScriptVal->"name"); + /system/script/set owner=($ScriptVal->"name") \ + source=[ $IfThenElse ($ScriptUpdatesCRLF = true) $SourceCRLF $SourceNew ] $Script; + :if ($ScriptVal->"name" = "global-config" || \ + $ScriptVal->"name" = "global-functions" || \ + $ScriptVal->"name" ~ ("^mod/.")) do={ + :set ReloadGlobal true; + } + } on-error={ } } - :if ($ReloadGlobalFunctions = true) do={ - $LogPrint info $0 ("Reloading global functions."); - :do { + :if ($ReloadGlobal = true) do={ + $LogPrint info $0 ("Reloading global configuration and functions."); + :set GlobalConfigReady false; + :set GlobalFunctionsReady false; + :delay 1s; + + :onerror Err { + /system/script/run global-config; /system/script/run global-functions; - } on-error={ - $LogPrint error $0 ("Reloading global functions failed!"); + } do={ + $LogPrint error $0 ("Reloading global configuration and functions failed! " . $Err); } } - :if ($ReloadGlobalConfig = true) do={ - $LogPrint info $0 ("Reloading global configuration."); - :do { - /system/script/run global-config; - } on-error={ - $LogPrint error $0 ("Reloading global configuration failed!" . \ - " Syntax error or missing overlay?"); - } + :if ($CommitId != "unknown" && $CommitIdBefore != $CommitId) do={ + $LogPrint info $0 ("Updated to commit: " . $CommitInfo . "/" . [ :pick $CommitId 0 8 ]); } :if ($ExpectedConfigVersionBefore > $ExpectedConfigVersion) do={ @@ -1118,24 +1355,24 @@ :global GlobalConfigMigration; :local ChangeLogCode; - :do { + :onerror Err { :local Url ($ScriptUpdatesBaseUrl . "news-and-changes.rsc" . $ScriptUpdatesUrlSuffix); $LogPrint debug $0 ("Fetching news, changes and migration: " . $Url); :local Result [ /tool/fetch check-certificate=yes-without-crl \ - http-header-field=({ [ $FetchUserAgent $0 ] }) $Url output=user as-value ]; + http-header-field=({ [ $FetchUserAgentStr $0 ] }) $Url output=user as-value ]; :if ($Result->"status" = "finished") do={ :set ChangeLogCode ($Result->"data"); } - } on-error={ - $LogPrint warning $0 ("Failed fetching news, changes and migration!"); + } do={ + $LogPrint warning $0 ("Failed fetching news, changes and migration: " . $Err); } :if ([ :len $ChangeLogCode ] > 0) do={ :if ([ $ValidateSyntax $ChangeLogCode ] = true) do={ - :do { + :onerror Err { [ :parse $ChangeLogCode ]; - } on-error={ - $LogPrint warning $0 ("The changelog failed to run!"); + } do={ + $LogPrint warning $0 ("The changelog failed to run: " . $Err); } } else={ $LogPrint warning $0 ("The changelog failed syntax validation!"); @@ -1145,18 +1382,24 @@ :if ([ :len $GlobalConfigMigration ] > 0) do={ :for I from=($ExpectedConfigVersionBefore + 1) to=$ExpectedConfigVersion do={ :local Migration ($GlobalConfigMigration->[ :tostr $I ]); - :if ([ :typeof $Migration ] = "str") do={ - :if ([ $ValidateSyntax $Migration ] = true) do={ - $LogPrint info $0 ("Applying migration for change " . $I . ": " . $Migration); - :do { - [ :parse $Migration ]; - } on-error={ - $LogPrint warning $0 ("Migration code for change " . $I . " failed to run!"); - } - } else={ + :do { + :if ([ :typeof $Migration ] != "str") do={ + $LogPrint debug $0 ("Migration code for change " . $I . " is not available."); + :error false; + } + + :if ([ $ValidateSyntax $Migration ] = false) do={ $LogPrint warning $0 ("Migration code for change " . $I . " failed syntax validation!"); + :error false; } - } + + $LogPrint info $0 ("Applying migration for change " . $I . ": " . $Migration); + :onerror Err { + [ :parse $Migration ]; + } do={ + $LogPrint warning $0 ("Migration code for change " . $I . " failed to run: " . $Err); + } + } on-error={ } } } @@ -1196,12 +1439,14 @@ :set GlobalConfigChanges; :set GlobalConfigMigration; } -} +} do={ + :global ExitError; $ExitError false $0 $Err; +} } # lock script against multiple invocation :set ScriptLock do={ - :local Script [ :tostr $1 ]; - :local WaitMax ([ :tonum $3 ] * 10); + :local Script [ :tostr $1 ]; + :local WaitMax [ :totime $2 ]; :global GetRandom20CharAlNum; :global IfThenElse; @@ -1290,6 +1535,10 @@ :set ($ScriptLockOrder->$Script) ({}); } + :if ([ :typeof $WaitMax ] = "nil" ) do={ + :set WaitMax 0s; + } + :if ([ :len [ /system/script/find where name=$Script ] ] = 0) do={ $LogPrint error $0 ("A script named '" . $Script . "' does not exist!"); :error false; @@ -1309,30 +1558,36 @@ :local MyTicket [ $GetRandom20CharAlNum 6 ]; $AddTicket $Script $MyTicket; - :local WaitCount 0; - :while ($WaitMax > $WaitCount && ([ $IsFirstTicket $Script $MyTicket ] = false || [ $TicketCount $Script ] < [ $JobCount $Script ])) do={ - :set WaitCount ($WaitCount + 1); - :delay 100ms; + :local WaitInterval ($WaitMax / 20); + :local WaitTime $WaitMax; + :while ($WaitTime > 0 && \ + ([ $IsFirstTicket $Script $MyTicket ] = false || \ + [ $TicketCount $Script ] < [ $JobCount $Script ])) do={ + :set WaitTime ($WaitTime - $WaitInterval); + :delay $WaitInterval; } - :if ([ $IsFirstTicket $Script $MyTicket ] = true && [ $TicketCount $Script ] = [ $JobCount $Script ]) do={ + :if ([ $IsFirstTicket $Script $MyTicket ] = true && \ + [ $TicketCount $Script ] = [ $JobCount $Script ]) do={ $RemoveTicket $Script $MyTicket; $CleanupTickets $Script; :return true; } $RemoveTicket $Script $MyTicket; - $LogPrint info $0 ("Script '" . $Script . "' started more than once" . [ $IfThenElse ($WaitCount > 0) \ - " and timed out waiting for lock" "" ] . "..."); + $LogPrint debug $0 ("Script '" . $Script . "' started more than once" . \ + [ $IfThenElse ($WaitTime < $WaitMax) " and timed out waiting for lock" "" ] . "..."); :return false; } # send notification via NotificationFunctions - expects at least two string arguments -:set SendNotification do={ +:set SendNotification do={ :onerror Err { :global SendNotification2; - $SendNotification2 ({ subject=$1; message=$2; link=$3; silent=$4 }); -} + $SendNotification2 ({ origin=$0; subject=$1; message=$2; link=$3; silent=$4 }); +} do={ + :global ExitError; $ExitError false $0 $Err; +} } # send notification via NotificationFunctions - expects one array argument :set SendNotification2 do={ @@ -1416,12 +1671,7 @@ # convert line endings, UNIX -> DOS :set Unix2Dos do={ - :local Input [ :tostr $1 ]; - - :global CharacterReplace; - - :return [ $CharacterReplace [ $CharacterReplace $Input \ - ("\n") ("\r\n") ] ("\r\r\n") ("\r\n") ]; + :return [ :tocrlf [ :tostr $1 ] ]; } # url encoding @@ -1455,9 +1705,12 @@ :set ValidateSyntax do={ :local Code [ :tostr $1 ]; - :do { + :global LogPrint; + + :onerror Err { [ :parse (":local Validate do={\n" . $Code . "\n}") ]; - } on-error={ + } do={ + $LogPrint debug $0 ("Valdation failed: " . $Err); :return false; } :return true; @@ -1472,7 +1725,7 @@ :global CharacterReplace; :set Input [ $CharacterReplace $Input "." "," ]; - :foreach I in={ "alpha"; "beta"; "rc" } do={ + :foreach I in={ "zero"; "alpha"; "beta"; "rc" } do={ :set Input [ $CharacterReplace $Input $I ("," . $I . ",") ]; } @@ -1483,6 +1736,7 @@ :set Return ($Return + 0xff00); :set Multi ($Multi / 0x100); } else={ + :if ($Value = "zero") do={ } :if ($Value = "alpha") do={ :set Return ($Return + 0x3f00); } :if ($Value = "beta") do={ :set Return ($Return + 0x5f00); } :if ($Value = "rc") do={ :set Return ($Return + 0x7f00); } @@ -1520,19 +1774,31 @@ :global CleanFilePath; :global EitherOr; + :global MAX; :set FileName [ $CleanFilePath $FileName ]; - :local I 1; - :local Delay ([ :totime [ $EitherOr $WaitTime 2s ] ] / 20); + :local Delay ([ $MAX [ $EitherOr $WaitTime 2s ] 100ms ] / 9); - :while ([ :len [ /file/find where name=$FileName ] ] = 0) do={ - :if ($I >= 20) do={ - :return false; - } + :do { + :retry { + :if ([ :len [ /file/find where name=$FileName ] ] = 0) do={ + :error false; + } + } delay=$Delay max=10; + } on-error={ + :return false; + } + + :while ([ :len [ /file/find where name=$FileName ] ] > 0) do={ + :do { + /file/get $FileName; + :return true; + } on-error={ } :delay $Delay; - :set I ($I + 1); + :set Delay ($Delay * 3 / 2); } - :return true; + + :return false; } # wait to be fully connected (default route is reachable, time is sync, DNS resolves) @@ -1559,10 +1825,10 @@ :foreach Script in=[ /system/script/find where name ~ "^mod/." ] do={ :local ScriptVal [ /system/script/get $Script ]; :if ([ $ValidateSyntax ($ScriptVal->"source") ] = true) do={ - :do { + :onerror Err { /system/script/run $Script; - } on-error={ - $LogPrint error $0 ("Module '" . $ScriptVal->"name" . "' failed to run."); + } do={ + $LogPrint error $0 ("Module '" . $ScriptVal->"name" . "' failed to run: " . $Err); } } else={ $LogPrint error $0 ("Module '" . $ScriptVal->"name" . "' failed syntax validation, skipping."); |