#!rsc
# RouterOS script: global-functions
# Copyright (c) 2013-2019 Christian Hesse <mail@eworm.de>
#                         Michael Gisbers <michael@gisbers.de>
#
# global functions

# expected configuration version
:global ExpectedConfigVersion 10;

# global variables not to be changed by user
:global SentConfigChangesNotification "-";
:global SentRouterosUpdateNotification "-";
:global SentLteFirmwareUpgradeNotification "-";
:global Identity [ / system identity get name ];

# url encoding
:global UrlEncode do={
  :local Input [ :tostr $1 ];
  :local Return "";

  :if ([ :len $Input ] > 0) do={
    :local Chars " !\"#\$%&'()*+,-./:;<=>\?@[\\]^_`{|}~";
    :local Subs { "%20"; "%21"; "%22"; "%23"; "%24"; "%25"; "%26"; "%27"; "%28"; "%29";
                  "%2A"; "%2B"; "%2C"; "%2D"; "%2E"; "%2F"; "%3A"; "%3B"; "%3C"; "%3D";
                  "%3E"; "%3F"; "%40"; "%5B"; "%5C"; "%5D"; "%5E"; "%5F"; "%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 ([ :len $Replace ] > 0) do={
        :set Char ($Subs->$Replace);
      }
      :set Return ($Return . $Char);
    }
  }

  :return $Return;
}

# character replace
:global CharacterReplace do={
  :local String [ :tostr $1 ];
  :local ReplaceFrom [ :tostr $2 ];
  :local ReplaceWith [ :tostr $3 ];
  :local Len [ :len $ReplaceFrom ];
  :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) 999 ];
  }

  :return ($Return . $String);
}

# check and import required certificates
:global CertificateAvailable do={
  :local CommonName [ :tostr $1 ];

  :global ScriptUpdatesBaseUrl;
  :global ScriptUpdatesUrlSuffix;

  :global UrlEncode;
  :global WaitForFile;

  :if ([ / certificate print count-only where common-name=$CommonName ] = 0) do={
    :log info ("Certificate with CommonName " . $CommonName . \
      " not available, downloading and importing.");
    :do {
      :local LocalFileName ($CommonName . ".pem");
      :local UrlFileName ([ $UrlEncode $CommonName ] . ".pem");
      / tool fetch check-certificate=yes-without-crl \
        ($ScriptUpdatesBaseUrl . "certs/" . \
        $UrlFileName . $ScriptUpdatesUrlSuffix) \
        dst-path=$LocalFileName;
      $WaitForFile $LocalFileName;
      / certificate import file-name=$LocalFileName passphrase="";
      / file remove $LocalFileName;
    } on-error={
      :log warning "Failed imprting certificate!";
    }
  }
}

# send notification via e-mail
:global SendEMail do={
  :local Subject [ :tostr $1 ];
  :local Message [ :tostr $2 ];
  :local Attach  [ :tostr $3 ];

  :global Identity;
  :global EmailGeneralTo;
  :global EmailGeneralCc;

  :if ([ :len $EmailGeneralTo ] > 0) do={
    :do {
      :local Signature [ / system note get note ];
      :if ([ :len $Signature ] > 0) do={
        :set Signature ("\n-- \n" . $Signature);
      }
      / tool e-mail send to=$EmailGeneralTo cc=$EmailGeneralCc \
        subject=("[" . $Identity . "] " . $Subject) \
        body=($Message . $Signature) file=$Attach;
    } on-error={
      :log warning "Failed sending notification mail!";
    }
  }
}

# send notification via telegram
:global SendTelegram do={
  :local Subject [ :tostr $1 ];
  :local Message [ :tostr $2 ];
  :local Silent  [ :tostr $3 ];

  :global Identity;
  :global TelegramTokenId;
  :global TelegramChatId;

  :global UrlEncode;
  :global CertificateAvailable;

  :if ([ :len $TelegramTokenId ] > 0 && [ :len $TelegramChatId ] > 0) do={
    $CertificateAvailable "Go Daddy Secure Certificate Authority - G2";
    :do {
      / tool fetch check-certificate=yes-without-crl keep-result=no http-method=post \
        ("https://api.telegram.org/bot" . $TelegramTokenId . "/sendMessage") \
        http-data=("chat_id=" . $TelegramChatId . "&disable_notification=" . $Silent . \
        "&text=" . [ $UrlEncode ("[" . $Identity . "] " . $Subject . "\n\n" . $Message) ]);
    } on-error={
      :log warning "Failed sending telegram notification!";
    }
  }
}

# send notification via e-mail and telegram
# Note that attachment is ignored for telegram, silent is ignored for e-mail!
:global SendNotification do={
  :local Subject [ :tostr $1 ];
  :local Message [ :tostr $2 ];
  :local Attach  [ :tostr $3 ];
  :local Silent  [ :tostr $4 ];

  :global SendEMail;
  :global SendTelegram;

  $SendEMail $Subject $Message $Attach;
  $SendTelegram $Subject $Message $Silent;
}


