From 2b3a8ea6f480345486aae46fa88a0d00cd7b2b0a Mon Sep 17 00:00:00 2001 From: vados-dev <192440777+vados-dev@users.noreply.github.com> Date: Wed, 1 Apr 2026 03:45:48 +0300 Subject: [PATCH] Auto --- html/AM-CheckUpdates.rsc | 2 +- html/AM-GlobalFunc.rsc | 18 +- html/AM-Init-Script.rsc | 196 +- html/AM-SFTP-backup.rsc | 653 +++--- html/AM-setup.rsc | 2 +- html/README.md | 9 +- html/global-functions.rsc | 1958 +++++++++++++++++ html/logo/index.html | 10 +- html/mod/AM-DateTime.rsc | 22 +- html/{ => mod}/AM-GitBackup.rsc | 7 +- html/mod/AM-IPbridge.rsc | 30 + html/mod/{ipcalc.rsc => AM-IPcalc.rsc} | 41 +- html/mod/AM-RunOnce.rsc | 3 +- html/mod/AM-TG-Bot.rsc | 2 +- html/mod/AM-TG-Notifications.rsc | 2 +- .../AM-UpdateCertsCustom.rsc} | 5 +- 16 files changed, 2494 insertions(+), 466 deletions(-) create mode 100644 html/global-functions.rsc rename html/{ => mod}/AM-GitBackup.rsc (89%) create mode 100644 html/mod/AM-IPbridge.rsc rename html/mod/{ipcalc.rsc => AM-IPcalc.rsc} (53%) rename html/{Update-Certificates.rsc => mod/AM-UpdateCertsCustom.rsc} (95%) diff --git a/html/AM-CheckUpdates.rsc b/html/AM-CheckUpdates.rsc index d137fe875..9fbbaefd7 100644 --- a/html/AM-CheckUpdates.rsc +++ b/html/AM-CheckUpdates.rsc @@ -41,7 +41,7 @@ :set ExitOK true; :error false; } - + :local DoUpdate do={ :local ScriptName [ :tostr $1 ]; diff --git a/html/AM-GlobalFunc.rsc b/html/AM-GlobalFunc.rsc index 3600c5869..7e27cdf6f 100644 --- a/html/AM-GlobalFunc.rsc +++ b/html/AM-GlobalFunc.rsc @@ -16,9 +16,9 @@ :global GlobalFuncReady false; # Git commit id & info, expected configuration version -:global CommitId "main/196/3419e47d"; +:global CommitId "main/197/6f15f19c"; :global CommitTAG "unknown"; -:global CommitInfo "Autocommit in repo: ros.vados.ru, generated by owner: root, in 04:35:03 18.03.2026."; +:global CommitInfo "Autocommit in repo: ros.vados.ru, generated by owner: root, in 03:45:02 01.04.2026."; # The variables are received right here. :global GlobalConfVersion 138; @@ -149,11 +149,11 @@ :retry command={/system/package/update set channel=$updateChannel} delay=300ms max=5 on-error={}; $LogPrint debug $0 ("Set updates channel to: $updateChannel"); } - + :local Update [ /system/package/update/get ]; :local Channel ($Update->"channel"); $LogPrint debug $0 ("Update Channel: " . $Channel . "."); - + :local StatusPkg "undefined"; $LogPrint debug $0 ("Checking for updates..."); :retry command={:set $StatusPkg ([/system/package/update/check-for-updates without-paging as-value]->"status")} \ @@ -191,14 +191,14 @@ :global devBoardName; :global devModel; - + :global arrDeviceInfo [:toarray {"section"={"key"=""; "value"="";} }]; #:do {:set ($arrDeviceInfo->"Updates") [[ :parse "/system/package/update/get" ]]} on-error={} :global devRbRevision; :global devRbSerialNumber; :global devRbCurrentFw; :global devRbUpgradeFw; - + :global devRunning; :global devCurArch; :global ROsVerCurrent; @@ -211,14 +211,14 @@ :global runningVersion; :global devBuildTime; :global GetPubIp; - + :local License [/system/license/get]; :local Resource [/system/resource/get]; :set devBuildTime ($Resource->"build-time"); :set runningChannelVersion ($Resource->"version"); :set runningChannel [$ParseCustom $runningChannelVersion "(" ")" 1]; :set runningVersion [$ParseCustom $runningChannelVersion " "]; - + :do { :if ([:pick $devBoardName 0 3] != "CHR" or [:pick $devBoardName 0 3] != "x86") do={ :local RouterBoard; @@ -269,7 +269,7 @@ :local Resource [/system/resource/get]; :local Snmp [ /snmp/get ]; :local Update [ /system/package/update/get ]; - + :set Return ([ $FormatLine "Hostname:" $Identity ] . "\n" . \ [ $IfThenElse ([ :len ($Snmp->"location") ] > 0) \ ([ $FormatLine "Location:" ($Snmp->"location") ] . "\n") ] . \ diff --git a/html/AM-Init-Script.rsc b/html/AM-Init-Script.rsc index 64ff49f61..42b7d2c97 100644 --- a/html/AM-Init-Script.rsc +++ b/html/AM-Init-Script.rsc @@ -1,98 +1,98 @@ -#!rsc by Vados -# RouterOS script: AM-Init -# Script comment: Initial Script for setup routeros scrypt system -# Copyright (c) 2005-2026 Vados -# -# -# requires RouterOS, version=7.19 -# requires device-mode, fetch -# -# Additional commands: -# /system/script/set source=[ :tocrlf [ get $ScriptName source ] ] $ScriptName; -# -#:global GlobalEnvRemove true; /system/script {run Amster-GlobalEnvRemove; }; -#:delay 1s; -#:global ReloadGlobal true; - -:global GlobalScriptsUrl "https://ros.vados.ru/"; -:global defOwner "vados"; -:global InitFirstRun; - -:set InitFirstRun do={ - :global GlobalScriptsUrl; - :global defOwner; - #:global ParseKeyValueStore; - #:local AmUpdCRLF true; - #:local IfThenElse; - #:set IfThenElse do={:if ([ :tostr $1 ] = "true" || [ :tobool $1 ] = true) do={:return $2}; :return $3} - #/certificate/settings/set builtin-trust-store=fetch; - #/tool/fetch "$scriptsUrl/certs/Root-YE.pem" dst-path="root-ye.pem"; - #/certificate/import file-name="root-ye.pem" passphrase=""; - # For basic verification we rename the certificate and print it by fingerprint. Make sure exactly this one certificate ("Root-YE") is shown. - #/certificate/set name="Root-YE" [ find where common-name="Root YE" ]; - #/certificate/print proplist=name,fingerprint where fingerprint="e14ffcad5b0025731006caa43a121a22d8e9700f4fb9cf852f02a708aa5d5666"; - #:log info "\nGlobalScriptsUrl = $GlobalScriptsUrl"; - :local Scripts { - "Amster-GlobalConfig" - "Amster-GlobalFunc"; -# "Amster-SFTP-backup" -# "Amster-BkpEmail-and-upd"; - }; -# /system/script/set owner=($ScriptVal->"name") \ -# source=[ $IfThenElse ($ScriptUpdatesCRLF = true) $SourceCRLF $SourceNew ] $Script; - :foreach Script in=$Scripts do={ - :put "Installing $Script..."; - /system/script/remove [ find where name=$Script ]; - /system/script/add name=$Script owner=$defOwner source=([ /tool/fetch ("$GlobalScriptsUrl" . $Script . ".rsc") output=user as-value ]->"data"); - /system/script/set source=[ :tocrlf [ get $Script source ] ] $Script; - }; - /system/script { run Amster-GlobalConfig; run Amster-GlobalFunc; } -} -:global InitUpdRun; -:set InitUpdRun do={ - #:local UpdScript [ :tostr $1 ]; - #:global ParseKeyValueStore; - :global GlobalScriptsUrl; - :global defOwner; - :local Scripts { -# "Amster-GlobalConfig" -# "Amster-GlobalFunc" -# "Amster-GlobalRemove" - "Amster-backup-SFTP" - "Amster-SFTP-backup" - "Amster-BkpEmail-and-upd" - "Amster-TG-Bot" - "Amster-TG-Notification"; - }; - :foreach Script in=$Scripts do={ - :put "updating $Script..."; - /system/script/remove [ find where name=$Script ]; - /system/script/add name=$Script owner=$defOwner source=([ /tool/fetch ("$GlobalScriptsUrl" . $Script . ".rsc") output=user as-value ]->"data"); - /system/script/set source=[ :tocrlf [ get $Script source ] ] $Script; - }; -# /system/script/add name=$Script owner=$AmOwner source=([ /tool/fetch check-certificate=yes-without-crl ("$GlobalScriptsUrl" . $Script . ".rsc") output=user as-value ]->"data"); }; -} - -$InitFirstRun; -#$InitUpdRun; - -#$setUpdExist; -#:global setUpdExist; -#:global ParseKeyValueStore; -#:set setUpdExist do={ -#:do { -#:global ParseKeyValueStore; -# :foreach Script in=[ /system/script/find where source~"^#!rsc by Vados\r?\n#comment=*" ] do={ -# :local ScriptVal [ /system/script/get $Script ]; -# :local ScriptInfo [ $ParseKeyValueStore ($ScriptVal->"#comment") ]; -# :local scriptName ($ScriptVal->"name"); -#:log warning ("\n\$scriptName = ". $scriptName); -#: log warning ("\n\$ScriptInfo = ". $ScriptInfo); -# } -#} on-error={} -#} -#$name;$owner;$policy;$dont-require-permissions;$last-started;$run-count;$source;$invalid;$comment;$.id;$.nextid;$.dead;$.about; -#;(evl / (evl /docommand=;(evl / (evl /foreachcounter=$Script;do=;(evl / (evl /localname=$ScriptVal;value=(evl (evl /system/script/getnumber=$Script))) (evl /localname=$ScriptInfo;value=(evl (<%% $ParseKeyValueStore (> $ParseKeyValueStore);(-> $ScriptVal comment)))) (evl /localname=$scriptName) (evl /setname=$scriptName;value=(-> #$ScriptVal name)) (evl /log/errormessage=(. -# $ ScriptInfo = $ScriptInfo)));in=(evl (evl /system/script/findwhere=$name;$owner;$policy;$dont-require-permissions;$last-started;$run-count;$source;$invalid;$comment;$.id;$.nextid;$.dead;$.about;(~ $source (. ^#!rsc by Vados -# ? -#));5))));on-error=;(evl /))) +#!rsc by Vados +# RouterOS script: AM-Init +# Script comment: Initial Script for setup routeros scrypt system +# Copyright (c) 2005-2026 Vados +# +# +# requires RouterOS, version=7.19 +# requires device-mode, fetch +# +# Additional commands: +# /system/script/set source=[ :tocrlf [ get $ScriptName source ] ] $ScriptName; +# +#:global GlobalEnvRemove true; /system/script {run Amster-GlobalEnvRemove; }; +#:delay 1s; +#:global ReloadGlobal true; + +:global GlobalScriptsUrl "https://ros.vados.ru/"; +:global defOwner "vados"; +:global InitFirstRun; + +:set InitFirstRun do={ + :global GlobalScriptsUrl; + :global defOwner; + #:global ParseKeyValueStore; + #:local AmUpdCRLF true; + #:local IfThenElse; + #:set IfThenElse do={:if ([ :tostr $1 ] = "true" || [ :tobool $1 ] = true) do={:return $2}; :return $3} + #/certificate/settings/set builtin-trust-store=fetch; + #/tool/fetch "$scriptsUrl/certs/Root-YE.pem" dst-path="root-ye.pem"; + #/certificate/import file-name="root-ye.pem" passphrase=""; + # For basic verification we rename the certificate and print it by fingerprint. Make sure exactly this one certificate ("Root-YE") is shown. + #/certificate/set name="Root-YE" [ find where common-name="Root YE" ]; + #/certificate/print proplist=name,fingerprint where fingerprint="e14ffcad5b0025731006caa43a121a22d8e9700f4fb9cf852f02a708aa5d5666"; + #:log info "\nGlobalScriptsUrl = $GlobalScriptsUrl"; + :local Scripts { + "Amster-GlobalConfig" + "Amster-GlobalFunc"; +# "Amster-SFTP-backup" +# "Amster-BkpEmail-and-upd"; + }; +# /system/script/set owner=($ScriptVal->"name") \ +# source=[ $IfThenElse ($ScriptUpdatesCRLF = true) $SourceCRLF $SourceNew ] $Script; + :foreach Script in=$Scripts do={ + :put "Installing $Script..."; + /system/script/remove [ find where name=$Script ]; + /system/script/add name=$Script owner=$defOwner source=([ /tool/fetch ("$GlobalScriptsUrl" . $Script . ".rsc") output=user as-value ]->"data"); + /system/script/set source=[ :tocrlf [ get $Script source ] ] $Script; + }; + /system/script { run Amster-GlobalConfig; run Amster-GlobalFunc; } +} +:global InitUpdRun; +:set InitUpdRun do={ + #:local UpdScript [ :tostr $1 ]; + #:global ParseKeyValueStore; + :global GlobalScriptsUrl; + :global defOwner; + :local Scripts { +# "Amster-GlobalConfig" +# "Amster-GlobalFunc" +# "Amster-GlobalRemove" + "Amster-backup-SFTP" + "Amster-SFTP-backup" + "Amster-BkpEmail-and-upd" + "Amster-TG-Bot" + "Amster-TG-Notification"; + }; + :foreach Script in=$Scripts do={ + :put "updating $Script..."; + /system/script/remove [ find where name=$Script ]; + /system/script/add name=$Script owner=$defOwner source=([ /tool/fetch ("$GlobalScriptsUrl" . $Script . ".rsc") output=user as-value ]->"data"); + /system/script/set source=[ :tocrlf [ get $Script source ] ] $Script; + }; +# /system/script/add name=$Script owner=$AmOwner source=([ /tool/fetch check-certificate=yes-without-crl ("$GlobalScriptsUrl" . $Script . ".rsc") output=user as-value ]->"data"); }; +} + +$InitFirstRun; +#$InitUpdRun; + +#$setUpdExist; +#:global setUpdExist; +#:global ParseKeyValueStore; +#:set setUpdExist do={ +#:do { +#:global ParseKeyValueStore; +# :foreach Script in=[ /system/script/find where source~"^#!rsc by Vados\r?\n#comment=*" ] do={ +# :local ScriptVal [ /system/script/get $Script ]; +# :local ScriptInfo [ $ParseKeyValueStore ($ScriptVal->"#comment") ]; +# :local scriptName ($ScriptVal->"name"); +#:log warning ("\n\$scriptName = ". $scriptName); +#: log warning ("\n\$ScriptInfo = ". $ScriptInfo); +# } +#} on-error={} +#} +#$name;$owner;$policy;$dont-require-permissions;$last-started;$run-count;$source;$invalid;$comment;$.id;$.nextid;$.dead;$.about; +#;(evl / (evl /docommand=;(evl / (evl /foreachcounter=$Script;do=;(evl / (evl /localname=$ScriptVal;value=(evl (evl /system/script/getnumber=$Script))) (evl /localname=$ScriptInfo;value=(evl (<%% $ParseKeyValueStore (> $ParseKeyValueStore);(-> $ScriptVal comment)))) (evl /localname=$scriptName) (evl /setname=$scriptName;value=(-> #$ScriptVal name)) (evl /log/errormessage=(. +# $ ScriptInfo = $ScriptInfo)));in=(evl (evl /system/script/findwhere=$name;$owner;$policy;$dont-require-permissions;$last-started;$run-count;$source;$invalid;$comment;$.id;$.nextid;$.dead;$.about;(~ $source (. ^#!rsc by Vados +# ? +#));5))));on-error=;(evl /))) diff --git a/html/AM-SFTP-backup.rsc b/html/AM-SFTP-backup.rsc index ec8d6d2ba..868d2c803 100644 --- a/html/AM-SFTP-backup.rsc +++ b/html/AM-SFTP-backup.rsc @@ -1,327 +1,326 @@ -#!rsc by Vados -# RouterOS script: AM-SFTP-backup -# Script comment: BackUp via /tool fetch SFTP -# Copyright (c) 2005-2026 Vados -# -# -# -# requires RouterOS, version=7.21 -# requires device-mode, fetch, scheduler -# -# Based on: -# https://forum.mikrotik.com/viewtopic.php?t=159432 -# https://forum.mikrotik.com/viewtopic.php?p=858564#p858564 -# -### Info Log Action -# $stage (string) selects action text -# $msg (string) additional message, usually the backup stage or filename -# $error (bool) (optional) creates error log instead of info if 'true' -:local ExitOK false; -:onerror Err { - :global GlobalConfReady; :global GlobalFuncReady; - :retry { :if ($GlobalConfReady != true || $GlobalFuncReady != true) \ - do={ :error ("Global configs or functions not ready."); }; } delay=500ms max=50; - :local ScriptName [ :jobname ]; - -:global Identity; -:global checkRunJob; -:global LogPrint; -:global RandomDelay; -:global MkDir; -:global RmDir; -:global RmFile; -:global FileGet; -:global WaitForFile; -:global MiniDateTimeStamp; -:global OSVersion; -:global devBoardName; -:global BackupRandomDelay; -:global BackupUploadUrl; -:global BackupUploadUser; -:global BackupUploadPass; -:global BackupRmLocal; -:global BackupSendBinary; -:global BackupLocalRoot; -:global BackupEncrypt; -:global BackupPassword; -:global BackupSens; -:global BackupSendGlobalConfig; -:global BackupUser; -:global BackupLicense; -:global BackupSshKeys; -:global BackupCerts; -:global BackupCertsPasswd; -:global BackupUserMan; -:global BackupDude; -:global BackupUserList; -:global SendTelegram; -:global SendNotification; -### End Configuration - - :if ([ :len [ /system/scheduler/find where name="BackupEveryDaySFTP" ] ] = 0) do={ - $LogPrint warning $ScriptName ("SystemScheduler NOT SET!"); - /system/scheduler/add name=$ScriptName on-event="/system/script { run $ScriptName; }" comment="Backup Every Day on SFTP" interval=1w policy=ftp,reboot,read,write,policy,test,password,sniff,sensitive,romon; start-time=startup; - } - - :if ([ $checkRunJob $ScriptName ] = false && $BackupRandomDelay > 0) do={ - $RandomDelay $BackupRandomDelay; - } - -:local dateTimeStampFix [$MiniDateTimeStamp]; -:local bkpDirName ($Identity . "-" . $dateTimeStampFix); -:local BkpPath; -:local FilePref ($Identity . "-"); - -:if ($BackupLocalRoot = "") do={ - $LogPrint warning $ScriptName ("\n\$BackupLocalRoot is not set! I set \$BackupRmLocal to true!" . $Err); - :set $BackupRmLocal true; -:do { -$MkDir ($bkpDirName); -:set BkpPath $bkpDirName; - } on-error={$LogPrint error $ScriptName ("ERROR Create Directory $BkpPath" . $Err);} -} else={ -:do { -$MkDir ($BackupLocalRoot . "/" . $bkpDirName); -:set BkpPath ($BackupLocalRoot . "/" . $bkpDirName); - } on-error={$LogPrint error $ScriptName ("ERROR Create Directories $BkpPath" . $Err);} -} -:local locFilePref ($BkpPath . "/" . $FilePref); - -$LogPrint warning $ScriptName ("\$locFilePref = $locFilePref" . $Err); - -:local remFilePref ($FilePref . $dateTimeStampFix . "-"); -#($bkpDirName . "/" . $BkpFilePrefix); -#:if ($SFTPpath != "") do={ -# :set remFilePref ($SFTPpath . "/" . $remFilePref); -#} else={:set remFilePref ("/" . $remFilePref)} - -$LogPrint warning $ScriptName ("\$remFilePref = $remFilePref"); - -### Process local filename to create remote filename -### Return array for file array -## Strips path separator '/' from local file name and replaces with '_' -# 'lfile' (string) the local filename -# 'lpref' (string) the local prefix to strip from start of lfile (if present) -# 'rpref' (string) remote prefix to prepend to remote filename -# 'clear' (bool) whether to delete local file after uploading -:local dofnames do={ - :local rfile ""; - :local rfilef ""; - # Strip Local Prefix if present - if ([:find $lfile $lpref -1] = 0) do={ - :set rfile [:pick $lfile [:len $lpref] [:len $lfile]]; - } else={ - :set rfile $lfile; - } - $LogPrint warning $ScriptName ("\n\$rfile = $rfile"); - # Convert / to _ - :for i from=0 to=([:len $rfile] - 1) do={ - :local char [:pick $rfile $i]; - :if ($char = "/") do={ - :set $char "_"; - } - :set rfilef ($rfilef . $char); - $LogPrint warning $ScriptName ("\n\$rfilef = " . $rfilef); - $LogPrint warning $ScriptName ("\n\$char = " . $char); - } - # Prepend Remote Prefix - :set rfile ($rpref . $rfilef); - $LogPrint warning $ScriptName ("\n\$rfile = " . $rfile); - $LogPrint warning $ScriptName ("\n\$rpref = " . $rpref); - $LogPrint warning $ScriptName ("\n\$rfilef = " . $rfilef); - $LogPrint warning $ScriptName ("\n\$lfile = " . $lfile); - $LogPrint warning $ScriptName ("\n\$clear = " . $clear); - # Return array - :return {lfile=$lfile; rfile=$rfile; clear=$clear}; -} - -### Delete Local File(s) -# $lfile (string) local file to be deleted -:local dodelete do={ - :global BackupRmLocal; - :global RmFile; - :global LogPrint; - if ($BackupRmLocal = true) do={ - :do { - $RmFile $lfile; - } on-error={ - $LogPrint error $ScriptName ("Error remove file " . $lfile . "." . $Err); - } -# if ([:len [/file find where name="$lfile"]] > 0) do={ -# /file remove [find where name="$lfile"]; - } -} - -:local filesa [:toarray ""]; -:local cfilename ""; -:local lfilename ""; -:local rfilename ""; - -### Binary Backup -if ($BackupSendBinary = true) do={ - :set cfilename ($locFilePref . "backup"); - :set lfilename ($cfilename . ".backup"); - if ($BackupEncrypt = false) do={ - :do { - /system backup save dont-encrypt=yes name=$cfilename; - } on-error={$LogPrint error $ScriptName ("Binary backup save " .$cfilename . " dont-encrypted failed" . $Err);} - } else={ - :do { - /system backup save encryption=aes-sha256 name=$cfilename password=$BackupPassword; - } on-error={$LogPrint error $ScriptName ("Binary backup save " .$cfilename . " encrypted failed" . $Err);} - } - :set ($filesa->([:len $filesa])) [$dofnames lfile=$lfilename lpref=$locFilePref rpref=$remFilePref clear=$BackupRmLocal]; -} - -### Generic Export -if ($BackupSendGlobalConfig = true) do={ - :set cfilename ($locFilePref . "export"); - :set lfilename ($cfilename . ".rsc"); - if (($OSVersion = "6" and $BackupSens = true) or ($OSVersion = "7" and $BackupSens = false)) do={ - :do { - /export compact file=$cfilename; - } on-error={$LogPrint error $ScriptName ("Export Global compact " .$cfilename . " failed" . $Err);} - } else={ - if ($OSVersion = "6") do={ - $LogPrint info $ScriptName ("Start Global compact if OSVersion = 6 (hide-sensitive)"); - :do { - /export compact hide-sensitive file=$cfilename; - } on-error={$LogPrint error $ScriptName ("Export Global compact if OSVersion = 6 (hide-sensitive) " .$cfilename . " failed" . $Err);} - } else={ - $LogPrint info $ScriptName ("Start Global compact"); - :do { - if ($BackupSens = true) do={ - /export compact hide-sensitive file=$cfilename; - } else={ - /export compact show-sensitive file=$cfilename; - } - } on-error={$LogPrint error $ScriptName ("Export Global compact (\$BackupSens = " . $BackupSens . ") " .$cfilename . " failed" . $Err);} - } - } - :set ($filesa->([:len $filesa])) [$dofnames lfile=$lfilename lpref=$locFilePref rpref=$remFilePref clear=$BackupRmLocal]; -} - -### User Export -if ($BackupUser = true) do={ - :set cfilename ($locFilePref . "user"); - :set lfilename ($cfilename . ".rsc"); - if (($OSVersion = "6" and $BackupSens = true) or ($OSVersion = "7" and $BackupSens = false)) do={ - :do { - /user export compact file=$cfilename; - } on-error={$LogPrint error $ScriptName ("User Export " . $cfilename . " failed" . $Err);} - } else={ - if ($OSVersion = "6") do={ - $LogPrint info $ScriptName ("User Export (hide-sensitive)" . $lfilename); - :do { - /user export compact hide-sensitive file=$cfilename; - } on-error={$LogPrint error $ScriptName ("User Export (hide-sensitive) " .$cfilename . " failed" . $Err);} - } else={ - $LogPrint info $ScriptName ("User Export (show-sensitive) " . $lfilename); - :do { - /user export compact show-sensitive file=$cfilename; - } on-error={$LogPrint error $ScriptName ("User Export (show-sensitive) " .$cfilename . " failed" . $Err);} - } - } - :set ($filesa->([:len $filesa])) [$dofnames lfile=$lfilename lpref=$locFilePref rpref=$remFilePref clear=$BackupRmLocal]; -} - -### License Export -if ($BackupLicense = true and $devBoardName != "CHR") do={ - :set lfilename ([/system license get software-id] . ".key"); - :set rfilename ($remFilePref . "license.key"); - :do { - /system license output; - } on-error={$LogPrint error $ScriptName ("Backup " . $lfilename . " failed" . $Err);} - :set ($filesa->([:len $filesa])) {lfile=$lfilename; rfile=$rfilename; clear=$BackupRmLocal}; -} - -### SSH Keys -if ($BackupSshKeys = true) do={ - :set cfilename ($locFilePref . "host-key"); - :do { - /ip ssh export-host-key key-file-prefix=$cfilename; - } on-error={$LogPrint error $ScriptName ("Backup SSH Keys " . $cfilename . " failed" . $Err);} - :foreach lfile in=[/file find where name~"^$cfilename"] do={ - :set ($filesa->([:len $filesa])) [$dofnames lfile=[/file get $lfile name] lpref=$locFilePref rpref=$remFilePref clear=$BackupRmLocal]; - } -} - -### Certificates -if ($BackupCerts = true) do={ - :foreach cert in=[/certificate find] do={ - :local certname [/certificate get $cert name]; - :local cfilename ($locFilePref . "cert-" . $certname); - :do { - /certificate export-certificate $cert file-name=$cfilename \ - type=pkcs12 export-passphrase=$BackupCertsPasswd; - } on-error={$LogPrint error $ScriptName ("Backup Certificate " . $cfilename . " failed" . $Err);} - :set ($filesa->([:len $filesa])) [$dofnames lfile=($cfilename . ".p12") lpref=$locFilePref rpref=$remFilePref clear=$BackupRmLocal]; - } -} - -# User-Manager -if ($BackupUserMan = true) do={ - :set cfilename ($locFilePref . "user-manager"); - :set lfilename ($cfilename . ".umb"); - :do { - $dodelete lfile=$lfilename; - } on-error={$LogPrint error $ScriptName ("User-Manager " . $lfilename . " clear failed" . $Err);} - if ($OSVersion = "6") do={ - :do { - /tool user-manager database save name=$cfilename; - } on-error={$LogPrint error $ScriptName ("Backup User-Manager " . $cfilename . " failed" . $Err);} - } - if ($OSVersion = "7") do={ - :do { - /user-manager database save name=$cfilename; - } on-error={$LogPrint error $ScriptName ("Backup User-Manager " . $cfilename . " failed" . $Err);} - } - :set ($filesa->([:len $filesa])) [$dofnames lfile=$lfilename lpref=$locFilePref rpref=$remFilePref clear=$BackupRmLocal]; -} - -# The Dude -if ($BackupDude = true) do={ - :set lfilename ($locFilePref . "the-dude.db"); - :do { - $dodelete lfile=$lfilename; - } on-error={$LogPrint error $ScriptName ("Dude " . $lfilename . " clear failed" . $Err);} - $dolog stage="create" msg=$logstage; - :do { - /dude export-db backup-file=$lfilename; - } on-error={$LogPrint error $ScriptName ("Backup Dude " . $lfilename . " failed" . $Err);} - :set ($filesa->([:len $filesa])) [$dofnames lfile=$lfilename lpref=$locFilePref rpref=$remFilePref clear=$BackupRmLocal]; -} - -# User File List -if ([:len $BackupUserList] > 0) do={ - :foreach lfile in=[:toarray $BackupUserList] do={ - :set ($filesa->([:len $filesa])) [$dofnames lfile=$lfile lpref=$locFilePref rpref=$remFilePref clear=false]; - } -} - -# Process Files Array -:local lfile ""; -:local rfile ""; -:local clear true; -/delay 10s; -:foreach a in=$filesa do={ - :set lfile ($a->"lfile"); - :set rfile ($a->"rfile"); - :set clear ($a->"clear"); - if ([:len [/file find where name="$lfile"]] > 0) do={ - $LogPrint info $ScriptName ("Upload " . $lfile . " AS " . $rfile); - :do { - /tool/fetch upload=yes url=($BackupUploadUrl . "/" . $rfile) \ - user=$BackupUploadUser password=$BackupUploadPass src-path=$lfile; -# /tool fetch address=$SFTPsrv user=$SFTPusr password=$SFTPpasswd src-path=$lfile dst-path=$rfile upload=yes mode=sftp; - } on-error={$LogPrint error $ScriptName ($rfile . $Err);} - if ($clear = true) do={ - $LogPrint info $ScriptName ("Delete " . $lfile) - :do { - :if ($BackupRmLocal = true) do={$RmFile $lfile} - } on-error={$LogPrint error $ScriptName ($lfile . $Err);} - } - } -} -:if (!$ExitError && $BackupRmLocal = true) do={$RmDir $BkpPath} -} do={:global ExitError; $ExitError $ExitOK [ :jobname ] $Err} +#!rsc by Vados +# RouterOS script: AM-SFTP-backup +# Script comment: BackUp via /tool fetch SFTP +# Copyright (c) 2005-2026 Vados +# +# +# requires RouterOS, version=7.21 +# requires device-mode, fetch, scheduler +# +# Based on: +# https://forum.mikrotik.com/viewtopic.php?t=159432 +# https://forum.mikrotik.com/viewtopic.php?p=858564#p858564 +# +### Info Log Action +# $stage (string) selects action text +# $msg (string) additional message, usually the backup stage or filename +# $error (bool) (optional) creates error log instead of info if 'true' +:local ExitOK false; +:onerror Err { + :global GlobalConfReady; :global GlobalFuncReady; + :retry { :if ($GlobalConfReady != true || $GlobalFuncReady != true) \ + do={ :error ("Global configs or functions not ready."); }; } delay=500ms max=50; + :local ScriptName [ :jobname ]; + +:global Identity; +:global checkRunJob; +:global LogPrint; +:global RandomDelay; +:global MkDir; +:global RmDir; +:global RmFile; +:global FileGet; +:global WaitForFile; +:global MiniDateTimeStamp; +:global OSVersion; +:global devBoardName; +:global BackupRandomDelay; +:global BackupUploadUrl; +:global BackupUploadUser; +:global BackupUploadPass; +:global BackupRmLocal; +:global BackupSendBinary; +:global BackupLocalRoot; +:global BackupEncrypt; +:global BackupPassword; +:global BackupSens; +:global BackupSendGlobalConfig; +:global BackupUser; +:global BackupLicense; +:global BackupSshKeys; +:global BackupCerts; +:global BackupCertsPasswd; +:global BackupUserMan; +:global BackupDude; +:global BackupUserList; +:global SendTelegram; +:global SendNotification; +### End Configuration + + :if ([ :len [ /system/scheduler/find where name="BackupEveryDaySFTP" ] ] = 0) do={ + $LogPrint warning $ScriptName ("SystemScheduler NOT SET!"); + /system/scheduler/add name=$ScriptName on-event="/system/script { run $ScriptName; }" comment="Backup Every Day on SFTP" interval=1w policy=ftp,reboot,read,write,policy,test,password,sniff,sensitive,romon; start-time=startup; + } + + :if ([ $checkRunJob $ScriptName ] = false && $BackupRandomDelay > 0) do={ + $RandomDelay $BackupRandomDelay; + } + +:local dateTimeStampFix [$MiniDateTimeStamp]; +:local bkpDirName ($Identity . "-" . $dateTimeStampFix); +:local BkpPath; +:local FilePref ($Identity . "-"); + +:if ($BackupLocalRoot = "") do={ + $LogPrint warning $ScriptName ("\n\$BackupLocalRoot is not set! I set \$BackupRmLocal to true!" . $Err); + :set $BackupRmLocal true; +:do { +$MkDir ($bkpDirName); +:set BkpPath $bkpDirName; + } on-error={$LogPrint error $ScriptName ("ERROR Create Directory $BkpPath" . $Err);} +} else={ +:do { +$MkDir ($BackupLocalRoot . "/" . $bkpDirName); +:set BkpPath ($BackupLocalRoot . "/" . $bkpDirName); + } on-error={$LogPrint error $ScriptName ("ERROR Create Directories $BkpPath" . $Err);} +} +:local locFilePref ($BkpPath . "/" . $FilePref); + +$LogPrint warning $ScriptName ("\$locFilePref = $locFilePref" . $Err); + +:local remFilePref ($FilePref . $dateTimeStampFix . "-"); +#($bkpDirName . "/" . $BkpFilePrefix); +#:if ($SFTPpath != "") do={ +# :set remFilePref ($SFTPpath . "/" . $remFilePref); +#} else={:set remFilePref ("/" . $remFilePref)} + +$LogPrint warning $ScriptName ("\$remFilePref = $remFilePref"); + +### Process local filename to create remote filename +### Return array for file array +## Strips path separator '/' from local file name and replaces with '_' +# 'lfile' (string) the local filename +# 'lpref' (string) the local prefix to strip from start of lfile (if present) +# 'rpref' (string) remote prefix to prepend to remote filename +# 'clear' (bool) whether to delete local file after uploading +:local dofnames do={ + :local rfile ""; + :local rfilef ""; + # Strip Local Prefix if present + if ([:find $lfile $lpref -1] = 0) do={ + :set rfile [:pick $lfile [:len $lpref] [:len $lfile]]; + } else={ + :set rfile $lfile; + } + $LogPrint warning $ScriptName ("\n\$rfile = $rfile"); + # Convert / to _ + :for i from=0 to=([:len $rfile] - 1) do={ + :local char [:pick $rfile $i]; + :if ($char = "/") do={ + :set $char "_"; + } + :set rfilef ($rfilef . $char); + $LogPrint warning $ScriptName ("\n\$rfilef = " . $rfilef); + $LogPrint warning $ScriptName ("\n\$char = " . $char); + } + # Prepend Remote Prefix + :set rfile ($rpref . $rfilef); + $LogPrint warning $ScriptName ("\n\$rfile = " . $rfile); + $LogPrint warning $ScriptName ("\n\$rpref = " . $rpref); + $LogPrint warning $ScriptName ("\n\$rfilef = " . $rfilef); + $LogPrint warning $ScriptName ("\n\$lfile = " . $lfile); + $LogPrint warning $ScriptName ("\n\$clear = " . $clear); + # Return array + :return {lfile=$lfile; rfile=$rfile; clear=$clear}; +} + +### Delete Local File(s) +# $lfile (string) local file to be deleted +:local dodelete do={ + :global BackupRmLocal; + :global RmFile; + :global LogPrint; + if ($BackupRmLocal = true) do={ + :do { + $RmFile $lfile; + } on-error={ + $LogPrint error $ScriptName ("Error remove file " . $lfile . "." . $Err); + } +# if ([:len [/file find where name="$lfile"]] > 0) do={ +# /file remove [find where name="$lfile"]; + } +} + +:local filesa [:toarray ""]; +:local cfilename ""; +:local lfilename ""; +:local rfilename ""; + +### Binary Backup +if ($BackupSendBinary = true) do={ + :set cfilename ($locFilePref . "backup"); + :set lfilename ($cfilename . ".backup"); + if ($BackupEncrypt = false) do={ + :do { + /system backup save dont-encrypt=yes name=$cfilename; + } on-error={$LogPrint error $ScriptName ("Binary backup save " .$cfilename . " dont-encrypted failed" . $Err);} + } else={ + :do { + /system backup save encryption=aes-sha256 name=$cfilename password=$BackupPassword; + } on-error={$LogPrint error $ScriptName ("Binary backup save " .$cfilename . " encrypted failed" . $Err);} + } + :set ($filesa->([:len $filesa])) [$dofnames lfile=$lfilename lpref=$locFilePref rpref=$remFilePref clear=$BackupRmLocal]; +} + +### Generic Export +if ($BackupSendGlobalConfig = true) do={ + :set cfilename ($locFilePref . "export"); + :set lfilename ($cfilename . ".rsc"); + if (($OSVersion = "6" and $BackupSens = true) or ($OSVersion = "7" and $BackupSens = false)) do={ + :do { + /export compact file=$cfilename; + } on-error={$LogPrint error $ScriptName ("Export Global compact " .$cfilename . " failed" . $Err);} + } else={ + if ($OSVersion = "6") do={ + $LogPrint info $ScriptName ("Start Global compact if OSVersion = 6 (hide-sensitive)"); + :do { + /export compact hide-sensitive file=$cfilename; + } on-error={$LogPrint error $ScriptName ("Export Global compact if OSVersion = 6 (hide-sensitive) " .$cfilename . " failed" . $Err);} + } else={ + $LogPrint info $ScriptName ("Start Global compact"); + :do { + if ($BackupSens = true) do={ + /export compact hide-sensitive file=$cfilename; + } else={ + /export compact show-sensitive file=$cfilename; + } + } on-error={$LogPrint error $ScriptName ("Export Global compact (\$BackupSens = " . $BackupSens . ") " .$cfilename . " failed" . $Err);} + } + } + :set ($filesa->([:len $filesa])) [$dofnames lfile=$lfilename lpref=$locFilePref rpref=$remFilePref clear=$BackupRmLocal]; +} + +### User Export +if ($BackupUser = true) do={ + :set cfilename ($locFilePref . "user"); + :set lfilename ($cfilename . ".rsc"); + if (($OSVersion = "6" and $BackupSens = true) or ($OSVersion = "7" and $BackupSens = false)) do={ + :do { + /user export compact file=$cfilename; + } on-error={$LogPrint error $ScriptName ("User Export " . $cfilename . " failed" . $Err);} + } else={ + if ($OSVersion = "6") do={ + $LogPrint info $ScriptName ("User Export (hide-sensitive)" . $lfilename); + :do { + /user export compact hide-sensitive file=$cfilename; + } on-error={$LogPrint error $ScriptName ("User Export (hide-sensitive) " .$cfilename . " failed" . $Err);} + } else={ + $LogPrint info $ScriptName ("User Export (show-sensitive) " . $lfilename); + :do { + /user export compact show-sensitive file=$cfilename; + } on-error={$LogPrint error $ScriptName ("User Export (show-sensitive) " .$cfilename . " failed" . $Err);} + } + } + :set ($filesa->([:len $filesa])) [$dofnames lfile=$lfilename lpref=$locFilePref rpref=$remFilePref clear=$BackupRmLocal]; +} + +### License Export +if ($BackupLicense = true and $devBoardName != "CHR") do={ + :set lfilename ([/system license get software-id] . ".key"); + :set rfilename ($remFilePref . "license.key"); + :do { + /system license output; + } on-error={$LogPrint error $ScriptName ("Backup " . $lfilename . " failed" . $Err);} + :set ($filesa->([:len $filesa])) {lfile=$lfilename; rfile=$rfilename; clear=$BackupRmLocal}; +} + +### SSH Keys +if ($BackupSshKeys = true) do={ + :set cfilename ($locFilePref . "host-key"); + :do { + /ip ssh export-host-key key-file-prefix=$cfilename; + } on-error={$LogPrint error $ScriptName ("Backup SSH Keys " . $cfilename . " failed" . $Err);} + :foreach lfile in=[/file find where name~"^$cfilename"] do={ + :set ($filesa->([:len $filesa])) [$dofnames lfile=[/file get $lfile name] lpref=$locFilePref rpref=$remFilePref clear=$BackupRmLocal]; + } +} + +### Certificates +if ($BackupCerts = true) do={ + :foreach cert in=[/certificate find] do={ + :local certname [/certificate get $cert name]; + :local cfilename ($locFilePref . "cert-" . $certname); + :do { + /certificate export-certificate $cert file-name=$cfilename \ + type=pkcs12 export-passphrase=$BackupCertsPasswd; + } on-error={$LogPrint error $ScriptName ("Backup Certificate " . $cfilename . " failed" . $Err);} + :set ($filesa->([:len $filesa])) [$dofnames lfile=($cfilename . ".p12") lpref=$locFilePref rpref=$remFilePref clear=$BackupRmLocal]; + } +} + +# User-Manager +if ($BackupUserMan = true) do={ + :set cfilename ($locFilePref . "user-manager"); + :set lfilename ($cfilename . ".umb"); + :do { + $dodelete lfile=$lfilename; + } on-error={$LogPrint error $ScriptName ("User-Manager " . $lfilename . " clear failed" . $Err);} + if ($OSVersion = "6") do={ + :do { + /tool user-manager database save name=$cfilename; + } on-error={$LogPrint error $ScriptName ("Backup User-Manager " . $cfilename . " failed" . $Err);} + } + if ($OSVersion = "7") do={ + :do { + /user-manager database save name=$cfilename; + } on-error={$LogPrint error $ScriptName ("Backup User-Manager " . $cfilename . " failed" . $Err);} + } + :set ($filesa->([:len $filesa])) [$dofnames lfile=$lfilename lpref=$locFilePref rpref=$remFilePref clear=$BackupRmLocal]; +} + +# The Dude +if ($BackupDude = true) do={ + :set lfilename ($locFilePref . "the-dude.db"); + :do { + $dodelete lfile=$lfilename; + } on-error={$LogPrint error $ScriptName ("Dude " . $lfilename . " clear failed" . $Err);} + $dolog stage="create" msg=$logstage; + :do { + /dude export-db backup-file=$lfilename; + } on-error={$LogPrint error $ScriptName ("Backup Dude " . $lfilename . " failed" . $Err);} + :set ($filesa->([:len $filesa])) [$dofnames lfile=$lfilename lpref=$locFilePref rpref=$remFilePref clear=$BackupRmLocal]; +} + +# User File List +if ([:len $BackupUserList] > 0) do={ + :foreach lfile in=[:toarray $BackupUserList] do={ + :set ($filesa->([:len $filesa])) [$dofnames lfile=$lfile lpref=$locFilePref rpref=$remFilePref clear=false]; + } +} + +# Process Files Array +:local lfile ""; +:local rfile ""; +:local clear true; +/delay 10s; +:foreach a in=$filesa do={ + :set lfile ($a->"lfile"); + :set rfile ($a->"rfile"); + :set clear ($a->"clear"); + if ([:len [/file find where name="$lfile"]] > 0) do={ + $LogPrint info $ScriptName ("Upload " . $lfile . " AS " . $rfile); + :do { + /tool/fetch upload=yes url=($BackupUploadUrl . "/" . $rfile) \ + user=$BackupUploadUser password=$BackupUploadPass src-path=$lfile; +# /tool fetch address=$SFTPsrv user=$SFTPusr password=$SFTPpasswd src-path=$lfile dst-path=$rfile upload=yes mode=sftp; + } on-error={$LogPrint error $ScriptName ($rfile . $Err);} + if ($clear = true) do={ + $LogPrint info $ScriptName ("Delete " . $lfile) + :do { + :if ($BackupRmLocal = true) do={$RmFile $lfile} + } on-error={$LogPrint error $ScriptName ($lfile . $Err);} + } + } +} +:if (!$ExitError && $BackupRmLocal = true) do={$RmDir $BkpPath} +} do={:global ExitError; $ExitError $ExitOK [ :jobname ] $Err} diff --git a/html/AM-setup.rsc b/html/AM-setup.rsc index d72322aba..db58eda74 100644 --- a/html/AM-setup.rsc +++ b/html/AM-setup.rsc @@ -20,7 +20,7 @@ # For run from terminal: #/system/script/add name=AM-setup owner=AM-Setup source=([/tool/fetch check-certificate=yes-without-crl "https://ros.vados.ru/AM-setup.rsc" output=user as-value ]->"data"); # -:global ScriptUpdatesCRLF true; +:global ScriptUpdatesCRLF "LF"; if ($ScriptUpdatesCRLF != false) do={ :foreach Script in=[ /system/script/find where source~"^#!rsc by Vados" ] do={ if ($ScriptUpdatesCRLF = true) do={ diff --git a/html/README.md b/html/README.md index 5e6c9364d..4e7976203 100644 --- a/html/README.md +++ b/html/README.md @@ -73,17 +73,14 @@ Huge configuration and lots of scripts give an extra risk. **Take care!** :global GlobalScriptsUrl "https://ros.vados.ru/"; :global RemoveMe false; - - /system/script/add name="AM-setup" owner="A -M-setup" source=([/tool/fetch check-certificate=yes-without-crl url=($GlobalScriptsUrl . "AM-setup.rsc") output=user as-value]->"data"); + + /system/script/add name="AM-setup" owner="AM-setup" source=([/tool/fetch check-certificate=yes-without-crl url=($GlobalScriptsUrl . "AM-setup.rsc") output=user as-value]->"data"); /system/script/run AM-setup; * One string: - :global GlobalScriptsUrl "https://ros.vados.ru/"; :global RemoveMe false; /system/script/add name="AM-setup" owner="A -M-setup" source=([/tool/fetch check-certificate=yes-without-crl url=($GlobalScriptsUrl . "AM-setup.rsc") output=user as-value]->"data"); - /system/script/run AM-setup; + :global GlobalScriptsUrl "https://ros.vados.ru/"; :global RemoveMe false; /system/script/add name="AM-setup" owner="AM-setup" source=([/tool/fetch check-certificate=yes-without-crl url=($GlobalScriptsUrl . "AM-setup.rsc") output=user as-value]->"data"); /system/script/run AM-setup; > 💡️ **Hint**: To prevent the installation script from being deleted 30 seconds after its completion, you can use a global variable [`:global RemoveMe false;`]. Don't worry, the script will delete all its global variables. Shall we check? ;) diff --git a/html/global-functions.rsc b/html/global-functions.rsc new file mode 100644 index 000000000..cb74f991e --- /dev/null +++ b/html/global-functions.rsc @@ -0,0 +1,1958 @@ +#!rsc by RouterOS +# RouterOS script: global-functions +# Copyright (c) 2013-2026 Christian Hesse +# Michael Gisbers +# https://rsc.eworm.de/COPYING.md +# +# requires RouterOS, version=7.19 +# requires device-mode, fetch, scheduler +# +# global functions +# https://rsc.eworm.de/ + +:local ScriptName [ :jobname ]; + +# Git commit id & info, expected configuration version +:global CommitId "unknown"; +:global CommitInfo "unknown"; +:global ExpectedConfigVersion 139; + +# global variables not to be changed by user +:global GlobalFunctionsReady false; +:global Identity [ /system/identity/get name ]; + +# global functions +:global AlignRight; +:global CertificateAvailable; +:global CertificateDownload; +:global CertificateNameByCN; +:global CharacterMultiply; +:global CharacterReplace; +:global CleanFilePath; +:global CleanName; +:global CommitBrief; +:global DeviceInfo; +:global Dos2Unix; +:global DownloadPackage; +:global EitherOr; +:global EscapeForRegEx; +:global ExitError; +:global ExitOnError; +:global FetchHuge; +:global FetchUserAgentStr; +:global FileExists; +:global FileGet; +:global FormatLine; +:global FormatMultiLines; +:global GetMacVendor; +:global GetRandom20CharAlNum; +:global GetRandom20CharHex; +:global GetRandomNumber; +:global Grep; +:global HexToNum; +:global HumanReadableNum; +:global IfThenElse; +:global IsDefaultRouteReachable; +:global IsDNSResolving; +:global IsFullyConnected; +:global IsMacLocallyAdministered; +:global IsTimeSync; +:global LogPrint; +:global LogPrintOnce; +:global LogPrintVerbose; +:global MAX; +:global MIN; +:global MkDir; +:global NetMask4; +:global NetMask6; +:global NotificationFunctions; +:global ParseDate; +:global ParseKeyValueStore; +:global PrettyPrint; +:global ProtocolStrip; +:global RandomDelay; +:global RequiredRouterOS; +:global RmDir; +:global RmFile; +:global ScriptFromTerminal; +:global ScriptInstallUpdate; +:global ScriptLock; +:global SendNotification; +:global SendNotification2; +:global SymbolByUnicodeName; +:global SymbolForNotification; +:global Unix2Dos; +:global UrlEncode; +:global ValidateSyntax; +:global VersionToNum; +:global WaitDefaultRouteReachable; +:global WaitDNSResolving; +:global WaitForFile; +:global WaitFullyConnected; +:global WaitTimeSync; + +# align string to the right +:set AlignRight do={ + :local Input [ :tostr $1 ]; + :local Len [ :tonum $2 ]; + + :global CharacterMultiply; + :global EitherOr; + + :set Len [ $EitherOr $Len 8 ]; + :local Spaces [ $CharacterMultiply " " $Len ]; + + :return ([ :pick $Spaces 0 ($Len - [ :len $Input ]) ] . $Input); +} + +# check and download required certificate +:set CertificateAvailable do={ + :local CommonName [ :tostr $1 ]; + :local UseFor [ :tostr $2 ]; + + :global CertificateDownload; + :global EitherOr; + :global LogPrint; + :global ParseKeyValueStore; + + :set UseFor [ $EitherOr $UseFor "undefined" ]; + + :if ([ /system/resource/get free-hdd-space ] < 8388608 && \ + [ /certificate/settings/get crl-download ] = true && \ + [ /certificate/settings/get crl-store ] = "system") do={ + $LogPrint warning $0 ("This system has low free flash space but " . \ + "is configured to download certificate CRLs to system!"); + } + + :if ([ :len $CommonName ] = 0) do={ + $LogPrint warning $0 ("No CommonName given!"); + :return false; + } + + :local CertSettings [ /certificate/settings/get ]; + :if ((($CertSettings->"builtin-trust-anchors") = "trusted" || \ + ($CertSettings->"builtin-trust-store") ~ $UseFor || \ + ($CertSettings->"builtin-trust-store") = "all") && \ + [ :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."); + :if ([ $CertificateDownload $CommonName ] = false) do={ + :return false; + } + } + + :if ([ :len [ /certificate/find where common-name=$CommonName ] ] > 1) do={ + $LogPrint info $0 ("There are " . $CertCount . " Certificates with CommonName '" . $CommonName . "'. Should be ok."); + :return true; + } + + :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={ + :local IssuerCN ([ $ParseKeyValueStore ($CertVal->"issuer") ]->"CN"); + $LogPrint info $0 ("Certificate chain for '" . $CommonName . "' is incomplete, missing '" . $IssuerCN . "'."); + :if ([ $CertificateDownload $IssuerCN ] = false) do={ + :return false; + } + } + :set CertVal [ /certificate/get [ find where skid=($CertVal->"akid") ] ]; + } + :return true; +} + +# download and import certificate +:set CertificateDownload do={ + :local CommonName [ :tostr $1 ]; + + :global ScriptUpdatesBaseUrl; + :global ScriptUpdatesUrlSuffix; + + :global CertificateNameByCN; + :global CleanName; + :global FetchUserAgentStr; + :global LogPrint; + :global RmFile; + :global WaitForFile; + + $LogPrint info $0 ("Downloading and importing certificate with " . \ + "CommonName '" . $CommonName . "'."); + :local FileName ([ $CleanName $CommonName ] . ".pem"); + :do { + /tool/fetch check-certificate=yes-without-crl http-header-field=({ [ $FetchUserAgentStr $0 ] }) \ + ($ScriptUpdatesBaseUrl . "certs/" . $FileName . $ScriptUpdatesUrlSuffix) \ + dst-path=$FileName as-value; + $WaitForFile $FileName; + } on-error={ + $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 Match [ :tostr $1 ]; + + :global CleanName; + :global LogPrint; + + :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) +:set CharacterMultiply do={ + :local Str [ :tostr $1 ]; + :local Num [ :tonum $2 ]; + + :if ($Num = 0) do={ + :return ""; + } + + :local Return ""; + :for I from=1 to=$Num do={ + :set Return ($Return . $Str); + } + :return $Return; +} + +# character replace +:set CharacterReplace do={ + :local String [ :tostr $1 ]; + :local ReplaceFrom [ :tostr $2 ]; + :local ReplaceWith [ :tostr $3 ]; + :local Return ""; + + :if ($ReplaceFrom = "") do={ + :return $String; + } + + :while ([ :typeof [ :find $String $ReplaceFrom ] ] != "nil") do={ + :local Pos [ :find $String $ReplaceFrom ]; + :set Return ($Return . [ :pick $String 0 $Pos ] . $ReplaceWith); + :set String [ :pick $String ($Pos + [ :len $ReplaceFrom ]) [ :len $String ] ]; + } + + :return ($Return . $String); +} + +# clean file path +:set CleanFilePath do={ + :local Path [ :tostr $1 ]; + + :global CharacterReplace; + + :while ($Path ~ "//") do={ + :set $Path [ $CharacterReplace $Path "//" "/" ]; + } + :if ([ :pick $Path 0 ] = "/") do={ + :set Path [ :pick $Path 1 [ :len $Path ] ]; + } + :if ([ :pick $Path ([ :len $Path ] - 1) ] = "/") do={ + :set Path [ :pick $Path 0 ([ :len $Path ] - 1) ]; + } + + :return $Path; +} + +# clean name for DNS, file and more +:set CleanName do={ + :local Input [ :tostr $1 ]; + + :local Return ""; + + :for I from=0 to=([ :len $Input ] - 1) do={ + :local Char [ :pick $Input $I ]; + :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; +} + +# return a brief commit description +:set CommitBrief do={ + :global CommitId; + :global CommitInfo; + + :if ($CommitId = "unknown") do={ + :return "unknown"; + } + + :return ($CommitInfo . "/" . [ :pick $CommitId 0 8 ]); +} + +# get readable device info +:set DeviceInfo do={ + :global ExpectedConfigVersion; + :global Identity; + + :global CommitBrief; + :global IfThenElse; + :global FormatLine; + + :local License [ /system/license/get ]; + :local Resource [ /system/resource/get ]; + :local RouterBoard; + :do { + :set RouterBoard [[ :parse "/system/routerboard/get" ]]; + } on-error={ } + :local Snmp [ /snmp/get ]; + :local Update [ /system/package/update/get ]; + + :return ( \ + [ $FormatLine "Hostname" $Identity ] . "\n" . \ + [ $IfThenElse ([ :len ($Snmp->"location") ] > 0) \ + ([ $FormatLine "Location" ($Snmp->"location") ] . "\n") ] . \ + [ $IfThenElse ([ :len ($Snmp->"contact") ] > 0) \ + ([ $FormatLine "Contact" ($Snmp->"contact") ] . "\n") ] . \ + "Hardware:\n" . \ + [ $FormatLine " Board" ($Resource->"board-name") ] . "\n" . \ + [ $FormatLine " Arch" ($Resource->"architecture-name") ] . "\n" . \ + [ $IfThenElse ($RouterBoard->"routerboard" = true) \ + ([ $FormatLine " Model" ($RouterBoard->"model") ] . \ + [ $IfThenElse ([ :len ($RouterBoard->"revision") ] > 0) \ + (" " . $RouterBoard->"revision") ] . "\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" && \ + $Update->"installed-version" != $Update->"latest-version") \ + ([ $FormatLine " Available" ($Update->"latest-version") ] . "\n") ] . \ + [ $IfThenElse ($RouterBoard->"routerboard" = true && \ + $RouterBoard->"current-firmware" != $RouterBoard->"upgrade-firmware") \ + ([ $FormatLine " Firmware" ($RouterBoard->"current-firmware") ] . "\n") ] . \ + "RouterOS-Scripts:\n" . \ + [ $FormatLine " Commit" [ $CommitBrief ] ] . "\n" . \ + [ $FormatLine " Version" $ExpectedConfigVersion ]); +} + +# convert line endings, DOS -> UNIX +:set Dos2Unix do={ + :return [ :tolf [ :tostr $1 ] ]; +} + +# download package from upgrade server +:set DownloadPackage do={ + :local PkgName [ :tostr $1 ]; + :local PkgVer [ :tostr $2 ]; + :local PkgArch [ :tostr $3 ]; + :local PkgDir [ :tostr $4 ]; + + :global CertificateAvailable; + :global CleanFilePath; + :global FileExists; + :global LogPrint; + :global MkDir; + :global RmFile; + :global WaitForFile; + + :if ([ :len $PkgName ] = 0) do={ :return false; } + :if ([ :len $PkgVer ] = 0) do={ :set PkgVer [ /system/package/update/get installed-version ]; } + :if ([ :len $PkgArch ] = 0) do={ :set PkgArch [ /system/resource/get architecture-name ]; } + + :if ($PkgName = "system") do={ :set PkgName "routeros"; } + + :local PkgFile ($PkgName . "-" . $PkgVer . "-" . $PkgArch . ".npk"); + :if ($PkgArch = "x86_64") do={ :set PkgFile ($PkgName . "-" . $PkgVer . ".npk"); } + :local PkgDest [ $CleanFilePath ($PkgDir . "/" . $PkgFile) ]; + + :if ([ $MkDir $PkgDir ] = false) do={ + $LogPrint warning $0 ("Failed creating directory, not downloading package."); + :return false; + } + + :if ([ $FileExists $PkgDest "package" ] = true) do={ + $LogPrint info $0 ("Package file " . $PkgName . " already exists."); + :return true; + } + + :if ([ $CertificateAvailable "ISRG Root X1" "fetch" ] = false) do={ + $LogPrint error $0 ("Downloading required certificate failed."); + :return false; + } + + :local Url ("https://upgrade.mikrotik.com/routeros/" . $PkgVer . "/" . $PkgFile); + $LogPrint info $0 ("Downloading package file '" . $PkgName . "'..."); + $LogPrint debug $0 ("... from url: " . $Url); + + :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; + } + + :if ([ $FileExists $PkgDest "package" ] = false) do={ + $LogPrint warning $0 ("Downloaded file is not a package, removing."); + $RmFile $PkgDest; + :return false; + } + + :return true; +} + +# return either first (if "true") or second +:set EitherOr do={ + :global IfThenElse; + + :if ([ :typeof $1 ] = "num") do={ + :return [ $IfThenElse ($1 != 0) $1 $2 ]; + } + :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 ]; +} + +# escape for regular expression +:set EscapeForRegEx do={ + :local Input [ :tostr $1 ]; + + :if ([ :len $Input ] = 0) do={ + :return ""; + } + + :local Return ""; + :local Chars ("^.[]\$()|*+?{}\\"); + + :for I from=0 to=([ :len $Input ] - 1) do={ + :local Char [ :pick $Input $I ]; + :if ([ :find $Chars $Char ]) do={ + :set Char ("\\" . $Char); + } + :set Return ($Return . $Char); + } + + :return $Return; +} + +# wrapper for $ExitOnError with additional parameter +:set ExitError do={ + :local ExitOK [ :tostr $1 ]; + :local Name [ :tostr $2 ]; + :local Error [ :tostr $3 ]; + + :global ExitOnError; + + :if ($ExitOK = "false") do={ + $ExitOnError $Name $Error; + } +} + +# simple macro to print error message on unintentional error +:set ExitOnError do={ + :local Name [ :tostr $1 ]; + :local Error [ :tostr $2 ]; + + :global IfThenElse; + :global LogPrint; + + $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; + } + + :if ([ $WaitForFile $FileName 5s ] = false) do={ + $LogPrint debug $0 ("The file downloaded from " . $Url . " did not show up."); + :return false; + } + + :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 FetchUserAgentStr do={ + :local Caller [ :tostr $1 ]; + + :global CommitBrief; + :global IfThenElse; + + :local Resource [ /system/resource/get ]; + + :return ("User-Agent: Mikrotik/" . $Resource->"version" . " " . $Resource->"architecture-name" . \ + " " . $Caller . "/Fetch (https://rsc.eworm.de/; " . [ $CommitBrief ] . ")"); +} + +# 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 ]; + :local Value [ :tostr $2 ]; + :local Indent [ :tonum $3 ]; + :local Spaces; + :local Return ""; + + :global CharacterMultiply; + :global EitherOr; + + :set Indent [ $EitherOr $Indent 16 ]; + :local Spaces [ $CharacterMultiply " " $Indent ]; + + :if ([ :len $Key ] > 0) do={ :set Return ($Key . ":"); } + :if ([ :len $Key ] > ($Indent - 2)) do={ + :set Return ($Return . "\n" . [ :pick $Spaces 0 $Indent ] . $Value); + } else={ + :set Return ($Return . [ :pick $Spaces 0 ($Indent - [ :len $Return ]) ] . $Value); + } + + :return $Return; +} + +# format multiple lines for output +:set FormatMultiLines do={ + :local Key [ :tostr $1 ]; + :local Values [ :toarray $2 ]; + :local Indent [ :tonum $3 ]; + :local Return; + + :global FormatLine; + + :set Return [ $FormatLine $Key ($Values->0) $Indent ]; + :foreach Value in=[ :pick $Values 1 [ :len $Values ] ] do={ + :set Return ($Return . "\n" . [ $FormatLine "" $Value $Indent ]); + } + + :return $Return; +} + +# get MAC vendor +:set GetMacVendor do={ + :local Mac [ :tostr $1 ]; + + :global CertificateAvailable; + :global IsMacLocallyAdministered; + :global LogPrint; + + :if ([ $IsMacLocallyAdministered $Mac ] = true) do={ + :return "locally administered"; + } + + :do { + :if ([ $CertificateAvailable "GTS Root R4" "fetch" ] = false) do={ + $LogPrint warning $0 ("Downloading required certificate failed."); + :error false; + } + :local Vendor ([ /tool/fetch check-certificate=yes-without-crl \ + ("https://api.macvendors.com/" . [ :pick $Mac 0 8 ]) output=user as-value ]->"data"); + :return $Vendor; + } on-error={ + :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."); + } do={ + $LogPrint warning $0 ("Failed getting mac vendor: " . $Err); + } + :return "unknown vendor"; + } +} + +# generate random 20 chars alphabetical (A-Z & a-z) and numerical (0-9) +:set GetRandom20CharAlNum do={ + :global EitherOr; + + :return [ :rndstr length=[ $EitherOr [ :tonum $1 ] 20 ] from="ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789" ]; +} + +# generate random 20 chars hex (0-9 and a-f) +:set GetRandom20CharHex do={ + :global EitherOr; + + :return [ :rndstr length=[ $EitherOr [ :tonum $1 ] 20 ] from="0123456789abcdef" ]; +} + +# generate random number +:set GetRandomNumber do={ + :global EitherOr; + + :return [ :rndnum from=0 to=[ $EitherOr [ :tonum $1 ] 4294967295 ] ]; +} + +# return first line that matches a pattern +:set Grep do={ + :local Input ([ :tostr $1 ] . "\n"); + :local Pattern [ :tostr $2 ]; + + :if ([ :typeof [ :find $Input $Pattern ] ] = "nil") do={ + :return []; + } + + :do { + # Do *NOT* try :deserialize here to split lines. It can fail with + # scripts, and it *does* fail with global-functions! + :local Len [ :find $Input "\n" ]; + :local Line [ :pick $Input 0 $Len ]; + :if ([ :typeof [ :find $Line $Pattern ] ] = "num") do={ + :return $Line; + } + :set Input [ :pick $Input ($Len + 1) [ :len $Input ] ]; + } while=([ :len $Input ] > 0); + + :return []; +} + +# convert from hex (string) to num +:set HexToNum do={ + :local Input [ :tostr $1 ]; + + :global HexToNum; + + :if ([ :pick $Input 0 ] = "*") do={ + :return [ $HexToNum [ :pick $Input 1 [ :len $Input ] ] ]; + } + + :return [ :tonum ("0x" . $Input) ]; +} + +# return human readable number +:set HumanReadableNum do={ + :local Input [ :tonum $1 ]; + :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; + } + + :for I from=0 to=[ :len $Prefix ] do={ + :set Pow ($Pow * $Base); + :if ($Input / $Base < $Pow) do={ + :set Prefix [ :pick $Prefix $I ]; + :local Tmp1 ($Input * 100 / $Pow); + :local Tmp2 ($Tmp1 / 100); + :if ($Tmp2 >= 100) do={ + :return ($Tmp2 . $Prefix . $Bin); + } + :return ($Tmp2 . "." . \ + [ :pick $Tmp1 [ :len $Tmp2 ] ([ :len $Tmp1 ] - [ :len $Tmp2 ] + 1) ] . \ + $Prefix . $Bin); + } + } +} + +# mimic conditional/ternary operator (condition ? consequent : alternative) +:set IfThenElse do={ + :if ([ :tostr $1 ] = "true" || [ :tobool $1 ] = true) do={ + :return $2; + } + :return $3; +} + +# check if default route is reachable +:set IsDefaultRouteReachable do={ + :if ([ :len [ /ip/route/find where dst-address=0.0.0.0/0 active routing-table=main ] ] > 0) do={ + :return true; + } + :return false; +} + +# check if DNS is resolving +:set IsDNSResolving do={ + :do { + :resolve "low-ttl.eworm.de"; + } on-error={ + :return false; + } + :return true; +} + +# check if system is is fully connected (default route reachable, DNS resolving, time sync) +:set IsFullyConnected do={ + :global IsDefaultRouteReachable; + :global IsDNSResolving; + :global IsTimeSync; + + :if ([ $IsDefaultRouteReachable ] = false) do={ + :return false; + } + :if ([ $IsDNSResolving ] = false) do={ + :return false; + } + :if ([ $IsTimeSync ] = false) do={ + :return false; + } + :return true; +} + +# check if mac address is locally administered +:set IsMacLocallyAdministered do={ + :if ([ :tonum ("0x" . [ :pick $1 0 [ :find $1 ":" ] ]) ] & 2 = 2) do={ + :return true; + } + :return false; +} + +# check if system time is sync +:set IsTimeSync do={ + :global IsTimeSyncCached; + :global IsTimeSyncResetNtp; + + :global LogPrintOnce; + + :if ($IsTimeSyncCached = true) do={ + :return true; + } + + :if ([ /system/ntp/client/get enabled ] = true) do={ + :if ([ /system/ntp/client/get status ] = "synchronized") do={ + :set IsTimeSyncCached true; + :return true; + } + + :local Uptime [ /system/resource/get uptime ]; + :if ([ :typeof $IsTimeSyncResetNtp ] = "nothing") do={ + :set IsTimeSyncResetNtp $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; + /system/ntp/client/set enabled=yes; + :return false; + } + + :if ([ /system/license/get ]->"level" = "free" || \ + [ /system/resource/get ]->"board-name" = "x86") do={ + $LogPrintOnce debug $0 ("No ntp client configured, relying on RTC for CHR free license and x86."); + :return true; + } + + :if ([ /ip/cloud/get update-time ] = true) do={ + :if ([ :typeof [ /ip/cloud/get public-address ] ] = "ip") do={ + :set IsTimeSyncCached true; + :return true; + } + :return false; + } + + $LogPrintOnce debug $0 ("No time source configured! Returning gracefully..."); + :return true; +} + +# log and print with same text +:set LogPrint do={ + :local Severity [ :tostr $1 ]; + :local Name [ :tostr $2 ]; + :local Message [ :tostr $3 ]; + + :global PrintDebug; + :global PrintDebugOverride; + + :global EitherOr; + + :local Debug [ $EitherOr ($PrintDebugOverride->$Name) $PrintDebug ]; + + :local PrintSeverity do={ + :global TerminalColorOutput; + + :if ($TerminalColorOutput != true) do={ + :return $1; + } + + :local Color { debug=96; info=97; warning=93; error=91 }; + :return ("\1B[" . $Color->$1 . "m" . $1 . "\1B[0m"); + } + + :local Log ([ $EitherOr $Name "" ] . ": " . $Message); + :if ($Severity ~ ("^(debug|error|info)\$")) do={ + :if ($Severity = "debug") do={ :log debug $Log; } + :if ($Severity = "error") do={ :log error $Log; } + :if ($Severity = "info" ) do={ :log info $Log; } + } else={ + :log warning $Log; + :set Severity "warning"; + } + + :if ($Severity != "debug" || $Debug = true) do={ + :put ([ $PrintSeverity $Severity ] . ": " . $Message); + } +} + +# log and print, once until reboot +:set LogPrintOnce do={ + :local Severity [ :tostr $1 ]; + :local Name [ :tostr $2 ]; + :local Message [ :tostr $3 ]; + + :global LogPrint; + + :global LogPrintOnceMessages; + + :if ([ :typeof $LogPrintOnceMessages ] = "nothing") do={ + :set LogPrintOnceMessages ({}); + } + + :if ($LogPrintOnceMessages->$Message = 1) do={ + :return false; + } + + :if ([ :len [ /log/find where message=($Name . ": " . $Message) ] ] > 0) do={ + $LogPrint warning $0 \ + ("The message is already in log, scripting subsystem may have crashed before!"); + } + + :set ($LogPrintOnceMessages->$Message) 1; + $LogPrint $Severity $Name $Message; + :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; } + :return $2; +} + +# get min value +:set MIN do={ + :if ($1 < $2) do={ :return $1; } + :return $2; +} + +# create directory +:set MkDir do={ + :local Path [ :tostr $1 ]; + + :global CleanFilePath; + :global FileGet; + :global LogPrint; + :global RmDir; + :global WaitForFile; + + :local MkTmpfs do={ + :global LogPrint; + :global WaitForFile; + + :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."); + $RmDir "tmpfs"; + :onerror Err { + /disk/add slot=tmpfs type=tmpfs tmpfs-max-size=([ /system/resource/get total-memory ] / 3); + $WaitForFile "tmpfs"; + } do={ + $LogPrint warning $0 ("Creating disk of type tmpfs failed: " . $Err); + :return false; + } + :return true; + } + + :set Path [ $CleanFilePath $Path ]; + + :if ($Path = "") do={ + :return true; + } + + $LogPrint debug $0 ("Making directory: " . $Path); + + :local PathVal [ $FileGet $Path ]; + :if ($PathVal->"type" = "directory") do={ + $LogPrint debug $0 ("... which already exists."); + :return true; + } + + :if ([ :pick $Path 0 5 ] = "tmpfs") do={ + :if ([ $MkTmpfs ] = false) do={ + :return false; + } + } + + :onerror Err { + /file/add type="directory" name=$Path; + $WaitForFile $Path; + } do={ + $LogPrint warning $0 ("Making directory '" . $Path . "' failed: " . $Err); + :return false; + } + + :return true; +} + +# return an IPv4 netmask for CIDR +:set NetMask4 do={ + :local CIDR [ :tonum $1 ]; + + :return ((255.255.255.255 << (32 - $CIDR)) & 255.255.255.255); +} + +# return an IPv6 netmask for CIDR +:set NetMask6 do={ + :local CIDR [ :tostr $1 ]; + + :global IfThenElse; + :global MAX; + :global MIN; + + :global NetMask6Cache; + + :if ([ :typeof ($NetMask6Cache->$CIDR) ] = "ip6") do={ + :return ($NetMask6Cache->$CIDR); + } + + :if ([ :typeof $NetMask6Cache ] = "nothing") do={ + :set NetMask6Cache ({}); + } + + :local Mask ""; + :for I from=0 to=7 do={ + :set Mask ($Mask . \ + [ :convert from=num to=hex (0xffff - (0xffff >> [ :tonum [ $MIN [ $MAX ($CIDR - (16 * $I)) 0 ] 16 ] ])) ] . \ + [ $IfThenElse ($I < 7) ":" ]); + } + :set Mask [ :toip6 $Mask ]; + :set ($NetMask6Cache->$CIDR) $Mask; + :return $Mask; +} + +# prepare NotificationFunctions array +:if ([ :typeof $NotificationFunctions ] != "array") do={ + :set NotificationFunctions ({}); +} + +# parse the date and return a named array +:set ParseDate do={ + :local Date [ :tostr $1 ]; + + :return ({ "year"=[ :tonum [ :pick $Date 0 4 ] ]; + "month"=[ :tonum [ :pick $Date 5 7 ] ]; + "day"=[ :tonum [ :pick $Date 8 10 ] ] }); +} + +# 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={ + :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; + } + } + :return $Result; +} + +# print lines with trailing carriage return +:set PrettyPrint do={ + :put [ :tocrlf [ :tostr $1 ] ]; +} + +# strip protocol from from url string +:set ProtocolStrip do={ + :local Input [ :tostr $1 ]; + + :local Pos [ :find $Input "://" ]; + :if ([ :typeof $Pos ] = "nil") do={ + :return $Input; + } + :return [ :pick $Input ($Pos + 3) [ :len $Input ] ]; +} + +# delay a random amount of seconds +:set RandomDelay do={ + :local Time [ :tonum $1 ]; + :local Unit [ :tostr $2 ]; + + :global EitherOr; + :global GetRandomNumber; + :global MAX; + + :if ($Time = 0) do={ + :return false; + } + + :delay ([ $MAX 10 [ $GetRandomNumber ([ :tonsec [ :totime ($Time . [ $EitherOr $Unit "s" ]) ] ] / 1000000) ] ] . "ms"); +} + +# check for required RouterOS version +:set RequiredRouterOS do={ + :local Caller [ :tostr $1 ]; + :local Required [ :tostr $2 ]; + :local Warn [ :tostr $3 ]; + + :global IfThenElse; + :global LogPrint; + :global VersionToNum; + + :if (!($Required ~ "^\\d+\\.\\d+((alpha|beta|rc|\\.)\\d+|)\$")) do={ + $LogPrint error $0 ("No valid RouterOS version: " . $Required); + :return false; + } + + :if ([ $VersionToNum $Required ] > [ $VersionToNum [ /system/package/update/get installed-version ] ]) do={ + :if ($Warn = "true") do={ + $LogPrint warning $0 ("This " . [ $IfThenElse ([ :pick $Caller 0 ] = ("\$")) "function" "script" ] . \ + " '" . $Caller . "' (at least specific functionality) requires RouterOS " . $Required . ". Please update!"); + } + :return false; + } + :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 ]; + :while ([ :typeof ($Job->"parent") ] = "id") do={ + :set Job [ /system/script/job/get [ find where .id=($Job->"parent") ] ]; + } + :if (($Job->"type") = "login") do={ + $LogPrint debug $0 ("Script " . $Script . " started from terminal."); + :return true; + } + } + + $LogPrint debug $0 ("Script " . $Script . " NOT started from terminal."); + :return false; +} + +# install new scripts, update existing scripts +:set ScriptInstallUpdate do={ :onerror Err { + :local Scripts [ :toarray $1 ]; + :local NewComment [ :tostr $2 ]; + + :global CommitId; + :global ExpectedConfigVersion; + :global GlobalConfigReady; + :global GlobalFunctionsReady; + :global Identity; + :global IDonate; + :global NoNewsAndChangesNotification; + :global ScriptUpdatesBaseUrl; + :global ScriptUpdatesCRLF; + :global ScriptUpdatesUrlSuffix; + + :global CertificateAvailable; + :global EitherOr; + :global FetchUserAgentStr; + :global Grep; + :global IfThenElse; + :global LogPrint; + :global LogPrintOnce; + :global ParseKeyValueStore; + :global RequiredRouterOS; + :global SendNotification2; + :global SymbolForNotification; + :global ValidateSyntax; + + :if ([ $CertificateAvailable "Root YE" "fetch" ] = false) do={ + $LogPrint warning $0 ("Downloading certificate failed, trying without."); + } + + :foreach Script in=$Scripts do={ + :if ([ :len [ /system/script/find where name=$Script ] ] > 0) do={ + $LogPrint warning $0 ("Requested to add script '" . $Script . "', but that exists already!"); + } else={ + $LogPrint info $0 ("Adding new script: " . $Script); + /system/script/add name=$Script owner=$Script source="#!rsc by RouterOS\n" comment=$NewComment; + } + } + + :local CommitIdBefore $CommitId; + :local ExpectedConfigVersionBefore $ExpectedConfigVersion; + :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 ]; + :local ScriptInfo [ $ParseKeyValueStore ($ScriptVal->"comment") ]; + :local SourceNew; + + :foreach Scheduler in=[ /system/scheduler/find where on-event~("\\b" . $ScriptVal->"name" . "\\b") ] do={ + :local SchedulerVal [ /system/scheduler/get $Scheduler ]; + :if ($ScriptVal->"policy" != $SchedulerVal->"policy") do={ + $LogPrint warning $0 ("Policies differ for script '" . $ScriptVal->"name" . \ + "' and its scheduler '" . $SchedulerVal->"name" . "'!"); + } + } + + :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") "fetch" ] = 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=({ [ $FetchUserAgentStr $0 ] }) $Url output=user as-value ]; + :if ($Result->"status" = "finished") do={ + :set SourceNew [ :tolf ($Result->"data") ]; + } + } do={ + $LogPrint warning $0 ("Failed fetching script '" . $ScriptVal->"name" . "': " . $Err); + :if ($Err != "Fetch failed with status 404") do={ + :error false; + } + + :if ($ScriptVal->"source" = "#!rsc by RouterOS\n") do={ + $LogPrint warning $0 ("Removing dummy. Typo on installation?"); + /system/script/remove $Script; + :error false; + } + :if ([ :len ($ScriptInfo->"base-url") ] = 0 && [ :len ($ScriptInfo->"url-suffix") ] = 0 && \ + [ :len $CheckSum ] = 0) do={ + $LogPrintOnce warning $0 \ + ("Added the script manually? Skip updates with 'ignore=true' in comment."); + } + :error false; + } + + :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); + } + } + :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" ~ ("^(global-functions\\.d|mod)/.")) do={ + :set ReloadGlobal true; + } + } on-error={ } + } + + :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; + } do={ + $LogPrint error $0 ("Reloading global configuration and functions failed! " . $Err); + } + } + + :if ($ExpectedConfigVersionBefore > $ExpectedConfigVersion) do={ + $LogPrint warning $0 ("The configuration version decreased from " . \ + $ExpectedConfigVersionBefore . " to " . $ExpectedConfigVersion . \ + ". Installed an older version?"); + } + + :if ($ExpectedConfigVersionBefore < $ExpectedConfigVersion) do={ + :global GlobalConfigChanges; + :global GlobalConfigMigration; + :local ChangeLogCode; + + :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=({ [ $FetchUserAgentStr $0 ] }) $Url output=user as-value ]; + :if ($Result->"status" = "finished") do={ + :set ChangeLogCode ($Result->"data"); + } + } do={ + $LogPrint warning $0 ("Failed fetching news, changes and migration: " . $Err); + } + + :if ([ :len $ChangeLogCode ] > 0) do={ + :if ([ $ValidateSyntax $ChangeLogCode ] = true) do={ + :onerror Err { + [ :parse $ChangeLogCode ]; + } do={ + $LogPrint warning $0 ("The changelog failed to run: " . $Err); + } + } else={ + $LogPrint warning $0 ("The changelog failed syntax validation!"); + } + } + + :if ([ :len $GlobalConfigMigration ] > 0) do={ + :for I from=($ExpectedConfigVersionBefore + 1) to=$ExpectedConfigVersion do={ + :local Migration ($GlobalConfigMigration->[ :tostr $I ]); + :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={ } + } + } + + :local NotificationMessage ("The configuration version on " . $Identity . " increased " . \ + "to " . $ExpectedConfigVersion . ", current configuration may need modification. " . \ + "Please review and update global-config-overlay, then re-run global-config."); + $LogPrint info $0 ($NotificationMessage); + + :if ([ :len $GlobalConfigChanges ] > 0) do={ + :set NotificationMessage ($NotificationMessage . "\n\nChanges:"); + :for I from=($ExpectedConfigVersionBefore + 1) to=$ExpectedConfigVersion do={ + :local Change ($GlobalConfigChanges->[ :tostr $I ]); + :set NotificationMessage ($NotificationMessage . "\n " . \ + [ $SymbolForNotification "pushpin" "*" ] . $Change); + $LogPrint info $0 ("Change " . $I . ": " . $Change); + } + } else={ + :set NotificationMessage ($NotificationMessage . "\n\nNews and changes are not available."); + } + + :if ($NoNewsAndChangesNotification != true) do={ + :local Link; + :if ($IDonate != true) do={ + :set NotificationMessage ($NotificationMessage . \ + "\n\n==== donation hint ====\n" . \ + "This project is developed in private spare time and usage is " . \ + "free of charge for you. If you like the scripts and think this is " . \ + "of value for you or your business please consider a donation."); + :set Link "https://rsc.eworm.de/#donate"; + } + + $SendNotification2 ({ origin=$0; \ + subject=([ $SymbolForNotification "pushpin" ] . "News and configuration changes"); \ + message=$NotificationMessage; link=$Link }); + } + + :set GlobalConfigChanges; + :set GlobalConfigMigration; + } +} do={ + :global ExitOnError; $ExitOnError $0 $Err; +} } + +# lock script against multiple invocation +:set ScriptLock do={ + :local Script [ :tostr $1 ]; + :local WaitMax [ :totime $2 ]; + + :global GetRandom20CharAlNum; + :global IfThenElse; + :global LogPrint; + + :global ScriptLockOrder; + :if ([ :typeof $ScriptLockOrder ] = "nothing") do={ + :set ScriptLockOrder ({}); + } + :if ([ :typeof ($ScriptLockOrder->$Script) ] = "nothing") do={ + :set ($ScriptLockOrder->$Script) ({}); + } + + :local JobCount do={ + :local Script [ :tostr $1 ]; + + :return [ :len [ /system/script/job/find where script=$Script ] ]; + } + + :local TicketCount do={ + :local Script [ :tostr $1 ]; + + :global ScriptLockOrder; + + :local Count 0; + :foreach Ticket in=($ScriptLockOrder->$Script) do={ + :if ([ :typeof $Ticket ] != "nothing") do={ + :set Count ($Count + 1); + } + } + :return $Count; + } + + :local IsFirstTicket do={ + :local Script [ :tostr $1 ]; + :local Check [ :tostr $2 ]; + + :global ScriptLockOrder; + + :foreach Ticket in=($ScriptLockOrder->$Script) do={ + :if ($Ticket = $Check) do={ :return true; } + :if ([ :typeof $Ticket ] != "nothing" && $Ticket != $Check) do={ :return false; } + } + :return false; + } + + :local AddTicket do={ + :local Script [ :tostr $1 ]; + :local Add [ :tostr $2 ]; + + :global ScriptLockOrder; + + :while (true) do={ + :local Pos [ :len ($ScriptLockOrder->$Script) ]; + :set ($ScriptLockOrder->$Script->$Pos) $Add; + :delay 10ms; + :if (($ScriptLockOrder->$Script->$Pos) = $Add) do={ :return true; } + } + } + + :local RemoveTicket do={ + :local Script [ :tostr $1 ]; + :local Remove [ :tostr $2 ]; + + :global ScriptLockOrder; + + :foreach Id,Ticket in=($ScriptLockOrder->$Script) do={ + :while (($ScriptLockOrder->$Script->$Id) = $Remove) do={ + :set ($ScriptLockOrder->$Script->$Id); + :delay 10ms; + } + } + } + + :local CleanupTickets do={ + :local Script [ :tostr $1 ]; + + :global ScriptLockOrder; + + :foreach Ticket in=($ScriptLockOrder->$Script) do={ + :if ([ :typeof $Ticket ] != "nothing") do={ + :return false; + } + } + + :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; + } + + :if ([ $JobCount $Script ] = 0) do={ + $LogPrint error $0 ("No script '" . $Script . "' is running!"); + :error false; + } + + :if ([ $TicketCount $Script ] >= [ $JobCount $Script ]) do={ + $LogPrint error $0 ("More tickets than running scripts '" . $Script . "', resetting!"); + :set ($ScriptLockOrder->$Script) ({}); + /system/script/job/remove [ find where script=$Script ]; + } + + :local MyTicket [ $GetRandom20CharAlNum 6 ]; + $AddTicket $Script $MyTicket; + + :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={ + $RemoveTicket $Script $MyTicket; + $CleanupTickets $Script; + :return true; + } + + $RemoveTicket $Script $MyTicket; + $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={ :onerror Err { + :global SendNotification2; + + $SendNotification2 ({ origin=$0; subject=$1; message=$2; link=$3; silent=$4 }); +} do={ + :global ExitOnError; $ExitOnError $0 $Err; +} } + +# send notification via NotificationFunctions - expects one array argument +:set SendNotification2 do={ + :local Notification $1; + + :global NotificationFunctions; + + :foreach FunctionName,Discard in=$NotificationFunctions do={ + ($NotificationFunctions->$FunctionName) \ + ("\$NotificationFunctions->\"" . $FunctionName . "\"") \ + $Notification; + } +} + +# return UTF-8 symbol for unicode name +:set SymbolByUnicodeName do={ + :local Name [ :tostr $1 ]; + + :global EitherOr; + :global LogPrintOnce; + + :global SymbolsExtra; + + :local Symbols ({ + "abacus"="\F0\9F\A7\AE"; + "alarm-clock"="\E2\8F\B0"; + "arrow-down"="\E2\AC\87"; + "arrow-up"="\E2\AC\86"; + "calendar"="\F0\9F\93\85"; + "card-file-box"="\F0\9F\97\83"; + "chart-decreasing"="\F0\9F\93\89"; + "chart-increasing"="\F0\9F\93\88"; + "cloud"="\E2\98\81"; + "cross-mark"="\E2\9D\8C"; + "earth"="\F0\9F\8C\8D"; + "fire"="\F0\9F\94\A5"; + "floppy-disk"="\F0\9F\92\BE"; + "gear"="\E2\9A\99"; + "heart"="\E2\99\A5"; + "high-voltage-sign"="\E2\9A\A1"; + "incoming-envelope"="\F0\9F\93\A8"; + "information"="\E2\84\B9"; + "large-orange-circle"="\F0\9F\9F\A0"; + "large-red-circle"="\F0\9F\94\B4"; + "link"="\F0\9F\94\97"; + "lock-with-ink-pen"="\F0\9F\94\8F"; + "memo"="\F0\9F\93\9D"; + "mobile-phone"="\F0\9F\93\B1"; + "pushpin"="\F0\9F\93\8C"; + "scissors"="\E2\9C\82"; + "scroll"="\F0\9F\93\9C"; + "smiley-partying-face"="\F0\9F\A5\B3"; + "smiley-smiling-face"="\E2\98\BA"; + "smiley-winking-face-with-tongue"="\F0\9F\98\9C"; + "sparkles"="\E2\9C\A8"; + "speech-balloon"="\F0\9F\92\AC"; + "star"="\E2\AD\90"; + "warning-sign"="\E2\9A\A0"; + "white-heavy-check-mark"="\E2\9C\85" + }, $SymbolsExtra); + + :local Magic [ :pick [ /system/clock/get date ] 4 10 ]; + :local Special { + "information-04-01"="\F0\9F\9A\BB"; + "large-orange-circle-04-01"="\F0\9F\8D\8A"; + "large-orange-circle-10-31"="\F0\9F\8E\83"; + "large-red-circle-04-01"="\F0\9F\8D\92" }; + + :if ([ :len ($Symbols->$Name) ] = 0) do={ + $LogPrintOnce warning $0 ("No symbol available for name '" . $Name . "'!"); + :return ""; + } + + :return ([ $EitherOr ($Special->($Name . $Magic)) ($Symbols->$Name) ] . "\EF\B8\8F"); +} + +# return symbol for notification +:set SymbolForNotification do={ + :global NotificationsWithSymbols; + :global SymbolByUnicodeName; + :global IfThenElse; + + :if ($NotificationsWithSymbols != true) do={ + :return [ $IfThenElse ([ :len $2 ] > 0) ([ :tostr $2 ] . " ") "" ]; + } + :local Return ""; + :foreach Symbol in=[ :toarray $1 ] do={ + :set Return ($Return . [ $SymbolByUnicodeName $Symbol ]); + } + :return ($Return . " "); +} + +# convert line endings, UNIX -> DOS +:set Unix2Dos do={ + :return [ :tocrlf [ :tostr $1 ] ]; +} + +# url encoding +:set UrlEncode do={ + :local Input [ :tostr $1 ]; + + :if ([ :len $Input ] = 0) do={ + :return ""; + } + + :local Return ""; + :local Chars ("\n\r !\"#\$%&'()*+,:;<=>?@[\\]^`{|}~"); + :local Subs { "%0A"; "%0D"; "%20"; "%21"; "%22"; "%23"; "%24"; "%25"; "%26"; "%27"; + "%28"; "%29"; "%2A"; "%2B"; "%2C"; "%3A"; "%3B"; "%3C"; "%3D"; "%3E"; "%3F"; + "%40"; "%5B"; "%5C"; "%5D"; "%5E"; "%60"; "%7B"; "%7C"; "%7D"; "%7E" }; + + :for I from=0 to=([ :len $Input ] - 1) do={ + :local Char [ :pick $Input $I ]; + :local Replace [ :find $Chars $Char ]; + + :if ([ :typeof $Replace ] = "num") do={ + :set Char ($Subs->$Replace); + } + :set Return ($Return . $Char); + } + + :return $Return; +} + +# basic syntax validation +:set ValidateSyntax do={ + :local Code [ :tostr $1 ]; + + :global LogPrint; + + :onerror Err { + [ :parse (":local Validate do={\n" . $Code . "\n}") ]; + } do={ + $LogPrint debug $0 ("Valdation failed: " . $Err); + :return false; + } + :return true; +} + +# convert version string to numeric value +:set VersionToNum do={ + :local Input [ :tostr $1 ]; + :local Multi 0x1000000; + :local Return 0; + + :global CharacterReplace; + + :set Input [ $CharacterReplace $Input "." "," ]; + :foreach I in={ "zero"; "alpha"; "beta"; "rc" } do={ + :set Input [ $CharacterReplace $Input $I ("," . $I . ",") ]; + } + + :foreach Value in=([ :toarray $Input ], 0) do={ + :local Num [ :tonum $Value ]; + :if ($Multi = 0x100) do={ + :if ([ :typeof $Num ] = "num") do={ + :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); } + } + } + :if ([ :typeof $Num ] = "num") do={ :set Return ($Return + ($Value * $Multi)); } + :set Multi ($Multi / 0x100); + } + + :return $Return; +} + +# wait for default route to be reachable +:set WaitDefaultRouteReachable do={ + :global IsDefaultRouteReachable; + + :while ([ $IsDefaultRouteReachable ] = false) do={ + :delay 1s; + } +} + +# wait for DNS to resolve +:set WaitDNSResolving do={ + :global IsDNSResolving; + + :while ([ $IsDNSResolving ] = false) do={ + :delay 1s; + } +} + +# wait for file to be available +:set WaitForFile do={ + :local FileName [ :tostr $1 ]; + :local WaitTime [ :totime $2 ]; + + :global CleanFilePath; + :global EitherOr; + :global MAX; + + :set FileName [ $CleanFilePath $FileName ]; + :local Delay ([ $MAX [ $EitherOr $WaitTime 2s ] 100ms ] / 9); + + :do { + :retry { + /file/get $FileName; + :return true; + } delay=$Delay max=10; + } on-error={ } + + :while ([ :len [ /file/find where name=$FileName ] ] > 0) do={ + :do { + /file/get $FileName; + :return true; + } on-error={ } + :delay $Delay; + :set Delay ($Delay * 3 / 2); + } + + :return false; +} + +# wait to be fully connected (default route is reachable, time is sync, DNS resolves) +:set WaitFullyConnected do={ + :global WaitDefaultRouteReachable; + :global WaitDNSResolving; + :global WaitTimeSync; + + $WaitDefaultRouteReachable; + $WaitTimeSync; + $WaitDNSResolving; +} + +# wait for time to become synced +:set WaitTimeSync do={ + :global IsTimeSync; + + :while ([ $IsTimeSync ] = false) do={ + :delay 1s; + } +} + +# load modules +:foreach Script in=[ /system/script/find where name ~ "^(global-functions\\.d|mod)/." ] do={ + :local ScriptVal [ /system/script/get $Script ]; + :if ([ $ValidateSyntax ($ScriptVal->"source") ] = true) do={ + :onerror Err { + /system/script/run $Script; + } do={ + $LogPrint error $0 ("Module '" . $ScriptVal->"name" . "' failed to run: " . $Err); + } + } else={ + $LogPrint error $0 ("Module '" . $ScriptVal->"name" . "' failed syntax validation, skipping."); + } +} + +# add (and fix) global scripts scheduler +/system/scheduler { + :local OnEvent "/system/script { run global-config; run global-functions; }"; + :if ([ :len [ find where name="global-scripts" ] ] = 0) do={ + add name="global-scripts" start-time=startup; + } + set on-event=$OnEvent [ find where name="global-scripts" on-event!=$OnEvent ]; + enable [ find where name="global-scripts" disabled ]; +} + +# Log success +:local Resource [ /system/resource/get ]; +$LogPrintOnce info $ScriptName ("Loaded " . \ + [ $IfThenElse ($CommitId != "unknown") ([ $CommitBrief ] . " ") ] . \ + "on " . $Resource->"board-name" . " with RouterOS " . $Resource->"version" . "."); + +# signal we are ready +:set GlobalFunctionsReady true; diff --git a/html/logo/index.html b/html/logo/index.html index e4e3633c3..0cf32cf55 100644 --- a/html/logo/index.html +++ b/html/logo/index.html @@ -5,10 +5,10 @@ - - - - + + + + @@ -38,7 +38,7 @@

Logo Color Changer

- +

Select the colors here: diff --git a/html/mod/AM-DateTime.rsc b/html/mod/AM-DateTime.rsc index cee377a9a..e23aec63a 100644 --- a/html/mod/AM-DateTime.rsc +++ b/html/mod/AM-DateTime.rsc @@ -1,9 +1,13 @@ #!rsc by Vados -# RouterOS function-collection -# DateTime Functions +# RouterOS script: mod/AM-DateTime +# Script comment: DateTime format functions +# +# +# requires RouterOS, version=7.21 +# requires device-mode, fetch, scheduler :global CurrentDateTime; -:global FullDateTimeStamp; +:global DateTimeStamp; :global MiniDateTimeStamp; :set CurrentDateTime do={ @@ -16,7 +20,9 @@ } else={:set currDate $rawDate} :return ($currDate . " " . $currTime); } -:set FullDateTimeStamp do={ + +:set DateTimeStamp do={ + :local getFmt [:tostr $1] /system clock :local vdate [get date] :local vtime [get time] @@ -69,9 +75,13 @@ :local s [:tonum $ss] :local Z "$gmtSg$gmtHr:$gmtMn" :local Unix (((((($totd * 24) + $H) * 60) + $m) * 60) + $s - $vgmt) -# :return "$yyyy-$MM-$dd\54$HH:$mm:$ss$Z $Www (YD: $YD) (MD: $MD) (WD: $WD) $Leap $Unix" - :return "$yyyy$MM$dd$HH$mm$ss" + :if ($getFmt = "Min") do={ + :return "$yyyy$MM$dd$HH$mm$ss"; + } else={ + :return "$yyyy$MM$dd$HH$mm$ss"; + } } + :set MiniDateTimeStamp do={ /system clock :local vdate [get date] diff --git a/html/AM-GitBackup.rsc b/html/mod/AM-GitBackup.rsc similarity index 89% rename from html/AM-GitBackup.rsc rename to html/mod/AM-GitBackup.rsc index 368b6d4fd..7d1fbb903 100644 --- a/html/AM-GitBackup.rsc +++ b/html/mod/AM-GitBackup.rsc @@ -1,4 +1,9 @@ -# MikroTik Git Config Backup +#!rsc by Vados +# RouterOS script: mod/AM-GitBackup +# Script comment: MikroTik Git Config Backup +# Copyright (c) 2005-2026 Vados +# +# # Edit the three variables below, then add this script under System > Scripts :local serverUrl "http://192.168.1.10:8080" diff --git a/html/mod/AM-IPbridge.rsc b/html/mod/AM-IPbridge.rsc new file mode 100644 index 000000000..c0ecbd887 --- /dev/null +++ b/html/mod/AM-IPbridge.rsc @@ -0,0 +1,30 @@ +#!rsc by Vados +# RouterOS script: mod/AM-IPbridge +# Script comment: Enable or disable ip addresses based on bridge port state +# Copyright (c) 2005-2026 Vados +# +# +# requires RouterOS, version=7.19 +# requires device-mode, fetch +# +:local ScriptName [ :jobname ]; + +# Add this Script scheduler +:if ([ :len [ /system/scheduler/find where name=$ScriptName ] ] = 0) do={ +:log warning ("SystemScheduler for script: " . $ScriptName . " is NOT SET! Fix It Now!"); +:put "Add or/and fix $ScriptName Script scheduler"; +:local OnEvent "/system/script/run $ScriptName;"; +:local Comment "Every Day Check For Fake Bridges"; +/system/scheduler/add name=$ScriptName comment=$Comment start-time=startup interval=1d on-event=$OnEvent; +} + +:foreach Bridge in=[ /interface/bridge/find ] do={ + :local BrName [ /interface/bridge/get $Bridge name ]; + :if ([ :len [ /interface/bridge/port/find where bridge=$BrName ] ] > 0) do={ + :if ([ :len [ /interface/bridge/port/find where bridge=$BrName and inactive=no ] ] = 0) do={ + /ip/address/disable [ find where !dynamic interface=$BrName ]; + } else={ + /ip/address/enable [ find where !dynamic interface=$BrName ]; + } + } +} diff --git a/html/mod/ipcalc.rsc b/html/mod/AM-IPcalc.rsc similarity index 53% rename from html/mod/ipcalc.rsc rename to html/mod/AM-IPcalc.rsc index 2aad0cb09..836df4fcd 100644 --- a/html/mod/ipcalc.rsc +++ b/html/mod/AM-IPcalc.rsc @@ -1,19 +1,48 @@ #!rsc by Vados -# RouterOS script: ipcalc +# RouterOS script: mod/AM-IPcalc # Script comment: ip address calculation # # # requires RouterOS, version=7.19 -:global IPCalc; -:global IPCalcReturn; +:local IPCalc; # print netmask, network, min host, max host and broadcast :set IPCalc do={ :onerror Err { :local Input [ :tostr $1 ]; - :global FormatLine; - :global IPCalcReturn; + :local FormatLine; + :local IPCalcReturn; :local Values [ $IPCalcReturn $1 ]; + :set FormatLine do={ + :local Key [ :tostr $1 ]; + :local Value [ :tostr $2 ]; + :local Indent [ :tonum $3 ]; + :local Spaces; + :local Return ""; + :local CharacterMultiply; + :local EitherOr; + :set CharacterMultiply do={ + :local Str [ :tostr $1 ]; + :local Num [ :tonum $2 ]; + :local Return ""; + :if ($Num = 0) do={:return ""} + :for I from=1 to=$Num do={:set Return ($Return . $Str)} + :return $Return; + } + :set EitherOr do={ + :local IfThenElse; + :set IfThenElse do={:if ([ :tostr $1 ] = "true" || [ :tobool $1 ] = true) do={ :return $2 }; :return $3; } + :if ([ :typeof $1 ] = "num") do={:return [ $IfThenElse ($1 != 0) $1 $2 ]} + :if ([ :typeof $1 ] = "time") do={:return [ $IfThenElse ($1 > 0s) $1 $2 ]} + :return [ $IfThenElse ([ :len [ :tostr $1 ] ] > 0) $1 $2 ]; + } + :set Indent [ $EitherOr $Indent 16 ]; + :local Spaces [ $CharacterMultiply " " $Indent ]; + :if ([ :len $Key ] > 0) do={ :set Return ($Key . ":")} + :if ([ :len $Key ] > ($Indent - 2)) do={:set Return ($Return . "\n" . [ :pick $Spaces 0 $Indent ] . $Value); + } else={:set Return ($Return . [ :pick $Spaces 0 ($Indent - [ :len $Return ]) ] . $Value)} + :return $Return; + } :put [ :tocrlf ( \ [ $FormatLine "Address" ($Values->"address") ] . "\n" . \ [ $FormatLine "Netmask" ($Values->"netmask") ] . "\n" . \ @@ -22,7 +51,7 @@ [ $FormatLine "HostMax" ($Values->"hostmax") ] . "\n" . \ [ $FormatLine "Broadcast" ($Values->"broadcast") ]) ]; } do={ - :global ExitOnError; $ExitOnError $0 $Err; + :log error ("Error:" ); } } # calculate and return netmask, network, min host, max host and broadcast diff --git a/html/mod/AM-RunOnce.rsc b/html/mod/AM-RunOnce.rsc index 9dfc1a4ef..4798bb590 100644 --- a/html/mod/AM-RunOnce.rsc +++ b/html/mod/AM-RunOnce.rsc @@ -1,7 +1,8 @@ #!rsc by Vados -# RouterOS script: RunOnce +# RouterOS script: mod/AM-RunOnce # Script comment: Download script and run it once # +# # requires RouterOS, version=7.19 :global ScriptRunOnce; diff --git a/html/mod/AM-TG-Bot.rsc b/html/mod/AM-TG-Bot.rsc index 17ef3aff3..6c7af2b40 100644 --- a/html/mod/AM-TG-Bot.rsc +++ b/html/mod/AM-TG-Bot.rsc @@ -1,5 +1,5 @@ #!rsc by Vados -# RouterOS script: AM-TG-Bot +# RouterOS script: mod/AM-TG-Bot # Script comment: Use Telegram to chat with your Router and send commands # Copyright (c) 2007-2026 Vados # diff --git a/html/mod/AM-TG-Notifications.rsc b/html/mod/AM-TG-Notifications.rsc index fdcc947a1..1190760b9 100644 --- a/html/mod/AM-TG-Notifications.rsc +++ b/html/mod/AM-TG-Notifications.rsc @@ -1,5 +1,5 @@ #!rsc by Vados -# RouterOS script: AM-TG-Notifications +# RouterOS script: mod/AM-TG-Notifications # Script comment: Script send Notifications in Telegram # Copyright (c) 2007-2026 Vados # diff --git a/html/Update-Certificates.rsc b/html/mod/AM-UpdateCertsCustom.rsc similarity index 95% rename from html/Update-Certificates.rsc rename to html/mod/AM-UpdateCertsCustom.rsc index c4012ca1b..b5351a43c 100644 --- a/html/Update-Certificates.rsc +++ b/html/mod/AM-UpdateCertsCustom.rsc @@ -1,10 +1,9 @@ #!rsc by Vados -# RouterOS script: Update-Certificates -# Script comment: Certificates CloudFlare, Quad9 +# RouterOS script: mod/AM-UpdateCertsCustom +# Script comment: Custom update Mikrotiks Certificates (Google, CloudFlare, Quad9...) # Copyright (c) 2005-2026 Vados # # -# # requires RouterOS, version=7.19 # requires device-mode, fetch # -- 2.52.0