#!rsc by RouterOS # RouterOS script: global-functions # Copyright (c) 2013-2021 Christian Hesse # Michael Gisbers # https://git.eworm.de/cgit/routeros-scripts/about/COPYING.md # # global functions # https://git.eworm.de/cgit/routeros-scripts/about/ # expected configuration version :global ExpectedConfigVersion 53; # global variables not to be changed by user :global GlobalFunctionsReady false; :global Identity [ / system identity get name ]; # global functions :global CertificateAvailable; :global CertificateDownload; :global CertificateNameByCN; :global CharacterReplace; :global CleanFilePath; :global DefaultRouteIsReachable; :global DeviceInfo; :global DNSIsResolving; :global DownloadPackage; :global EitherOr; :global EscapeForRegEx; :global FlushEmailQueue; :global FlushTelegramQueue; :global GetMacVendor; :global GetRandom20CharHex; :global GetRandomNumber; :global IfThenElse; :global IPCalc; :global LogPrintExit; :global LogPrintExit2; :global MkDir; :global NotificationFunctions; :global ParseKeyValueStore; :global QuotedPrintable; :global RandomDelay; :global RequiredRouterOS; :global ScriptFromTerminal; :global ScriptInstallUpdate; :global ScriptLock; :global SendEMail; :global SendEMail2; :global SendNotification; :global SendNotification2; :global SendTelegram; :global SendTelegram2; :global SymbolByUnicodeName; :global SymbolForNotification; :global TimeIsSync; :global UrlEncode; :global ValidateSyntax; :global VersionToNum; :global WaitDefaultRouteReachable; :global WaitDNSResolving; :global WaitForFile; :global WaitFullyConnected; :global WaitTimeSync; # check and download required certificate :set CertificateAvailable do={ :local CommonName [ :tostr $1 ]; :global CertificateDownload; :global LogPrintExit2; :global ParseKeyValueStore; :global RequiredRouterOS; :if ([ / system resource get free-hdd-space ] < 8388608 && \ [ / certificate settings get crl-download ] = true && \ [ / certificate settings get crl-store ] = "system") do={ $LogPrintExit2 warning $0 ("This system has low free flash space but " . \ "is configured to download certificate CRLs to system!") false; } :if ([ :len [ / certificate find where common-name=$CommonName ] ] = 0) do={ $LogPrintExit2 info $0 ("Certificate with CommonName \"" . $CommonName . "\" not available.") false; :if ([ $CertificateDownload $CommonName ] = false) do={ :return false; } } :if ([ $RequiredRouterOS $0 "6.47" ] = false) do={ :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={ $LogPrintExit2 info $0 ("Certificate chain for \"" . $CommonName . \ "\" is incomplete, missing \"" . ([ $ParseKeyValueStore ($CertVal->"issuer") ]->"CN") . "\".") false; :if ([ $CertificateDownload $CommonName ] = 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 LogPrintExit2; :global UrlEncode; :global WaitForFile; $LogPrintExit2 info $0 ("Downloading and importing certificate with " . \ "CommonName \"" . $CommonName . "\".") false; :do { :local LocalFileName ($CommonName . ".pem"); :local UrlFileName ([ $UrlEncode $CommonName ] . ".pem"); / tool fetch check-certificate=yes-without-crl \ ($ScriptUpdatesBaseUrl . "certs/" . \ $UrlFileName . $ScriptUpdatesUrlSuffix) \ dst-path=$LocalFileName as-value; $WaitForFile $LocalFileName; / certificate import file-name=$LocalFileName passphrase="" as-value; / file remove $LocalFileName; :foreach Cert in=[ / certificate find where name~("^" . $LocalFileName . "_[0-9]+\$") ] do={ $CertificateNameByCN [ / certificate get $Cert common-name ]; } } on-error={ $LogPrintExit2 warning $0 ("Failed importing certificate with " . \ "CommonName \"" . $CommonName . "\"!") false; :return false; } :return true; } # name a certificate by its common-name :set CertificateNameByCN do={ :local CommonName [ :tostr $1 ]; :global CharacterReplace; :local Cert [ / certificate find where common-name=$CommonName ]; / certificate set $Cert \ name=[ $CharacterReplace [ $CharacterReplace [ $CharacterReplace $CommonName "'" "-" ] " " "-" ] "---" "-" ]; } # 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; } # default route is reachable :set DefaultRouteIsReachable do={ :if ([ :len [ / ip route find where dst-address=0.0.0.0/0 active !blackhole !routing-mark !unreachable ] ] > 0) do={ :return true; } :return false; } # get readable device info :set DeviceInfo do={ :global ExpectedConfigVersion; :global GlobalConfigVersion; :global Identity; :global IfThenElse; :local Resource [ / system resource get ]; :local RouterBoard [ / system routerboard get ]; :local Update [ / system package update get ]; :return ( \ "Hostname: " . $Identity . \ "\nBoard name: " . $Resource->"board-name" . \ "\nArchitecture: " . $Resource->"architecture-name" . \ [ $IfThenElse ($RouterBoard->"routerboard" = true) \ ("\nModel: " . $RouterBoard->"model" . \ [ $IfThenElse ([ :len ($RouterBoard->"revision") ] > 0) \ (" " . $RouterBoard->"revision") ] . \ "\nSerial number: " . $RouterBoard->"serial-number") ] . \ "\nRouterOS:" . \ "\n Channel: " . $Update->"channel" . \ "\n Installed: " . $Update->"installed-version" . \ [ $IfThenElse ([ :typeof ($Update->"latest-version") ] != "nothing" && \ $Update->"installed-version" != $Update->"latest-version") \ ("\n Available: " . $Update->"latest-version") ] . \ "\nRouterOS-Scripts:" . \ "\n Current: " . $GlobalConfigVersion . \ [ $IfThenElse ($GlobalConfigVersion != $ExpectedConfigVersion) \ ("\n Expected: " . $ExpectedConfigVersion) ]); } # check if DNS is resolving :set DNSIsResolving do={ :global CharacterReplace; :do { :resolve "low-ttl.eworm.de"; } on-error={ :return false; } :return true; } # 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 LogPrintExit2; :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 ]; } :local PkgFile ($PkgName . "-" . $PkgVer . "-" . $PkgArch . ".npk"); :if ($PkgArch = "x86_64" || $PkgName ~ "^routeros-") do={ :set PkgFile ($PkgName . "-" . $PkgVer . ".npk"); } :local PkgDest [ $CleanFilePath ($PkgDir . "/" . $PkgFile) ]; :if ([ :len [ / file find where name=$PkgDest type="package" ] ] > 0) do={ $LogPrintExit2 info $0 ("Package file " . $PkgName . " already exists.") false; :return true; } :if ([ $CertificateAvailable "R3" ] = false) do={ $LogPrintExit2 error $0 ("Downloading required certificate failed.") true; } $LogPrintExit2 info $0 ("Downloading package file '" . $PkgName . "'...") false; :local Retry 3; :while ($Retry > 0) do={ :do { / tool fetch check-certificate=yes-without-crl \ ("https://upgrade.mikrotik.com/routeros/" . $PkgVer . "/" . $PkgFile) \ dst-path=$PkgDest; $WaitForFile $PkgDest; :if ([ / file get [ find where name=$PkgDest ] type ] = "package") do={ :return true; } } on-error={ $LogPrintExit2 debug $0 ("Downloading package file failed.") false; } / file remove [ find where name=$PkgDest ]; :set Retry ($Retry - 1); } $LogPrintExit2 warning $0 ("Downloading package file '" . $PkgName . "' failed.") false; :return false; } # return either first (if "true") or second :set EitherOr do={ :global IfThenElse; :if ([ :typeof $1 ] = "num") do={ :return [ $IfThenElse ($1 != 0) $1 $2 ]; } :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; } # flush e-mail queue :set FlushEmailQueue do={ :global EmailQueue; :global EitherOr; :global LogPrintExit2; :local AllDone true; :local QueueLen [ :len $EmailQueue ]; :if ([ :len [ / system scheduler find where name="FlushEmailQueue" ] ] > 0 && $QueueLen = 0) do={ $LogPrintExit2 warning $0 ("Flushing E-Mail messages from scheduler, but queue is empty.") false; } / system scheduler set interval=($QueueLen . "m") [ find where name="FlushEmailQueue" ]; :foreach Id,Message in=$EmailQueue do={ :if ([ :typeof $Message ] = "array" ) do={ / tool e-mail send to=($Message->"to") cc=($Message->"cc") subject=($Message->"subject") \ body=($Message->"body") file=[ $EitherOr ($Message->"attach") "" ]; :local Wait true; :do { :delay 1s; :local Status [ / tool e-mail get last-status ]; :if ($Status = "succeeded") do={ :set ($EmailQueue->$Id); :set Wait false; } :if ($Status = "failed") do={ :set AllDone false; :set Wait false; } } while=($Wait = true); } } :if ($AllDone = true && $QueueLen = [ :len $EmailQueue ]) do={ / system scheduler remove [ find where name="FlushEmailQueue" ]; :set EmailQueue; } else={ / system scheduler set interval=1m [ find where name="FlushEmailQueue" ]; } } # flush telegram queue :set FlushTelegramQueue do={ :global TelegramQueue; :global LogPrintExit2; :local AllDone true; :local QueueLen [ :len $TelegramQueue ]; :if ([ :len [ / system scheduler find where name="FlushTelegramQueue" ] ] > 0 && $QueueLen = 0) do={ $LogPrintExit2 warning $0 ("Flushing Telegram messages from scheduler, but queue is empty.") false; } :foreach Id,Message in=$TelegramQueue do={ :if ([ :typeof $Message ] = "array" ) do={ :do { / tool fetch check-certificate=yes-without-crl output=none http-method=post \ ("https://api.telegram.org/bot" . ($Message->"tokenid") . "/sendMessage") \ http-data=("chat_id=" . ($Message->"chatid") . \ "&disable_notification=" . ($Message->"silent") . \ "&disable_web_page_preview=true&parse_mode=" . ($Message->"parsemode") . \ "&text=" . ($Message->"text")) as-value; :set ($TelegramQueue->$Id); } on-error={ $LogPrintExit2 debug $0 ("Sending queued Telegram message failed.") false; :set AllDone false; } } } :if ($AllDone = true && $QueueLen = [ :len $TelegramQueue ]) do={ / system scheduler remove [ find where name="FlushTelegramQueue" ]; :set TelegramQueue; } } # get MAC vendor :set GetMacVendor do={ :local Mac [ :tostr $1 ]; :global CertificateAvailable; :global LogPrintExit2; :do { :if ([ $CertificateAvailable "Cloudflare Inc ECC CA-3" ] = false) do={ $LogPrintExit2 warning $0 ("Downloading required certificate failed.") true; } :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={ :do { / tool fetch check-certificate=yes-without-crl ("https://api.macvendors.com/") \ output=none as-value; $LogPrintExit2 debug $0 ("The mac vendor is not known in database.") false; } on-error={ $LogPrintExit2 warning $0 ("Failed getting mac vendor.") false; } :return "unknown vendor"; } } # generate random 20 chars hex (0-9 and a-f) :set GetRandom20CharHex do={ :local Random ([ / certificate scep-server otp generate minutes-valid=0 as-value ]->"password"); / certificate scep-server otp remove [ find where password=$Random ]; :return $Random; } # generate random number :set GetRandomNumber do={ :local Max 4294967295; :if ([ :typeof $1 ] != "nothing" ) do={ :set Max ([ :tonum $1 ] + 1); } :global GetRandom20CharHex; :local Num; :local 40CharHex ([ $GetRandom20CharHex ] . [ $GetRandom20CharHex ]); :for I from=0 to=39 do={ :local Char [ :pick $40CharHex $I ]; :if ($Char~"[0-9]") do={ :set Num ($Num . $Char); } } :return ([ :tonum [ :pick $Num 0 18 ] ] % $Max); } # mimic conditional/ternary operator (condition ? consequent : alternative) :set IfThenElse do={ :if ([ :tostr $1 ] = "true" || [ :tobool $1 ] = true) do={ :return $2; } :return $3; } # calculate and print netmask, network, min host, max host and broadcast :set IPCalc do={ :local Input [ :tostr $1 ]; :local Address [ :toip [ :pick $Input 0 [ :find $Input "/" ] ] ]; :local Bits [ :tonum [ :pick $Input ([ :find $Input "/" ] + 1) [ :len $Input ] ] ]; :local Mask ((255.255.255.255 << (32 - $Bits)) & 255.255.255.255); :local Return { "address"=$Address; "netmask"=$Mask; "networkaddress"=($Address & $Mask); "networkbits"=$Bits; "network"=(($Address & $Mask) . "/" . $Bits); "hostmin"=(($Address & $Mask) | 0.0.0.1); "hostmax"=(($Address | ~$Mask) ^ 0.0.0.1); "broadcast"=($Address | ~$Mask); } :put ( \ "Address: " . $Return->"address" . "\n\r" . \ "Netmask: " . $Return->"netmask" . "\n\r" . \ "Network: " . $Return->"network" . "\n\r" . \ "HostMin: " . $Return->"hostmin" . "\n\r" . \ "HostMax: " . $Return->"hostmax" . "\n\r" . \ "Broadcast: " . $Return->"broadcast"); :return $Return; } # deprecated compatibility wrapper :set LogPrintExit do={ :global LogPrintExit2; $LogPrintExit2 warning $0 ("This function is deprecated. Please use \$LogPrintExit2 instead.") false; $LogPrintExit2 $1 "unknown" $2 $3; } # log and print with same text, optionally exit :set LogPrintExit2 do={ :local Severity [ :tostr $1 ]; :local Name [ :tostr $2 ]; :local Message [ :tostr $3 ]; :local Exit [ :tostr $4 ]; :global 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 ($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" || $PrintDebug = true) do={ :if ($Exit = "true") do={ :error ([ $PrintSeverity $Severity ] . ": " . $Message); } else={ :put ([ $PrintSeverity $Severity ] . ": " . $Message); } } } # create directory :set MkDir do={ :local Dir [ :tostr $1 ]; :global CleanFilePath; :global WaitForFile; :set Dir [ $CleanFilePath $Dir ]; :if ([ :len [ / file find where name=$Dir type="directory" ] ] = 1) do={ :return true; } :local Return true; :local WwwVal [ / ip service get www ]; / ip service set www address=127.0.0.1/32 disabled=no port=80; :do { / tool fetch http://127.0.0.1/ dst-path=($Dir . "/tmp") as-value; $WaitForFile ($Dir . "/tmp"); / file remove ($Dir . "/tmp"); } on-error={ :set Return false; } / ip service set www address=($WwwVal->"address") \ disabled=($WwwVal->"disabled") port=($WwwVal->"port"); :return $Return; } # prepare NotificationFunctions array :if ([ :typeof $NotificationFunctions ] != "array") do={ :set NotificationFunctions [ :toarray "" ]; } # send notification via e-mail - expects one array argument :set ($NotificationFunctions->"email") do={ :local Notification $1; :global Identity; :global EmailGeneralTo; :global EmailGeneralToOverride; :global EmailGeneralCc; :global EmailGeneralCcOverride; :global EmailQueue; :global EitherOr; :global IfThenElse; :global LogPrintExit2; :global QuotedPrintable; :local To [ $EitherOr ($EmailGeneralToOverride->($Notification->"origin")) $EmailGeneralTo ]; :local Cc [ $EitherOr ($EmailGeneralCcOverride->($Notification->"origin")) $EmailGeneralCc ]; :if ([ :len $To ] = 0) do={ :return false; } :if ([ :typeof $EmailQueue ] = "nothing") do={ :set EmailQueue [ :toarray "" ]; } :local Signature [ / system note get note ]; :set ($EmailQueue->[ :len $EmailQueue ]) { to=$To; cc=$Cc; subject=[ $QuotedPrintable ("[" . $Identity . "] " . ($Notification->"subject")) ]; body=(($Notification->"message") . \ [ $IfThenElse ([ :len ($Notification->"link") ] > 0) ("\n\n" . ($Notification->"link")) "" ] . \ [ $IfThenElse ([ :len $Signature ] > 0) ("\n-- \n" . $Signature) "" ]); \ attach=($Notification->"attach") }; :if ([ :len [ / system scheduler find where name="FlushEmailQueue" ] ] = 0) do={ / system scheduler add name=FlushEmailQueue interval=1s start-time=startup \ on-event=":global FlushEmailQueue; \$FlushEmailQueue;"; } } # send notification via telegram - expects one array argument :set ($NotificationFunctions->"telegram") do={ :local Notification $1; :global Identity; :global TelegramChatId; :global TelegramChatIdOverride; :global TelegramFixedWidthFont; :global TelegramQueue; :global TelegramTokenId; :global TelegramTokenIdOverride; :global CertificateAvailable; :global CharacterReplace; :global EitherOr; :global IfThenElse; :global LogPrintExit2; :global SymbolForNotification; :global UrlEncode; :local EscapeMD do={ :global TelegramFixedWidthFont; :global CharacterReplace; :global IfThenElse; :if ($TelegramFixedWidthFont != true) do={ :return ($1 . [ $IfThenElse ($2 = "body") "\n" "" ]); } :local Return $1; :local Chars { "body"={ "\\"; "`" }; "hint"={ "_"; "*"; "["; "]"; "("; ")"; "~"; "`"; ">"; "#"; "+"; "-"; "="; "|"; "{"; "}"; "."; "!" }; } :foreach Char in=($Chars->$2) do={ :set Return [ $CharacterReplace $Return $Char ("\\" . $Char) ]; } :if ($2 = "body") do={ :return ("```\n" . $Return . "\n```"); } :return $Return; } :local ChatId [ $EitherOr ($TelegramChatIdOverride->($Notification->"origin")) $TelegramChatId ]; :local TokenId [ $EitherOr ($TelegramTokenIdOverride->($Notification->"origin")) $TelegramTokenId ]; :if ([ :len $TokenId ] = 0 || [ :len $ChatId ] = 0) do={ :return false; } :local Truncated false; :local LenLink [ :len ($Notification->"link") ]; :local Text ("[" . $Identity . "] " . ($Notification->"subject") . "\n\n" . ($Notification->"message")); :local LenText [ :len $Text ]; :if ($LenText > (3968 - $LenLink)) do={ :set Text [ $EscapeMD ([ :pick $Text 0 (3840 - $LenLink) ] . "...") "body" ]; :set Truncated true; } else={ :set Text [ $EscapeMD $Text "body" ]; } :if ($LenLink > 0) do={ :set Text ($Text . "\n" . [ $SymbolForNotification "link" ] . [ $EscapeMD ($Notification->"link") "hint" ]); } :if ($Truncated = true) do={ :set Text ($Text . "\n" . [ $SymbolForNotification "scissors" ] . \ [ $EscapeMD ("The Telegram message was too long and has been truncated, cut off " . \ (($LenText - [ :len $Text ]) * 100 / $LenText) . "%!") "hint" ]); } :set Text [ $UrlEncode $Text ]; :local ParseMode [ $IfThenElse ($TelegramFixedWidthFont = true) "MarkdownV2" "" ]; :do { :if ([ $CertificateAvailable "Go Daddy Secure Certificate Authority - G2" ] = false) do={ $LogPrintExit2 warning $0 ("Downloading required certificate failed.") true; } / tool fetch check-certificate=yes-without-crl output=none http-method=post \ ("https://api.telegram.org/bot" . $TokenId . "/sendMessage") \ http-data=("chat_id=" . $ChatId . "&disable_notification=" . ($Notification->"silent") . \ "&disable_web_page_preview=true&parse_mode=" . $ParseMode . "&text=" . $Text) as-value; } on-error={ $LogPrintExit2 info $0 ("Failed sending telegram notification! Queuing...") false; :if ([ :typeof $TelegramQueue ] = "nothing") do={ :set TelegramQueue [ :toarray "" ]; } :set Text ($Text . [ $UrlEncode ("\n" . [ $SymbolForNotification "alarm-clock" ] . \ [ $EscapeMD ("This message was queued since " . [ / system clock get date ] . \ " " . [ / system clock get time ] . " and may be obsolete.") "hint" ]) ]); :set ($TelegramQueue->[ :len $TelegramQueue ]) { chatid=$ChatId; tokenid=$TokenId; parsemode=$ParseMode; text=$Text; silent=($Notification->"silent") }; :if ([ :len [ / system scheduler find where name="FlushTelegramQueue" ] ] = 0) do={ / system scheduler add name=FlushTelegramQueue interval=1m start-time=startup \ on-event=":global FlushTelegramQueue; \$FlushTelegramQueue;"; } } } # parse key value store :set ParseKeyValueStore do={ :local Source $1; :if ([ :typeof $Source ] != "array") do={ :set Source [ :tostr $1 ]; } :local Result [ :toarray "" ]; :foreach KeyValue in=[ :toarray $Source ] do={ :if ([ :find $KeyValue "=" ]) do={ :set ($Result->[ :pick $KeyValue 0 [ :find $KeyValue "=" ] ]) \ [ :pick $KeyValue ([ :find $KeyValue "=" ] + 1) [ :len $KeyValue ] ]; } else={ :set ($Result->$KeyValue) true; } } :return $Result; } # convert string to quoted-printable :global QuotedPrintable do={ :local Input [ :tostr $1 ]; :if ([ :len $Input ] = 0) do={ :return $Input; } :local Return ""; :local Chars ("\80\81\82\83\84\85\86\87\88\89\8A\8B\8C\8D\8E\8F\90\91\92\93\94\95\96\97" . \ "\98\99\9A\9B\9C\9D\9E\9F\A0\A1\A2\A3\A4\A5\A6\A7\A8\A9\AA\AB\AC\AD\AE\AF\B0\B1\B2\B3" . \ "\B4\B5\B6\B7\B8\B9\BA\BB\BC\BD\BE\BF\C0\C1\C2\C3\C4\C5\C6\C7\C8\C9\CA\CB\CC\CD\CE\CF" . \ "\D0\D1\D2\D3\D4\D5\D6\D7\D8\D9\DA\DB\DC\DD\DE\DF\E0\E1\E2\E3\E4\E5\E6\E7\E8\E9\EA\EB" . \ "\EC\ED\EE\EF\F0\F1\F2\F3\F4\F5\F6\F7\F8\F9\FA\FB\FC\FD\FE\FF"); :local Hex { "0"; "1"; "2"; "3"; "4"; "5"; "6"; "7"; "8"; "9"; "A"; "B"; "C"; "D"; "E"; "F" }; :for I from=0 to=([ :len $Input ] - 1) do={ :local Char [ :pick $Input $I ]; :local Replace [ :find $Chars $Char ]; :if ($Char = "=") do={ :set Char "=3D"; } :if ([ :typeof $Replace ] = "num") do={ :set Char ("=" . ($Hex->($Replace / 16 + 8)) . ($Hex->($Replace % 16))); } :set Return ($Return . $Char); } :if ($Input = $Return) do={ :return $Input; } :return ("=\?utf-8\?Q\?" . $Return . "\?="); } # delay a random amount of seconds :set RandomDelay do={ :global GetRandomNumber; :delay ([ $GetRandomNumber $1 ] . "s"); } # check for required RouterOS version :set RequiredRouterOS do={ :local Caller [ :tostr $1 ]; :local Required [ :tostr $2 ]; :global IfThenElse; :global LogPrintExit2; :global VersionToNum; :if ([ $VersionToNum $Required ] > [ $VersionToNum [ / system package update get installed-version ] ]) do={ $LogPrintExit2 warning $0 ("This " . [ $IfThenElse ([ :pick $Caller 0 ] = "\$") "function" "script" ] . \ " '" . $Caller . "' (at least specific functionality) requires RouterOS " . $Required . ". Please update!") false; :return false; } :return true; } # check if script is run from terminal :set ScriptFromTerminal do={ :local Script [ :tostr $1 ]; :global LogPrintExit2; :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={ $LogPrintExit2 debug $0 ("Script " . $Script . " started from terminal.") false; :return true; } } $LogPrintExit2 debug $0 ("Script " . $Script . " NOT started from terminal.") false; :return false; } # install new scripts, update existing scripts :set ScriptInstallUpdate do={ :local Scripts [ :toarray $1 ]; :global ExpectedConfigVersion; :global GlobalConfigVersion; :global Identity; :global IDonate; :global NotificationsWithSymbols; :global ScriptUpdatesBaseUrl; :global ScriptUpdatesFetch; :global ScriptUpdatesUrlSuffix; :global SentConfigChangesNotification; :global CertificateAvailable; :global IfThenElse; :global LogPrintExit2; :global ParseKeyValueStore; :global ScriptInstallUpdate; :global SendNotification2; :global SymbolForNotification; :global ValidateSyntax; :if ([ $CertificateAvailable "R3" ] = false) do={ $LogPrintExit2 warning $0 ("Downloading certificate failed, trying without.") false; } :foreach Script in=$Scripts do={ :if ([ :len [ / system script find where name=$Script ] ] = 0) do={ $LogPrintExit2 info $0 ("Adding new script: " . $Script) false; / system script add name=$Script source="#!rsc by RouterOS\n"; } } :local ExpectedConfigVersionBefore $ExpectedConfigVersion; :local ReloadGlobalFunctions false; :local ReloadGlobalConfig false; :foreach Script in=[ / system script find where source~"^#!rsc( by RouterOS)\?\n" ] do={ :local ScriptVal [ / system script get $Script ]; :local ScriptFile [ / file find where name=("script-updates/" . $ScriptVal->"name") ]; :local SourceNew; :if ([ :len $ScriptFile ] > 0) do={ :set SourceNew [ / file get $ScriptFile content ]; / file remove $ScriptFile; } :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={ $LogPrintExit2 warning $0 ("Policies differ for script " . $ScriptVal->"name" . \ " and its scheduler " . $SchedulerVal->"name" . "!") false; } } :if ([ :len $SourceNew ] = 0 && $ScriptUpdatesFetch = true) do={ :local Comment [ $ParseKeyValueStore ($ScriptVal->"comment") ]; :if (!($Comment->"ignore" = true)) do={ $LogPrintExit2 debug $0 ("Fetching script from url: " . $ScriptVal->"name") false; :do { :local BaseUrl $ScriptUpdatesBaseUrl; :local UrlSuffix $ScriptUpdatesUrlSuffix; :if ([ :typeof ($Comment->"base-url") ] = "str") do={ :set BaseUrl ($Comment->"base-url"); } :if ([ :typeof ($Comment->"url-suffix") ] = "str") do={ :set UrlSuffix ($Comment->"url-suffix"); } :local Result [ / tool fetch check-certificate=yes-without-crl \ ($BaseUrl . $ScriptVal->"name" . $UrlSuffix) output=user as-value ]; :if ($Result->"status" = "finished") do={ :set SourceNew ($Result->"data"); } } on-error={ $LogPrintExit2 warning $0 ("Failed fetching " . $ScriptVal->"name") false; } } } :if ([ :len $SourceNew ] > 0) do={ :if ($SourceNew != $ScriptVal->"source") do={ :if ([ :pick $SourceNew 0 18 ] = "#!rsc by RouterOS\n") do={ :if ([ $ValidateSyntax $SourceNew ] = true) do={ :local DontRequirePermissions \ ($SourceNew~"\n# requires: dont-require-permissions=yes\n"); $LogPrintExit2 info $0 ("Updating script: " . $ScriptVal->"name") false; / system script set owner=($ScriptVal->"name") source=$SourceNew \ dont-require-permissions=$DontRequirePermissions $Script; :if ($ScriptVal->"name" = "global-config") do={ :set ReloadGlobalConfig true; } :if ($ScriptVal->"name" ~ "^global-functions(\$|\\.d/.)") do={ :set ReloadGlobalFunctions true; } } else={ $LogPrintExit2 warning $0 ("Syntax validation for script " . $ScriptVal->"name" . \ " failed! Ignoring!") false; } } else={ $LogPrintExit2 warning $0 ("Looks like new script " . $ScriptVal->"name" . \ " is not valid (missing shebang). Ignoring!") false; } } else={ $LogPrintExit2 debug $0 ("Script " . $ScriptVal->"name" . " did not change.") false; } } else={ $LogPrintExit2 debug $0 ("No update for script " . $ScriptVal->"name" . ".") false; } } :if ($ReloadGlobalFunctions = true) do={ $LogPrintExit2 info $0 ("Reloading global functions.") false; :do { / system script run global-functions; } on-error={ $LogPrintExit2 error $0 ("Reloading global functions failed!") false; } } :if ($ReloadGlobalConfig = true) do={ $LogPrintExit2 info $0 ("Reloading global configuration and overlay.") false; :do { / system script { run global-config; run global-config-overlay; } } on-error={ $LogPrintExit2 error $0 ("Reloading global configuration and overlay failed!" . \ " Syntax error or missing overlay\?") false; } } :if ($ExpectedConfigVersionBefore != $ExpectedConfigVersion) do={ :global GlobalConfigChanges; :global GlobalConfigMigration; :local ChangeLogCode; $LogPrintExit2 debug $0 ("Fetching news, changes and migration.") false; :do { :local Result [ / tool fetch check-certificate=yes-without-crl \ ($ScriptUpdatesBaseUrl . "global-config.changes" . $ScriptUpdatesUrlSuffix) \ output=user as-value ]; :if ($Result->"status" = "finished") do={ :set ChangeLogCode ($Result->"data"); } } on-error={ $LogPrintExit2 warning $0 ("Failed fetching news, changes and migration!") false; } :if ([ :len $ChangeLogCode ] > 0) do={ :if ([ $ValidateSyntax $ChangeLogCode ] = true) do={ [ :parse $ChangeLogCode ]; } else={ $LogPrintExit2 warning $0 ("The changelog failed syntax validation!") false; } } :if ([ :len $GlobalConfigMigration ] > 0) do={ :for I from=($ExpectedConfigVersionBefore + 1) to=$ExpectedConfigVersion do={ :local Migration ($GlobalConfigMigration->[ :tostr $I ]); :if ([ :typeof $Migration ] = "str") do={ :if ([ $ValidateSyntax $Migration ] = true) do={ $LogPrintExit2 info $0 ("Applying migration for change " . $I . ": " . $Migration) false; [ :parse $Migration ]; } else={ $LogPrintExit2 warning $0 ("Migration code for change " . $I . " failed syntax validation!") false; } } } } :if ($SentConfigChangesNotification != $ExpectedConfigVersion && \ $GlobalConfigVersion < $ExpectedConfigVersion) do={ :local NotificationMessage ("Current configuration on " . $Identity . \ " is out of date. Please update global-config-overlay, then increase " . \ "\$GlobalConfigVersion (currently " . $GlobalConfigVersion . \ ") to " . $ExpectedConfigVersion . " and re-run global-config-overlay."); $LogPrintExit2 info $0 ($NotificationMessage) false; :if ([ :len $GlobalConfigChanges ] > 0) do={ :set NotificationMessage ($NotificationMessage . "\n\nChanges:"); :for I from=($GlobalConfigVersion + 1) to=$ExpectedConfigVersion do={ :local Change ($GlobalConfigChanges->[ :tostr $I ]); :set NotificationMessage ($NotificationMessage . "\n " . \ [ $IfThenElse ($NotificationsWithSymbols = true) ("\E2\97\8F") "*" ] . " " . $Change); $LogPrintExit2 info $0 ("Change " . $I . ": " . $Change) false; } } else={ :set NotificationMessage ($NotificationMessage . "\n\nNews and changes are not available."); } :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://git.eworm.de/cgit/routeros-scripts/about/#donate"; } $SendNotification2 ({ origin=$0; \ subject=([ $SymbolForNotification "pushpin" ] . "News and configuration changes"); \ message=$NotificationMessage; link=$Link }); :set SentConfigChangesNotification $ExpectedConfigVersion; } :set GlobalConfigChanges; :set GlobalConfigMigration; } } # lock script against multiple invocation :set ScriptLock do={ :global LogPrintExit2; :local Script [ :tostr $1 ]; :if ([ :len [ / system script job find where script=$Script ] ] > 1) do={ $LogPrintExit2 info $0 ("Script " . $Script . " started more than once... Aborting.") true; } } # send notification via e-mail - expects at lease two string arguments :set SendEMail do={ :global SendEMail2; $SendEMail2 ({ subject=$1; message=$2; link=$3 }); } # send notification via e-mail - expects one array argument :set SendEMail2 do={ :local Notification $1; :global NotificationFunctions; ($NotificationFunctions->"e-mail") ("\$NotificationFunctions->\"e-mail\"") $Notification; } # send notification via NotificationFunctions - expects at lease two string arguments :set SendNotification do={ :global SendNotification2; $SendNotification2 ({ subject=$1; message=$2; link=$3; silent=$4 }); } # 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; } } # send notification via telegram - expects at lease two string arguments :set SendTelegram do={ :global SendTelegram2; $SendTelegram2 ({ subject=$1; message=$2; link=$3; silent=$4 }); } # send notification via telegram - expects one array argument :set SendTelegram2 do={ :local Notification $1; :global NotificationFunctions; ($NotificationFunctions->"telegram") ("\$NotificationFunctions->\"telegram\"") $Notification; } # return UTF-8 symbol for unicode name :set SymbolByUnicodeName do={ :local Symbols { "alarm-clock"="\E2\8F\B0"; "calendar"="\F0\9F\93\85"; "cloud"="\E2\98\81"; "cross-mark"="\E2\9D\8C"; "fire"="\F0\9F\94\A5"; "floppy-disk"="\F0\9F\92\BE"; "high-voltage-sign"="\E2\9A\A1"; "incoming-envelope"="\F0\9F\93\A8"; "link"="\F0\9F\94\97"; "lock-with-ink-pen"="\F0\9F\94\8F"; "mobile-phone"="\F0\9F\93\B1"; "pushpin"="\F0\9F\93\8C"; "scissors"="\E2\9C\82"; "sparkles"="\E2\9C\A8"; "up-arrow"="\E2\AC\86"; "warning-sign"="\E2\9A\A0"; "white-heavy-check-mark"="\E2\9C\85" } :return ($Symbols->$1); } # return symbol for notification :set SymbolForNotification do={ :global NotificationsWithSymbols; :global SymbolByUnicodeName; :if ($NotificationsWithSymbols != true) do={ :return ""; } :local Return ""; :foreach Symbol in=[ :toarray $1 ] do={ :set Return ($Return . [ $SymbolByUnicodeName $Symbol ]); } :return ($Return . " "); } # check if system time is sync :set TimeIsSync do={ :global LogPrintExit2; :if ([ / system ntp client get enabled ] = true) do={ :do { :if ([ / system ntp client get status ] = "synchronized") do={ :return true; } } on-error={ :if ([ :typeof [ / system ntp client get last-adjustment ] ] = "time") do={ :return true; } } :return false; } :if ([ / ip cloud get ddns-enabled ] = true && [ / ip cloud get update-time ] = true) do={ :if ([ :typeof [ / ip cloud get public-address ] ] = "ip") do={ :return true; } :return false; } $LogPrintExit2 debug $0 ("No time source configured! Returning gracefully...") false; :return true; } # 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 ]; :do { [ :parse (":local Validate do={\n" . $Code . "\n}") ]; } on-error={ :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 [ $CharacterReplace [ $CharacterReplace $Input \ "." "," ] "beta" ",beta," ] "rc" ",rc," ]; :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 = "beta") do={ :set Return ($Return + 0x3f00); } :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 DefaultRouteIsReachable; :while ([ $DefaultRouteIsReachable ] = false) do={ :delay 1s; } } # wait for DNS to resolve :set WaitDNSResolving do={ :global DNSIsResolving; :while ([ $DNSIsResolving ] = false) do={ :delay 1s; } } # wait for file to be available :set WaitForFile do={ :local FileName [ :tostr $1 ]; :global CleanFilePath; :set FileName [ $CleanFilePath $FileName ]; :local I 0; :while ([ :len [ / file find where name=$FileName ] ] = 0) do={ :if ($I > 20) do={ :return false; } :delay 100ms; :set I ($I + 1); } :return true; } # 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 LogPrintExit2; :global TimeIsSync; :while ([ $TimeIsSync ] = false) do={ :if ([ :len [ / system script find where name="rotate-ntp" ] ] > 0 && \ ([ / system resource get uptime ] % (180 * 1000000000)) = 0s) do={ :do { / system script run rotate-ntp; } on-error={ $LogPrintExit2 debug $0 ("Running rotate-ntp failed.") false; } } :delay 1s; } } # load modules :foreach Script in=[ / system script find where name ~ "^global-functions\\.d/." ] do={ / system script run $Script; } # check for required RouterOS version $RequiredRouterOS "global-functions" "6.47"; # signal we are ready :set GlobalFunctionsReady true;