# get MAC vendor
:global GetMacVendor do={
  :local Mac [ :tostr $1 ];

  :global CertificateAvailable;

  :do {
    :local Vendor;
    $CertificateAvailable "Let's Encrypt Authority X3";
    :set 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={
    :return "unknown vendor";
  }
}

# clean file path
:global 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;
}

# download package from upgrade server
:global DownloadPackage do={
  :local PkgName [ :tostr $1 ];
  :local PkgVer  [ :tostr $2 ];
  :local PkgArch [ :tostr $3 ];
  :local PkgDir  [ :tostr $4 ];

  :global CertificateAvailable;
  :global CleanFilePath;
  :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") do={
    :set PkgFile ($PkgName . "-" . $PkgVer . ".npk");
  }
  :local PkgDest [ $CleanFilePath ($PkgDir . "/" . $PkgFile) ];

  $CertificateAvailable "Let's Encrypt Authority X3";
  :do {
    / tool fetch check-certificate=yes-without-crl \
      ("https://upgrade.mikrotik.com/routeros/" . $PkgVer . "/" . $PkgFile) \
      dst-path=$PkgDest;
    $WaitForFile $PkgDest;
  } on-error={
    / file remove [ find where name=$PkgDest ];
    :return false;
  }

  :if ([ / file get [ find where name=$PkgDest ] type ] != "package") do={
    / file remove [ find where name=$PkgDest ];
    :return false;
  }

  :return true;
}

# lock script against multiple invocation
:global ScriptLock do={
  :local Script [ :tostr $1 ];

  :if ([ / system script job print count-only where script=$Script ] > 1) do={
    :log debug ("Script " . $Script . " started more than once... Aborting.");
    :error "Locked."
  }
}

# wait for file to be available
:global WaitForFile do={
  :global CleanFilePath;

  :local FileName [ $CleanFilePath [ :tostr $1 ] ];
  :local I 0;

  :while ([ file print count-only where name=$FileName ] = 0) do={
    :if ($I > 20) do={
      :return false;
    }
    :delay 100ms;
    :set I ($I + 1);
  }
  :return true;
}

# parse key value store
:global ParseKeyValueStore do={
  :global CharacterReplace;

  :local Source $1;
  :if ([ :typeof $Source ] != "array") do={
    :set Source [ :tostr $1 ];
  }
  :local Result [ :toarray "" ];
  :foreach KeyValue in=[ :toarray $Source ] do={
    :set KeyValue [ :toarray [ $CharacterReplace $KeyValue "=" "," ] ];
    :set ($Result->($KeyValue->0)) ($KeyValue->1);
  }
  :return $Result;
}

# generate random number
# Warning: This is a *very* weak algorithm and in *no way*
# useful for cryptography or similar!
:global GetRandom do={
  :local Max ([ :tonum $1 ] + 1);
  :local Sum 0;

  :foreach Interface in=[ /interface find ] do={
    :set Sum ($Sum + [ /interface get $Interface tx-byte ]);
  }
  :return ($Sum % $Max);
}

# delay a random amount of seconds
:global RandomDelay do={
  :global GetRandom;

  :delay ([ $GetRandom $1 ] . "s");
}

# get readable device info
:global DeviceInfo do={
  :global ExpectedConfigVersion;
  :global GlobalConfigVersion;
  :global Identity;

  :local Resource [ / system resource get ];
  :local RouterBoard [ / system routerboard get ];
  :local Update [ / system package update get ];

  :local Info ( \
    "Hostname:       " . $Identity . "\n" . \
    "Board name:     " . $Resource->"board-name" . "\n" . \
    "Architecture:   " . $Resource->"architecture-name");
  :if ($RouterBoard->"routerboard" = true) do={
    :set Info ($Info . "\n" . \
      "Model:          " . $RouterBoard->"model" . "\n" . \
      "Serial number:  " . $RouterBoard->"serial-number");
  }
  :set Info ($Info . "\n" . \
    "RouterOS:\n" . \
    "  Channel:      " . $Update->"channel" . "\n" . \
    "  Installed:    " . $Update->"installed-version");
  :if ([ :typeof ($Update->"latest-version") ] != "nothing" && \
       $Update->"installed-version" != $Update->"latest-version") do={
    :set Info ($Info . "\n" . \
      "  Available:    " . $Update->"latest-version");
  }
  :set Info ($Info . "\n" . \
      "RouterOS-Scripts Configuration Version:\n" . \
      "  Current:      " . $GlobalConfigVersion);
  :if ($GlobalConfigVersion != $ExpectedConfigVersion) do={
    :set Info ($Info . "\n" . \
      "  Expected:     " . $ExpectedConfigVersion);
  }

  :return $Info;
}