aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--.gitignore10
-rw-r--r--BRANCHES.md50
-rw-r--r--CONTRIBUTIONS.md49
-rw-r--r--COPYING.md675
-rw-r--r--INITIAL-COMMANDS.md55
-rw-r--r--Makefile33
-rw-r--r--README.d/01-download-certs.avifbin0 -> 4420 bytes
-rw-r--r--README.d/02-import-certs.avifbin0 -> 3606 bytes
-rw-r--r--README.d/03-check-certs.avifbin0 -> 12089 bytes
-rw-r--r--README.d/04-import-scripts.avifbin0 -> 3782 bytes
-rw-r--r--README.d/05-run-and-schedule-scripts.avifbin0 -> 1946 bytes
-rw-r--r--README.d/06-schedule-update.avifbin0 -> 2200 bytes
-rw-r--r--README.d/07-edit-global-config-overlay.avifbin0 -> 5103 bytes
-rw-r--r--README.d/08-apply-configuration.avifbin0 -> 2437 bytes
-rw-r--r--README.d/09-update-scripts.avifbin0 -> 1733 bytes
-rw-r--r--README.d/10-install-scripts.avifbin0 -> 2423 bytes
-rw-r--r--README.d/11-schedule-script.avifbin0 -> 4113 bytes
-rw-r--r--README.d/12-setup-lease-script.avifbin0 -> 7642 bytes
-rw-r--r--README.d/13-install-custom-script.avifbin0 -> 4589 bytes
-rw-r--r--README.d/14-remove-script.avifbin0 -> 1093 bytes
-rw-r--r--README.d/hello-world.rsc3
-rw-r--r--README.d/notification-news-and-changes.avifbin0 -> 14861 bytes
-rw-r--r--README.d/telegram-group.avifbin0 -> 891 bytes
-rw-r--r--README.d/upstream.pngbin0 -> 207 bytes
-rw-r--r--README.md330
-rw-r--r--accesslist-duplicates.capsman34
-rw-r--r--accesslist-duplicates.capsman.rsc34
-rw-r--r--accesslist-duplicates.local34
-rw-r--r--accesslist-duplicates.local.rsc34
-rw-r--r--accesslist-duplicates.template35
-rw-r--r--accesslist-duplicates.template.rsc43
-rw-r--r--accesslist-duplicates.wifi.rsc34
-rw-r--r--backup-cloud.rsc86
-rw-r--r--backup-email.rsc124
-rw-r--r--backup-partition.rsc57
-rw-r--r--backup-upload.rsc161
-rw-r--r--bridge-port-to-default42
-rw-r--r--bridge-port-toggle15
-rw-r--r--capsman-download-packages39
-rw-r--r--capsman-download-packages.capsman.rsc83
-rw-r--r--capsman-download-packages.template.rsc94
-rw-r--r--capsman-download-packages.wifi.rsc85
-rw-r--r--capsman-rolling-upgrade25
-rw-r--r--capsman-rolling-upgrade.capsman.rsc46
-rw-r--r--capsman-rolling-upgrade.template.rsc54
-rw-r--r--capsman-rolling-upgrade.wifi.rsc47
-rw-r--r--certificate-renew-issued14
-rw-r--r--certificate-renew-issued.rsc48
-rw-r--r--certs/Cloudflare-Inc-ECC-CA-3.pem163
-rw-r--r--certs/DigiCert-Global-G2-TLS-RSA-SHA256-2020-CA1.pem182
-rw-r--r--certs/DigiCert-TLS-Hybrid-ECC-SHA384-2020-CA1.pem174
-rw-r--r--certs/E1.pem124
-rw-r--r--certs/GTS-CA-1C3.pem242
-rw-r--r--certs/GTS-CA-1P5.pem238
-rw-r--r--certs/GlobalSign-Atlas-R3-DV-TLS-CA-2022-Q3.pem177
-rw-r--r--certs/Go Daddy Secure Certificate Authority - G2.pem51
-rw-r--r--certs/Go-Daddy-Secure-Certificate-Authority-G2.pem178
-rw-r--r--certs/Let's Encrypt Authority X3.pem83
-rw-r--r--certs/R3.pem237
-rw-r--r--certs/Starfield Secure Certificate Authority - G2.pem52
-rw-r--r--certs/Starfield-Secure-Certificate-Authority-G2.pem179
-rw-r--r--check-certificates104
-rw-r--r--check-certificates.rsc222
-rw-r--r--check-health72
-rw-r--r--check-health.rsc178
-rw-r--r--check-lte-firmware-upgrade32
-rw-r--r--check-lte-firmware-upgrade.rsc103
-rw-r--r--check-routeros-update84
-rw-r--r--check-routeros-update.rsc166
-rw-r--r--cloud-backup34
-rw-r--r--collect-wireless-mac.capsman66
-rw-r--r--collect-wireless-mac.capsman.rsc96
-rw-r--r--collect-wireless-mac.local66
-rw-r--r--collect-wireless-mac.local.rsc97
-rw-r--r--collect-wireless-mac.template68
-rw-r--r--collect-wireless-mac.template.rsc114
-rw-r--r--collect-wireless-mac.wifi.rsc96
-rw-r--r--contrib/logo-color.d/browser-01.avifbin0 -> 42058 bytes
-rw-r--r--contrib/logo-color.d/browser-02.avifbin0 -> 29025 bytes
-rw-r--r--contrib/logo-color.d/browser-03.avifbin0 -> 26034 bytes
-rw-r--r--contrib/logo-color.d/script.js12
-rw-r--r--contrib/logo-color.d/style.css5
-rw-r--r--contrib/logo-color.html40
-rw-r--r--contrib/notification.d/script.js6
-rw-r--r--contrib/notification.d/style.css36
-rw-r--r--contrib/notification.html35
-rw-r--r--daily-psk-schedule28
-rw-r--r--daily-psk.capsman95
-rw-r--r--daily-psk.capsman.rsc92
-rw-r--r--daily-psk.local95
-rw-r--r--daily-psk.local.rsc91
-rw-r--r--daily-psk.template101
-rw-r--r--daily-psk.template.rsc107
-rw-r--r--daily-psk.wifi.rsc92
-rw-r--r--dhcp-lease-comment.capsman20
-rw-r--r--dhcp-lease-comment.capsman.rsc39
-rw-r--r--dhcp-lease-comment.local20
-rw-r--r--dhcp-lease-comment.local.rsc39
-rw-r--r--dhcp-lease-comment.template21
-rw-r--r--dhcp-lease-comment.template.rsc44
-rw-r--r--dhcp-lease-comment.wifi.rsc39
-rw-r--r--dhcp-to-dns66
-rw-r--r--dhcp-to-dns.rsc126
-rw-r--r--doc/accesslist-duplicates.d/01-example.avifbin0 -> 5208 bytes
-rw-r--r--doc/accesslist-duplicates.md57
-rw-r--r--doc/backup-cloud.d/notification.avifbin0 -> 11629 bytes
-rw-r--r--doc/backup-cloud.md76
-rw-r--r--doc/backup-email.md68
-rw-r--r--doc/backup-partition.md61
-rw-r--r--doc/backup-upload.d/notification.avifbin0 -> 11776 bytes
-rw-r--r--doc/backup-upload.md92
-rw-r--r--doc/capsman-download-packages.md83
-rw-r--r--doc/capsman-rolling-upgrade.md60
-rw-r--r--doc/certificate-renew-issued.md61
-rw-r--r--doc/check-certificates.d/notification.avifbin0 -> 25274 bytes
-rw-r--r--doc/check-certificates.md97
-rw-r--r--doc/check-health.d/notification-01-cpu-utilization-high.avifbin0 -> 6481 bytes
-rw-r--r--doc/check-health.d/notification-02-cpu-utilization-ok.avifbin0 -> 6797 bytes
-rw-r--r--doc/check-health.d/notification-03-ram-utilization-high.avifbin0 -> 7527 bytes
-rw-r--r--doc/check-health.d/notification-04-ram-utilization-ok.avifbin0 -> 6637 bytes
-rw-r--r--doc/check-health.d/notification-05-voltage.avifbin0 -> 3829 bytes
-rw-r--r--doc/check-health.d/notification-06-temperature-high.avifbin0 -> 3519 bytes
-rw-r--r--doc/check-health.d/notification-07-temperature-ok.avifbin0 -> 3727 bytes
-rw-r--r--doc/check-health.d/notification-08-psu-fail.avifbin0 -> 3474 bytes
-rw-r--r--doc/check-health.d/notification-09-psu-ok.avifbin0 -> 3531 bytes
-rw-r--r--doc/check-health.md96
-rw-r--r--doc/check-lte-firmware-upgrade.d/notification.avifbin0 -> 5077 bytes
-rw-r--r--doc/check-lte-firmware-upgrade.md58
-rw-r--r--doc/check-routeros-update.d/notification.avifbin0 -> 6392 bytes
-rw-r--r--doc/check-routeros-update.md107
-rw-r--r--doc/cloud-backup.md1
-rw-r--r--doc/collect-wireless-mac.d/notification.avifbin0 -> 13378 bytes
-rw-r--r--doc/collect-wireless-mac.md77
-rw-r--r--doc/daily-psk.d/notification.avifbin0 -> 7040 bytes
-rw-r--r--doc/daily-psk.md88
-rw-r--r--doc/dhcp-lease-comment.md64
-rw-r--r--doc/dhcp-to-dns.md93
-rw-r--r--doc/early-errors.md2
-rw-r--r--doc/email-backup.md1
-rw-r--r--doc/firmware-upgrade-reboot.md43
-rw-r--r--doc/fw-addr-lists.md132
-rw-r--r--doc/global-wait.md47
-rw-r--r--doc/gps-track.md51
-rw-r--r--doc/hotspot-to-wpa.md124
-rw-r--r--doc/ip-addr-bridge.md39
-rw-r--r--doc/ipsec-to-dns.md57
-rw-r--r--doc/ipv6-update.md80
-rw-r--r--doc/lease-script.md54
-rw-r--r--doc/leds-mode.md57
-rw-r--r--doc/log-forward.d/notification.avifbin0 -> 6178 bytes
-rw-r--r--doc/log-forward.md95
-rw-r--r--doc/mod/bridge-port-to.md88
-rw-r--r--doc/mod/bridge-port-vlan.md92
-rw-r--r--doc/mod/inspectvar.d/inspectvar.avifbin0 -> 2838 bytes
-rw-r--r--doc/mod/inspectvar.md40
-rw-r--r--doc/mod/ipcalc.d/ipcalc.avifbin0 -> 1729 bytes
-rw-r--r--doc/mod/ipcalc.d/ipcalcreturn.avifbin0 -> 1247 bytes
-rw-r--r--doc/mod/ipcalc.md60
-rw-r--r--doc/mod/notification-email.md88
-rw-r--r--doc/mod/notification-matrix.d/01-authenticate.avifbin0 -> 3870 bytes
-rw-r--r--doc/mod/notification-matrix.d/02-join-room.avifbin0 -> 3745 bytes
-rw-r--r--doc/mod/notification-matrix.md132
-rw-r--r--doc/mod/notification-ntfy.md91
-rw-r--r--doc/mod/notification-telegram.d/newbot.avifbin0 -> 35870 bytes
-rw-r--r--doc/mod/notification-telegram.d/setuserpic.avifbin0 -> 38522 bytes
-rw-r--r--doc/mod/notification-telegram.md112
-rw-r--r--doc/mod/scriptrunonce.d/hello-world.rsc3
-rw-r--r--doc/mod/scriptrunonce.d/scriptrunonce.avifbin0 -> 2356 bytes
-rw-r--r--doc/mod/scriptrunonce.md59
-rw-r--r--doc/mod/ssh-keys-import.md73
-rw-r--r--doc/mode-button.md75
-rw-r--r--doc/netwatch-dns.md94
-rw-r--r--doc/netwatch-notify.d/notification-01-down.avifbin0 -> 4193 bytes
-rw-r--r--doc/netwatch-notify.d/notification-02-up.avifbin0 -> 4744 bytes
-rw-r--r--doc/netwatch-notify.md189
-rw-r--r--doc/netwatch-syslog.md5
-rw-r--r--doc/ospf-to-leds.md44
-rw-r--r--doc/packages-update.md78
-rw-r--r--doc/ppp-on-up.md44
-rw-r--r--doc/rotate-ntp.md3
-rw-r--r--doc/sms-action.md63
-rw-r--r--doc/sms-forward.d/notification.avifbin0 -> 4619 bytes
-rw-r--r--doc/sms-forward.md98
-rw-r--r--doc/ssh-keys-import.md2
-rw-r--r--doc/super-mario-theme.md38
-rw-r--r--doc/telegram-chat.d/01-chat-specific.avifbin0 -> 38468 bytes
-rw-r--r--doc/telegram-chat.d/02-chat-all.avifbin0 -> 54713 bytes
-rw-r--r--doc/telegram-chat.d/03-reply.avifbin0 -> 50126 bytes
-rw-r--r--doc/telegram-chat.md152
-rw-r--r--doc/unattended-lte-firmware-upgrade.md54
-rw-r--r--doc/update-gre-address.md48
-rw-r--r--doc/update-tunnelbroker.md50
-rw-r--r--doc/upload-backup.md1
-rw-r--r--email-backup53
-rw-r--r--firmware-upgrade-reboot.rsc54
-rw-r--r--fw-addr-lists.d/allow3
-rw-r--r--fw-addr-lists.d/block5
-rw-r--r--fw-addr-lists.d/mikrotik5
-rw-r--r--fw-addr-lists.rsc183
-rw-r--r--global-config127
-rw-r--r--global-config-overlay18
-rw-r--r--global-config-overlay.rsc12
-rw-r--r--global-config.changes16
-rw-r--r--global-config.rsc262
-rw-r--r--global-functions396
-rw-r--r--global-functions.rsc1576
-rw-r--r--global-wait.rsc12
-rw-r--r--gps-track27
-rw-r--r--gps-track.rsc48
-rw-r--r--hotspot-to-wpa29
-rw-r--r--hotspot-to-wpa-cleanup.capsman.rsc74
-rw-r--r--hotspot-to-wpa-cleanup.template.rsc81
-rw-r--r--hotspot-to-wpa-cleanup.wifi.rsc74
-rw-r--r--hotspot-to-wpa.capsman.rsc98
-rw-r--r--hotspot-to-wpa.template.rsc118
-rw-r--r--hotspot-to-wpa.wifi.rsc95
-rw-r--r--initial-commands23
-rw-r--r--ip-addr-bridge16
-rw-r--r--ip-addr-bridge.rsc18
-rw-r--r--ipsec-to-dns.rsc79
-rw-r--r--ipv6-update38
-rw-r--r--ipv6-update.rsc87
-rw-r--r--learn-mac-based-vlan12
-rw-r--r--lease-script46
-rw-r--r--lease-script.rsc59
-rw-r--r--leds-day-mode7
-rw-r--r--leds-day-mode.rsc9
-rw-r--r--leds-night-mode7
-rw-r--r--leds-night-mode.rsc9
-rw-r--r--leds-toggle-mode11
-rw-r--r--leds-toggle-mode.rsc13
-rw-r--r--log-forward.rsc102
-rw-r--r--logo.avifbin0 -> 2001 bytes
-rw-r--r--logo.pngbin0 -> 4428 bytes
-rw-r--r--logo.svg29
-rw-r--r--manage-umts28
-rw-r--r--mod/bridge-port-to.rsc68
-rw-r--r--mod/bridge-port-vlan.rsc77
-rw-r--r--mod/inspectvar.rsc59
-rw-r--r--mod/ipcalc.rsc52
-rw-r--r--mod/notification-email.rsc240
-rw-r--r--mod/notification-matrix.rsc266
-rw-r--r--mod/notification-ntfy.rsc148
-rw-r--r--mod/notification-telegram.rsc196
-rw-r--r--mod/scriptrunonce.rsc52
-rw-r--r--mod/ssh-keys-import.rsc114
-rw-r--r--mode-button-event20
-rw-r--r--mode-button-scheduler31
-rw-r--r--mode-button.rsc81
-rw-r--r--netwatch-dns.rsc130
-rw-r--r--netwatch-notify.rsc220
-rw-r--r--netwatch-syslog15
-rw-r--r--news-and-changes.rsc63
-rw-r--r--ospf-to-leds.rsc45
-rw-r--r--packages-update59
-rw-r--r--packages-update.rsc145
-rw-r--r--ppp-on-up28
-rw-r--r--ppp-on-up.rsc40
-rw-r--r--rotate-ntp17
-rw-r--r--script-updates130
-rw-r--r--sms-action21
-rw-r--r--sms-action.rsc37
-rw-r--r--sms-forward47
-rw-r--r--sms-forward.rsc95
-rw-r--r--ssh-keys-import11
-rw-r--r--super-mario-theme.rsc (renamed from super-mario-theme)6
-rw-r--r--telegram-chat.rsc178
-rw-r--r--template.md47
-rw-r--r--unattended-lte-firmware-upgrade28
-rw-r--r--unattended-lte-firmware-upgrade.rsc49
-rw-r--r--update-gre-address23
-rw-r--r--update-gre-address.rsc42
-rw-r--r--update-tunnelbroker36
-rw-r--r--update-tunnelbroker.rsc67
-rw-r--r--upload-backup64
275 files changed, 15399 insertions, 2904 deletions
diff --git a/.gitignore b/.gitignore
index f29d4e8..cf89f87 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,3 +1,13 @@
+# backup and temporary files
*~
+
+# patches and related files
+*.orig
*.patch
+*.rej
+
+# html files (as generated from markdown)
*.html
+
+# Mac OS X folder settings file
+.DS_Store
diff --git a/BRANCHES.md b/BRANCHES.md
new file mode 100644
index 0000000..f1062bb
--- /dev/null
+++ b/BRANCHES.md
@@ -0,0 +1,50 @@
+Installing from branches
+========================
+
+[![GitHub stars](https://img.shields.io/github/stars/eworm-de/routeros-scripts?logo=GitHub&style=flat&color=red)](https://github.com/eworm-de/routeros-scripts/stargazers)
+[![GitHub forks](https://img.shields.io/github/forks/eworm-de/routeros-scripts?logo=GitHub&style=flat&color=green)](https://github.com/eworm-de/routeros-scripts/network)
+[![GitHub watchers](https://img.shields.io/github/watchers/eworm-de/routeros-scripts?logo=GitHub&style=flat&color=blue)](https://github.com/eworm-de/routeros-scripts/watchers)
+[![required RouterOS version](https://img.shields.io/badge/RouterOS-7.13-yellow?style=flat)](https://mikrotik.com/download/changelogs/)
+[![Telegram group @routeros_scripts](https://img.shields.io/badge/Telegram-%40routeros__scripts-%2326A5E4?logo=telegram&style=flat)](https://t.me/routeros_scripts)
+[![donate with PayPal](https://img.shields.io/badge/Like_it%3F-Donate!-orange?logo=githubsponsors&logoColor=orange&style=flat)](https://www.paypal.com/cgi-bin/webscr?cmd=_s-xclick&hosted_button_id=A4ZXBD6YS2W8J)
+
+[⬅️ Go back to main README](README.md)
+
+> ⚠️ **Warning**: Living on the edge? Great, read on!
+> If not: Please use the `main` branch and leave this page!
+
+These scripts are developed in a [git](https://git-scm.com/) repository.
+Development and experimental branches are used to provide early access
+for specific changes. You can install scripts from these branches
+for testing.
+
+## Install single script
+
+To install a single script from `next` branch:
+
+ $ScriptInstallUpdate script-name "url-suffix=?h=next";
+
+## Switch existing script
+
+Alternatively switch an existing script to update from `next` branch:
+
+ /system/script/set comment="url-suffix=?h=next" script-name;
+ $ScriptInstallUpdate;
+
+## Switch installation
+
+Last but not least - to switch the complete installation to the `next`
+branch edit `global-config-overlay` and add:
+
+ :global ScriptUpdatesUrlSuffix "?h=next";
+
+... then reload the configuration and update:
+
+ /system/script/run global-config;
+ $ScriptInstallUpdate;
+
+> ℹ️ **Info**: Replace `next` with *whatever* to use another specific branch.
+
+---
+[⬅️ Go back to main README](README.md)
+[⬆️ Go back to top](#top)
diff --git a/CONTRIBUTIONS.md b/CONTRIBUTIONS.md
index 0c63f8e..13d0508 100644
--- a/CONTRIBUTIONS.md
+++ b/CONTRIBUTIONS.md
@@ -1,20 +1,59 @@
Past Contributions
==================
-Thanks a lot for your contributions!
+[![GitHub stars](https://img.shields.io/github/stars/eworm-de/routeros-scripts?logo=GitHub&style=flat&color=red)](https://github.com/eworm-de/routeros-scripts/stargazers)
+[![GitHub forks](https://img.shields.io/github/forks/eworm-de/routeros-scripts?logo=GitHub&style=flat&color=green)](https://github.com/eworm-de/routeros-scripts/network)
+[![GitHub watchers](https://img.shields.io/github/watchers/eworm-de/routeros-scripts?logo=GitHub&style=flat&color=blue)](https://github.com/eworm-de/routeros-scripts/watchers)
+[![required RouterOS version](https://img.shields.io/badge/RouterOS-7.13-yellow?style=flat)](https://mikrotik.com/download/changelogs/)
+[![Telegram group @routeros_scripts](https://img.shields.io/badge/Telegram-%40routeros__scripts-%2326A5E4?logo=telegram&style=flat)](https://t.me/routeros_scripts)
+[![donate with PayPal](https://img.shields.io/badge/Like_it%3F-Donate!-orange?logo=githubsponsors&logoColor=orange&style=flat)](https://www.paypal.com/cgi-bin/webscr?cmd=_s-xclick&hosted_button_id=A4ZXBD6YS2W8J)
+
+[⬅️ Go back to main README](README.md)
+
+Thanks a lot for your contributions! ❤️
## Patches
-These persons contributed code. See the git history for details!
+These persons contributed code or documentation. See the git history
+for details!
-* [Michael Gisbers](mailto:michael@gisbers.de)
+* [Anatoly Bubenkov](mailto:bubenkoff@gmail.com) (@bubenkoff)
+* [Ben Harris](mailto:mail@bharr.is) (@bharrisau)
+* [Daniel Ziegenberg](mailto:daniel@ziegenberg.at) (@ziegenberg)
+* [Michael Gisbers](mailto:michael@gisbers.de) (@mgisbers)
+* @netravnen
+* [netztrip](mailto:dave-tvg@netztrip.de) (@netztrip)
+* [Stefan Müller](mailto:stefan.mueller.83@gmail.com) (@PackElend)
## Donations
Add yourself to the list,
[donate with PayPal](https://www.paypal.com/cgi-bin/webscr?cmd=_s-xclick&hosted_button_id=A4ZXBD6YS2W8J)!
-* Reiner Vehrenkamp
-* Linux-Schmie.de Michael Gisbers
+* Abdul Mannan Abbasi
+* Andrea Ruffini Perico
+* Andrew Cox
* Christoph Boss (@Kampfwurst)
+* Daniel Ziegenberg (@ziegenberg)
+* Devin Dean (@dd2594gh)
+* Evaldo Gardenal
+* Giorgio Bikos
+* Harold Schoemaker
+* Hugo BV
* Klaus Michael Rübsam
+* Leonardo Valeri Manera
+* Linux-Schmie.de Michael Gisbers
+* Manuel Kuhn
+* Marek Čábák
+* Oleksandr Yukhymchuk
+* Peter Holtkamp
+* Reiner Vehrenkamp
+* Richard Österreicher
+* Simon Hitzemann
+* Sunny Chu (@sunnychuchu)
+* Ulrich Wessendorf
+* Zac Kornilakis
+
+---
+[⬅️ Go back to main README](README.md)
+[⬆️ Go back to top](#top)
diff --git a/COPYING.md b/COPYING.md
new file mode 100644
index 0000000..2fb2e74
--- /dev/null
+++ b/COPYING.md
@@ -0,0 +1,675 @@
+### GNU GENERAL PUBLIC LICENSE
+
+Version 3, 29 June 2007
+
+Copyright (C) 2007 Free Software Foundation, Inc.
+<https://fsf.org/>
+
+Everyone is permitted to copy and distribute verbatim copies of this
+license document, but changing it is not allowed.
+
+### Preamble
+
+The GNU General Public License is a free, copyleft license for
+software and other kinds of works.
+
+The licenses for most software and other practical works are designed
+to take away your freedom to share and change the works. By contrast,
+the GNU General Public License is intended to guarantee your freedom
+to share and change all versions of a program--to make sure it remains
+free software for all its users. We, the Free Software Foundation, use
+the GNU General Public License for most of our software; it applies
+also to any other work released this way by its authors. You can apply
+it to your programs, too.
+
+When we speak of free software, we are referring to freedom, not
+price. Our General Public Licenses are designed to make sure that you
+have the freedom to distribute copies of free software (and charge for
+them if you wish), that you receive source code or can get it if you
+want it, that you can change the software or use pieces of it in new
+free programs, and that you know you can do these things.
+
+To protect your rights, we need to prevent others from denying you
+these rights or asking you to surrender the rights. Therefore, you
+have certain responsibilities if you distribute copies of the
+software, or if you modify it: responsibilities to respect the freedom
+of others.
+
+For example, if you distribute copies of such a program, whether
+gratis or for a fee, you must pass on to the recipients the same
+freedoms that you received. You must make sure that they, too, receive
+or can get the source code. And you must show them these terms so they
+know their rights.
+
+Developers that use the GNU GPL protect your rights with two steps:
+(1) assert copyright on the software, and (2) offer you this License
+giving you legal permission to copy, distribute and/or modify it.
+
+For the developers' and authors' protection, the GPL clearly explains
+that there is no warranty for this free software. For both users' and
+authors' sake, the GPL requires that modified versions be marked as
+changed, so that their problems will not be attributed erroneously to
+authors of previous versions.
+
+Some devices are designed to deny users access to install or run
+modified versions of the software inside them, although the
+manufacturer can do so. This is fundamentally incompatible with the
+aim of protecting users' freedom to change the software. The
+systematic pattern of such abuse occurs in the area of products for
+individuals to use, which is precisely where it is most unacceptable.
+Therefore, we have designed this version of the GPL to prohibit the
+practice for those products. If such problems arise substantially in
+other domains, we stand ready to extend this provision to those
+domains in future versions of the GPL, as needed to protect the
+freedom of users.
+
+Finally, every program is threatened constantly by software patents.
+States should not allow patents to restrict development and use of
+software on general-purpose computers, but in those that do, we wish
+to avoid the special danger that patents applied to a free program
+could make it effectively proprietary. To prevent this, the GPL
+assures that patents cannot be used to render the program non-free.
+
+The precise terms and conditions for copying, distribution and
+modification follow.
+
+### TERMS AND CONDITIONS
+
+#### 0. Definitions.
+
+"This License" refers to version 3 of the GNU General Public License.
+
+"Copyright" also means copyright-like laws that apply to other kinds
+of works, such as semiconductor masks.
+
+"The Program" refers to any copyrightable work licensed under this
+License. Each licensee is addressed as "you". "Licensees" and
+"recipients" may be individuals or organizations.
+
+To "modify" a work means to copy from or adapt all or part of the work
+in a fashion requiring copyright permission, other than the making of
+an exact copy. The resulting work is called a "modified version" of
+the earlier work or a work "based on" the earlier work.
+
+A "covered work" means either the unmodified Program or a work based
+on the Program.
+
+To "propagate" a work means to do anything with it that, without
+permission, would make you directly or secondarily liable for
+infringement under applicable copyright law, except executing it on a
+computer or modifying a private copy. Propagation includes copying,
+distribution (with or without modification), making available to the
+public, and in some countries other activities as well.
+
+To "convey" a work means any kind of propagation that enables other
+parties to make or receive copies. Mere interaction with a user
+through a computer network, with no transfer of a copy, is not
+conveying.
+
+An interactive user interface displays "Appropriate Legal Notices" to
+the extent that it includes a convenient and prominently visible
+feature that (1) displays an appropriate copyright notice, and (2)
+tells the user that there is no warranty for the work (except to the
+extent that warranties are provided), that licensees may convey the
+work under this License, and how to view a copy of this License. If
+the interface presents a list of user commands or options, such as a
+menu, a prominent item in the list meets this criterion.
+
+#### 1. Source Code.
+
+The "source code" for a work means the preferred form of the work for
+making modifications to it. "Object code" means any non-source form of
+a work.
+
+A "Standard Interface" means an interface that either is an official
+standard defined by a recognized standards body, or, in the case of
+interfaces specified for a particular programming language, one that
+is widely used among developers working in that language.
+
+The "System Libraries" of an executable work include anything, other
+than the work as a whole, that (a) is included in the normal form of
+packaging a Major Component, but which is not part of that Major
+Component, and (b) serves only to enable use of the work with that
+Major Component, or to implement a Standard Interface for which an
+implementation is available to the public in source code form. A
+"Major Component", in this context, means a major essential component
+(kernel, window system, and so on) of the specific operating system
+(if any) on which the executable work runs, or a compiler used to
+produce the work, or an object code interpreter used to run it.
+
+The "Corresponding Source" for a work in object code form means all
+the source code needed to generate, install, and (for an executable
+work) run the object code and to modify the work, including scripts to
+control those activities. However, it does not include the work's
+System Libraries, or general-purpose tools or generally available free
+programs which are used unmodified in performing those activities but
+which are not part of the work. For example, Corresponding Source
+includes interface definition files associated with source files for
+the work, and the source code for shared libraries and dynamically
+linked subprograms that the work is specifically designed to require,
+such as by intimate data communication or control flow between those
+subprograms and other parts of the work.
+
+The Corresponding Source need not include anything that users can
+regenerate automatically from other parts of the Corresponding Source.
+
+The Corresponding Source for a work in source code form is that same
+work.
+
+#### 2. Basic Permissions.
+
+All rights granted under this License are granted for the term of
+copyright on the Program, and are irrevocable provided the stated
+conditions are met. This License explicitly affirms your unlimited
+permission to run the unmodified Program. The output from running a
+covered work is covered by this License only if the output, given its
+content, constitutes a covered work. This License acknowledges your
+rights of fair use or other equivalent, as provided by copyright law.
+
+You may make, run and propagate covered works that you do not convey,
+without conditions so long as your license otherwise remains in force.
+You may convey covered works to others for the sole purpose of having
+them make modifications exclusively for you, or provide you with
+facilities for running those works, provided that you comply with the
+terms of this License in conveying all material for which you do not
+control copyright. Those thus making or running the covered works for
+you must do so exclusively on your behalf, under your direction and
+control, on terms that prohibit them from making any copies of your
+copyrighted material outside their relationship with you.
+
+Conveying under any other circumstances is permitted solely under the
+conditions stated below. Sublicensing is not allowed; section 10 makes
+it unnecessary.
+
+#### 3. Protecting Users' Legal Rights From Anti-Circumvention Law.
+
+No covered work shall be deemed part of an effective technological
+measure under any applicable law fulfilling obligations under article
+11 of the WIPO copyright treaty adopted on 20 December 1996, or
+similar laws prohibiting or restricting circumvention of such
+measures.
+
+When you convey a covered work, you waive any legal power to forbid
+circumvention of technological measures to the extent such
+circumvention is effected by exercising rights under this License with
+respect to the covered work, and you disclaim any intention to limit
+operation or modification of the work as a means of enforcing, against
+the work's users, your or third parties' legal rights to forbid
+circumvention of technological measures.
+
+#### 4. Conveying Verbatim Copies.
+
+You may convey verbatim copies of the Program's source code as you
+receive it, in any medium, provided that you conspicuously and
+appropriately publish on each copy an appropriate copyright notice;
+keep intact all notices stating that this License and any
+non-permissive terms added in accord with section 7 apply to the code;
+keep intact all notices of the absence of any warranty; and give all
+recipients a copy of this License along with the Program.
+
+You may charge any price or no price for each copy that you convey,
+and you may offer support or warranty protection for a fee.
+
+#### 5. Conveying Modified Source Versions.
+
+You may convey a work based on the Program, or the modifications to
+produce it from the Program, in the form of source code under the
+terms of section 4, provided that you also meet all of these
+conditions:
+
+- a) The work must carry prominent notices stating that you modified
+ it, and giving a relevant date.
+- b) The work must carry prominent notices stating that it is
+ released under this License and any conditions added under
+ section 7. This requirement modifies the requirement in section 4
+ to "keep intact all notices".
+- c) You must license the entire work, as a whole, under this
+ License to anyone who comes into possession of a copy. This
+ License will therefore apply, along with any applicable section 7
+ additional terms, to the whole of the work, and all its parts,
+ regardless of how they are packaged. This License gives no
+ permission to license the work in any other way, but it does not
+ invalidate such permission if you have separately received it.
+- d) If the work has interactive user interfaces, each must display
+ Appropriate Legal Notices; however, if the Program has interactive
+ interfaces that do not display Appropriate Legal Notices, your
+ work need not make them do so.
+
+A compilation of a covered work with other separate and independent
+works, which are not by their nature extensions of the covered work,
+and which are not combined with it such as to form a larger program,
+in or on a volume of a storage or distribution medium, is called an
+"aggregate" if the compilation and its resulting copyright are not
+used to limit the access or legal rights of the compilation's users
+beyond what the individual works permit. Inclusion of a covered work
+in an aggregate does not cause this License to apply to the other
+parts of the aggregate.
+
+#### 6. Conveying Non-Source Forms.
+
+You may convey a covered work in object code form under the terms of
+sections 4 and 5, provided that you also convey the machine-readable
+Corresponding Source under the terms of this License, in one of these
+ways:
+
+- a) Convey the object code in, or embodied in, a physical product
+ (including a physical distribution medium), accompanied by the
+ Corresponding Source fixed on a durable physical medium
+ customarily used for software interchange.
+- b) Convey the object code in, or embodied in, a physical product
+ (including a physical distribution medium), accompanied by a
+ written offer, valid for at least three years and valid for as
+ long as you offer spare parts or customer support for that product
+ model, to give anyone who possesses the object code either (1) a
+ copy of the Corresponding Source for all the software in the
+ product that is covered by this License, on a durable physical
+ medium customarily used for software interchange, for a price no
+ more than your reasonable cost of physically performing this
+ conveying of source, or (2) access to copy the Corresponding
+ Source from a network server at no charge.
+- c) Convey individual copies of the object code with a copy of the
+ written offer to provide the Corresponding Source. This
+ alternative is allowed only occasionally and noncommercially, and
+ only if you received the object code with such an offer, in accord
+ with subsection 6b.
+- d) Convey the object code by offering access from a designated
+ place (gratis or for a charge), and offer equivalent access to the
+ Corresponding Source in the same way through the same place at no
+ further charge. You need not require recipients to copy the
+ Corresponding Source along with the object code. If the place to
+ copy the object code is a network server, the Corresponding Source
+ may be on a different server (operated by you or a third party)
+ that supports equivalent copying facilities, provided you maintain
+ clear directions next to the object code saying where to find the
+ Corresponding Source. Regardless of what server hosts the
+ Corresponding Source, you remain obligated to ensure that it is
+ available for as long as needed to satisfy these requirements.
+- e) Convey the object code using peer-to-peer transmission,
+ provided you inform other peers where the object code and
+ Corresponding Source of the work are being offered to the general
+ public at no charge under subsection 6d.
+
+A separable portion of the object code, whose source code is excluded
+from the Corresponding Source as a System Library, need not be
+included in conveying the object code work.
+
+A "User Product" is either (1) a "consumer product", which means any
+tangible personal property which is normally used for personal,
+family, or household purposes, or (2) anything designed or sold for
+incorporation into a dwelling. In determining whether a product is a
+consumer product, doubtful cases shall be resolved in favor of
+coverage. For a particular product received by a particular user,
+"normally used" refers to a typical or common use of that class of
+product, regardless of the status of the particular user or of the way
+in which the particular user actually uses, or expects or is expected
+to use, the product. A product is a consumer product regardless of
+whether the product has substantial commercial, industrial or
+non-consumer uses, unless such uses represent the only significant
+mode of use of the product.
+
+"Installation Information" for a User Product means any methods,
+procedures, authorization keys, or other information required to
+install and execute modified versions of a covered work in that User
+Product from a modified version of its Corresponding Source. The
+information must suffice to ensure that the continued functioning of
+the modified object code is in no case prevented or interfered with
+solely because modification has been made.
+
+If you convey an object code work under this section in, or with, or
+specifically for use in, a User Product, and the conveying occurs as
+part of a transaction in which the right of possession and use of the
+User Product is transferred to the recipient in perpetuity or for a
+fixed term (regardless of how the transaction is characterized), the
+Corresponding Source conveyed under this section must be accompanied
+by the Installation Information. But this requirement does not apply
+if neither you nor any third party retains the ability to install
+modified object code on the User Product (for example, the work has
+been installed in ROM).
+
+The requirement to provide Installation Information does not include a
+requirement to continue to provide support service, warranty, or
+updates for a work that has been modified or installed by the
+recipient, or for the User Product in which it has been modified or
+installed. Access to a network may be denied when the modification
+itself materially and adversely affects the operation of the network
+or violates the rules and protocols for communication across the
+network.
+
+Corresponding Source conveyed, and Installation Information provided,
+in accord with this section must be in a format that is publicly
+documented (and with an implementation available to the public in
+source code form), and must require no special password or key for
+unpacking, reading or copying.
+
+#### 7. Additional Terms.
+
+"Additional permissions" are terms that supplement the terms of this
+License by making exceptions from one or more of its conditions.
+Additional permissions that are applicable to the entire Program shall
+be treated as though they were included in this License, to the extent
+that they are valid under applicable law. If additional permissions
+apply only to part of the Program, that part may be used separately
+under those permissions, but the entire Program remains governed by
+this License without regard to the additional permissions.
+
+When you convey a copy of a covered work, you may at your option
+remove any additional permissions from that copy, or from any part of
+it. (Additional permissions may be written to require their own
+removal in certain cases when you modify the work.) You may place
+additional permissions on material, added by you to a covered work,
+for which you have or can give appropriate copyright permission.
+
+Notwithstanding any other provision of this License, for material you
+add to a covered work, you may (if authorized by the copyright holders
+of that material) supplement the terms of this License with terms:
+
+- a) Disclaiming warranty or limiting liability differently from the
+ terms of sections 15 and 16 of this License; or
+- b) Requiring preservation of specified reasonable legal notices or
+ author attributions in that material or in the Appropriate Legal
+ Notices displayed by works containing it; or
+- c) Prohibiting misrepresentation of the origin of that material,
+ or requiring that modified versions of such material be marked in
+ reasonable ways as different from the original version; or
+- d) Limiting the use for publicity purposes of names of licensors
+ or authors of the material; or
+- e) Declining to grant rights under trademark law for use of some
+ trade names, trademarks, or service marks; or
+- f) Requiring indemnification of licensors and authors of that
+ material by anyone who conveys the material (or modified versions
+ of it) with contractual assumptions of liability to the recipient,
+ for any liability that these contractual assumptions directly
+ impose on those licensors and authors.
+
+All other non-permissive additional terms are considered "further
+restrictions" within the meaning of section 10. If the Program as you
+received it, or any part of it, contains a notice stating that it is
+governed by this License along with a term that is a further
+restriction, you may remove that term. If a license document contains
+a further restriction but permits relicensing or conveying under this
+License, you may add to a covered work material governed by the terms
+of that license document, provided that the further restriction does
+not survive such relicensing or conveying.
+
+If you add terms to a covered work in accord with this section, you
+must place, in the relevant source files, a statement of the
+additional terms that apply to those files, or a notice indicating
+where to find the applicable terms.
+
+Additional terms, permissive or non-permissive, may be stated in the
+form of a separately written license, or stated as exceptions; the
+above requirements apply either way.
+
+#### 8. Termination.
+
+You may not propagate or modify a covered work except as expressly
+provided under this License. Any attempt otherwise to propagate or
+modify it is void, and will automatically terminate your rights under
+this License (including any patent licenses granted under the third
+paragraph of section 11).
+
+However, if you cease all violation of this License, then your license
+from a particular copyright holder is reinstated (a) provisionally,
+unless and until the copyright holder explicitly and finally
+terminates your license, and (b) permanently, if the copyright holder
+fails to notify you of the violation by some reasonable means prior to
+60 days after the cessation.
+
+Moreover, your license from a particular copyright holder is
+reinstated permanently if the copyright holder notifies you of the
+violation by some reasonable means, this is the first time you have
+received notice of violation of this License (for any work) from that
+copyright holder, and you cure the violation prior to 30 days after
+your receipt of the notice.
+
+Termination of your rights under this section does not terminate the
+licenses of parties who have received copies or rights from you under
+this License. If your rights have been terminated and not permanently
+reinstated, you do not qualify to receive new licenses for the same
+material under section 10.
+
+#### 9. Acceptance Not Required for Having Copies.
+
+You are not required to accept this License in order to receive or run
+a copy of the Program. Ancillary propagation of a covered work
+occurring solely as a consequence of using peer-to-peer transmission
+to receive a copy likewise does not require acceptance. However,
+nothing other than this License grants you permission to propagate or
+modify any covered work. These actions infringe copyright if you do
+not accept this License. Therefore, by modifying or propagating a
+covered work, you indicate your acceptance of this License to do so.
+
+#### 10. Automatic Licensing of Downstream Recipients.
+
+Each time you convey a covered work, the recipient automatically
+receives a license from the original licensors, to run, modify and
+propagate that work, subject to this License. You are not responsible
+for enforcing compliance by third parties with this License.
+
+An "entity transaction" is a transaction transferring control of an
+organization, or substantially all assets of one, or subdividing an
+organization, or merging organizations. If propagation of a covered
+work results from an entity transaction, each party to that
+transaction who receives a copy of the work also receives whatever
+licenses to the work the party's predecessor in interest had or could
+give under the previous paragraph, plus a right to possession of the
+Corresponding Source of the work from the predecessor in interest, if
+the predecessor has it or can get it with reasonable efforts.
+
+You may not impose any further restrictions on the exercise of the
+rights granted or affirmed under this License. For example, you may
+not impose a license fee, royalty, or other charge for exercise of
+rights granted under this License, and you may not initiate litigation
+(including a cross-claim or counterclaim in a lawsuit) alleging that
+any patent claim is infringed by making, using, selling, offering for
+sale, or importing the Program or any portion of it.
+
+#### 11. Patents.
+
+A "contributor" is a copyright holder who authorizes use under this
+License of the Program or a work on which the Program is based. The
+work thus licensed is called the contributor's "contributor version".
+
+A contributor's "essential patent claims" are all patent claims owned
+or controlled by the contributor, whether already acquired or
+hereafter acquired, that would be infringed by some manner, permitted
+by this License, of making, using, or selling its contributor version,
+but do not include claims that would be infringed only as a
+consequence of further modification of the contributor version. For
+purposes of this definition, "control" includes the right to grant
+patent sublicenses in a manner consistent with the requirements of
+this License.
+
+Each contributor grants you a non-exclusive, worldwide, royalty-free
+patent license under the contributor's essential patent claims, to
+make, use, sell, offer for sale, import and otherwise run, modify and
+propagate the contents of its contributor version.
+
+In the following three paragraphs, a "patent license" is any express
+agreement or commitment, however denominated, not to enforce a patent
+(such as an express permission to practice a patent or covenant not to
+sue for patent infringement). To "grant" such a patent license to a
+party means to make such an agreement or commitment not to enforce a
+patent against the party.
+
+If you convey a covered work, knowingly relying on a patent license,
+and the Corresponding Source of the work is not available for anyone
+to copy, free of charge and under the terms of this License, through a
+publicly available network server or other readily accessible means,
+then you must either (1) cause the Corresponding Source to be so
+available, or (2) arrange to deprive yourself of the benefit of the
+patent license for this particular work, or (3) arrange, in a manner
+consistent with the requirements of this License, to extend the patent
+license to downstream recipients. "Knowingly relying" means you have
+actual knowledge that, but for the patent license, your conveying the
+covered work in a country, or your recipient's use of the covered work
+in a country, would infringe one or more identifiable patents in that
+country that you have reason to believe are valid.
+
+If, pursuant to or in connection with a single transaction or
+arrangement, you convey, or propagate by procuring conveyance of, a
+covered work, and grant a patent license to some of the parties
+receiving the covered work authorizing them to use, propagate, modify
+or convey a specific copy of the covered work, then the patent license
+you grant is automatically extended to all recipients of the covered
+work and works based on it.
+
+A patent license is "discriminatory" if it does not include within the
+scope of its coverage, prohibits the exercise of, or is conditioned on
+the non-exercise of one or more of the rights that are specifically
+granted under this License. You may not convey a covered work if you
+are a party to an arrangement with a third party that is in the
+business of distributing software, under which you make payment to the
+third party based on the extent of your activity of conveying the
+work, and under which the third party grants, to any of the parties
+who would receive the covered work from you, a discriminatory patent
+license (a) in connection with copies of the covered work conveyed by
+you (or copies made from those copies), or (b) primarily for and in
+connection with specific products or compilations that contain the
+covered work, unless you entered into that arrangement, or that patent
+license was granted, prior to 28 March 2007.
+
+Nothing in this License shall be construed as excluding or limiting
+any implied license or other defenses to infringement that may
+otherwise be available to you under applicable patent law.
+
+#### 12. No Surrender of Others' Freedom.
+
+If conditions are imposed on you (whether by court order, agreement or
+otherwise) that contradict the conditions of this License, they do not
+excuse you from the conditions of this License. If you cannot convey a
+covered work so as to satisfy simultaneously your obligations under
+this License and any other pertinent obligations, then as a
+consequence you may not convey it at all. For example, if you agree to
+terms that obligate you to collect a royalty for further conveying
+from those to whom you convey the Program, the only way you could
+satisfy both those terms and this License would be to refrain entirely
+from conveying the Program.
+
+#### 13. Use with the GNU Affero General Public License.
+
+Notwithstanding any other provision of this License, you have
+permission to link or combine any covered work with a work licensed
+under version 3 of the GNU Affero General Public License into a single
+combined work, and to convey the resulting work. The terms of this
+License will continue to apply to the part which is the covered work,
+but the special requirements of the GNU Affero General Public License,
+section 13, concerning interaction through a network will apply to the
+combination as such.
+
+#### 14. Revised Versions of this License.
+
+The Free Software Foundation may publish revised and/or new versions
+of the GNU General Public License from time to time. Such new versions
+will be similar in spirit to the present version, but may differ in
+detail to address new problems or concerns.
+
+Each version is given a distinguishing version number. If the Program
+specifies that a certain numbered version of the GNU General Public
+License "or any later version" applies to it, you have the option of
+following the terms and conditions either of that numbered version or
+of any later version published by the Free Software Foundation. If the
+Program does not specify a version number of the GNU General Public
+License, you may choose any version ever published by the Free
+Software Foundation.
+
+If the Program specifies that a proxy can decide which future versions
+of the GNU General Public License can be used, that proxy's public
+statement of acceptance of a version permanently authorizes you to
+choose that version for the Program.
+
+Later license versions may give you additional or different
+permissions. However, no additional obligations are imposed on any
+author or copyright holder as a result of your choosing to follow a
+later version.
+
+#### 15. Disclaimer of Warranty.
+
+THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY
+APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT
+HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT
+WARRANTY OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT
+LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+A PARTICULAR PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND
+PERFORMANCE OF THE PROGRAM IS WITH YOU. SHOULD THE PROGRAM PROVE
+DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING, REPAIR OR
+CORRECTION.
+
+#### 16. Limitation of Liability.
+
+IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING
+WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR
+CONVEYS THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES,
+INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES
+ARISING OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT
+NOT LIMITED TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR
+LOSSES SUSTAINED BY YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM
+TO OPERATE WITH ANY OTHER PROGRAMS), EVEN IF SUCH HOLDER OR OTHER
+PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH DAMAGES.
+
+#### 17. Interpretation of Sections 15 and 16.
+
+If the disclaimer of warranty and limitation of liability provided
+above cannot be given local legal effect according to their terms,
+reviewing courts shall apply local law that most closely approximates
+an absolute waiver of all civil liability in connection with the
+Program, unless a warranty or assumption of liability accompanies a
+copy of the Program in return for a fee.
+
+END OF TERMS AND CONDITIONS
+
+### How to Apply These Terms to Your New Programs
+
+If you develop a new program, and you want it to be of the greatest
+possible use to the public, the best way to achieve this is to make it
+free software which everyone can redistribute and change under these
+terms.
+
+To do so, attach the following notices to the program. It is safest to
+attach them to the start of each source file to most effectively state
+the exclusion of warranty; and each file should have at least the
+"copyright" line and a pointer to where the full notice is found.
+
+ <one line to give the program's name and a brief idea of what it does.>
+ Copyright (C) <year> <name of author>
+
+ This program is free software: you can redistribute it and/or modify
+ it under the terms of the GNU General Public License as published by
+ the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ This program is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License
+ along with this program. If not, see <https://www.gnu.org/licenses/>.
+
+Also add information on how to contact you by electronic and paper
+mail.
+
+If the program does terminal interaction, make it output a short
+notice like this when it starts in an interactive mode:
+
+ <program> Copyright (C) <year> <name of author>
+ This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'.
+ This is free software, and you are welcome to redistribute it
+ under certain conditions; type `show c' for details.
+
+The hypothetical commands \`show w' and \`show c' should show the
+appropriate parts of the General Public License. Of course, your
+program's commands might be different; for a GUI interface, you would
+use an "about box".
+
+You should also get your employer (if you work as a programmer) or
+school, if any, to sign a "copyright disclaimer" for the program, if
+necessary. For more information on this, and how to apply and follow
+the GNU GPL, see <https://www.gnu.org/licenses/>.
+
+The GNU General Public License does not permit incorporating your
+program into proprietary programs. If your program is a subroutine
+library, you may consider it more useful to permit linking proprietary
+applications with the library. If this is what you want to do, use the
+GNU Lesser General Public License instead of this License. But first,
+please read <https://www.gnu.org/licenses/why-not-lgpl.html>.
diff --git a/INITIAL-COMMANDS.md b/INITIAL-COMMANDS.md
new file mode 100644
index 0000000..4a12197
--- /dev/null
+++ b/INITIAL-COMMANDS.md
@@ -0,0 +1,55 @@
+Initial commands
+================
+
+[![GitHub stars](https://img.shields.io/github/stars/eworm-de/routeros-scripts?logo=GitHub&style=flat&color=red)](https://github.com/eworm-de/routeros-scripts/stargazers)
+[![GitHub forks](https://img.shields.io/github/forks/eworm-de/routeros-scripts?logo=GitHub&style=flat&color=green)](https://github.com/eworm-de/routeros-scripts/network)
+[![GitHub watchers](https://img.shields.io/github/watchers/eworm-de/routeros-scripts?logo=GitHub&style=flat&color=blue)](https://github.com/eworm-de/routeros-scripts/watchers)
+[![required RouterOS version](https://img.shields.io/badge/RouterOS-7.13-yellow?style=flat)](https://mikrotik.com/download/changelogs/)
+[![Telegram group @routeros_scripts](https://img.shields.io/badge/Telegram-%40routeros__scripts-%2326A5E4?logo=telegram&style=flat)](https://t.me/routeros_scripts)
+[![donate with PayPal](https://img.shields.io/badge/Like_it%3F-Donate!-orange?logo=githubsponsors&logoColor=orange&style=flat)](https://www.paypal.com/cgi-bin/webscr?cmd=_s-xclick&hosted_button_id=A4ZXBD6YS2W8J)
+
+[⬅️ Go back to main README](README.md)
+
+> ⚠️ **Warning**: These command are inteneded for initial setup. If you are
+> not aware of the procedure please follow
+> [the long way in detail](README.md#the-long-way-in-detail).
+
+Run the complete base installation:
+
+ {
+ /tool/fetch "https://git.eworm.de/cgit/routeros-scripts/plain/certs/E1.pem" dst-path="letsencrypt-E1.pem" as-value;
+ :delay 1s;
+ /certificate/import file-name=letsencrypt-E1.pem passphrase="";
+ :if ([ :len [ /certificate/find where fingerprint="46494e30379059df18be52124305e606fc59070e5b21076ce113954b60517cda" or fingerprint="69729b8e15a86efc177a57afb7171dfc64add28c2fca8cf1507e34453ccb1470" ] ] != 2) do={
+ :error "Something is wrong with your certificates!";
+ };
+ /file/remove [ find where name="letsencrypt-E1.pem" ];
+ :delay 1s;
+ /system/script/set name=("global-config-overlay-" . [ /system/clock/get date ] . "-" . [ /system/clock/get time ]) [ find where name="global-config-overlay" ];
+ :foreach Script in={ "global-config"; "global-config-overlay"; "global-functions" } do={
+ /system/script/remove [ find where name=$Script ];
+ /system/script/add name=$Script owner=$Script source=([ /tool/fetch check-certificate=yes-without-crl ("https://git.eworm.de/cgit/routeros-scripts/plain/" . $Script . ".rsc") output=user as-value]->"data");
+ };
+ /system/script { run global-config; run global-functions; };
+ /system/scheduler/remove [ find where name="global-scripts" ];
+ /system/scheduler/add name="global-scripts" start-time=startup on-event="/system/script { run global-config; run global-functions; }";
+ :global CertificateNameByCN;
+ $CertificateNameByCN "E1";
+ $CertificateNameByCN "ISRG Root X2";
+ };
+
+Then continue setup with
+[scheduled automatic updates](README.md#scheduled-automatic-updates) or
+[editing configuration](README.md#editing-configuration).
+
+## Fix existing installation
+
+The [initial commands](#initial-commands) above allow to fix an existing
+installation in case it ever breaks. If `global-config-overlay` did exist
+before it is renamed with a date and time suffix (like
+`global-config-overlay-2024-01-25-09:33:12`). Make sure to restore the
+configuration overlay if required.
+
+---
+[⬅️ Go back to main README](README.md)
+[⬆️ Go back to top](#top)
diff --git a/Makefile b/Makefile
index f96e73e..d21713c 100644
--- a/Makefile
+++ b/Makefile
@@ -2,24 +2,35 @@
# template scripts -> final scripts
# markdown files -> html files
-TEMPLATE = $(wildcard *.template)
-CAPSMAN = $(TEMPLATE:.template=.capsman)
-LOCAL = $(TEMPLATE:.template=.local)
+CAPSMAN = $(wildcard *.capsman.rsc)
+LOCAL = $(wildcard *.local.rsc)
+WIFI = $(wildcard *.wifi.rsc)
-MARKDOWN = $(wildcard *.md)
-HTML = $(MARKDOWN:.md=.html)
+MARKDOWN = $(wildcard *.md doc/*.md doc/mod/*.md)
+HTML = $(MARKDOWN:.md=.html)
-all: $(CAPSMAN) $(LOCAL) $(HTML)
+all: $(CAPSMAN) $(LOCAL) $(WIFI) $(HTML)
%.html: %.md Makefile
- markdown $< | sed 's/href="\([-[:alnum:]]*\)\.md"/href="\1.html"/g' > $@
+ markdown $< | sed 's/href="\([-_\./[:alnum:]]*\)\.md"/href="\1.html"/g' > $@
-%.local: %.template Makefile
- sed -e '/\/ caps-man/d' -e 's|%PATH%|interface wireless|' -e 's|%TEMPL%|$(suffix $@)|' \
+%.capsman.rsc: %.template.rsc Makefile
+ sed -e '/\/interface\/wifi\//d' -e '/\/interface\/wireless\//d' -e 's|%TEMPL%|.capsman|' \
+ -e '/^# NOT \/caps-man\/ #$$/,/^# NOT \/caps-man\/ #$$/d' \
-e '/^# !!/,/^# !!/c # !! Do not edit this file, it is generated from template!' \
< $< > $@
-%.capsman: %.template Makefile
- sed -e '/\/ interface wireless/d' -e 's/%PATH%/caps-man/' -e 's/%TEMPL%/$(suffix $@)/' \
+%.local.rsc: %.template.rsc Makefile
+ sed -e '/\/caps-man\//d' -e '/\/interface\/wifi\//d' -e 's|%TEMPL%|.local|' \
+ -e '/^# NOT \/interface\/wireless\/ #$$/,/^# NOT \/interface\/wireless\/ #$$/d' \
-e '/^# !!/,/^# !!/c # !! Do not edit this file, it is generated from template!' \
< $< > $@
+
+%.wifi.rsc: %.template.rsc Makefile
+ sed -e '/\/caps-man\//d' -e '/\/interface\/wireless\//d' -e 's|%TEMPL%|.wifi|' \
+ -e '/^# NOT \/interface\/wifi\/ #$$/,/^# NOT \/interface\/wifi\/ #$$/d' \
+ -e '/^# !!/,/^# !!/c # !! Do not edit this file, it is generated from template!' \
+ < $< > $@
+
+clean:
+ rm -f $(HTML)
diff --git a/README.d/01-download-certs.avif b/README.d/01-download-certs.avif
new file mode 100644
index 0000000..b27b23b
--- /dev/null
+++ b/README.d/01-download-certs.avif
Binary files differ
diff --git a/README.d/02-import-certs.avif b/README.d/02-import-certs.avif
new file mode 100644
index 0000000..d42994b
--- /dev/null
+++ b/README.d/02-import-certs.avif
Binary files differ
diff --git a/README.d/03-check-certs.avif b/README.d/03-check-certs.avif
new file mode 100644
index 0000000..0477c39
--- /dev/null
+++ b/README.d/03-check-certs.avif
Binary files differ
diff --git a/README.d/04-import-scripts.avif b/README.d/04-import-scripts.avif
new file mode 100644
index 0000000..53439e4
--- /dev/null
+++ b/README.d/04-import-scripts.avif
Binary files differ
diff --git a/README.d/05-run-and-schedule-scripts.avif b/README.d/05-run-and-schedule-scripts.avif
new file mode 100644
index 0000000..37e1173
--- /dev/null
+++ b/README.d/05-run-and-schedule-scripts.avif
Binary files differ
diff --git a/README.d/06-schedule-update.avif b/README.d/06-schedule-update.avif
new file mode 100644
index 0000000..7c96f3a
--- /dev/null
+++ b/README.d/06-schedule-update.avif
Binary files differ
diff --git a/README.d/07-edit-global-config-overlay.avif b/README.d/07-edit-global-config-overlay.avif
new file mode 100644
index 0000000..f87fda8
--- /dev/null
+++ b/README.d/07-edit-global-config-overlay.avif
Binary files differ
diff --git a/README.d/08-apply-configuration.avif b/README.d/08-apply-configuration.avif
new file mode 100644
index 0000000..b66af1a
--- /dev/null
+++ b/README.d/08-apply-configuration.avif
Binary files differ
diff --git a/README.d/09-update-scripts.avif b/README.d/09-update-scripts.avif
new file mode 100644
index 0000000..f549fef
--- /dev/null
+++ b/README.d/09-update-scripts.avif
Binary files differ
diff --git a/README.d/10-install-scripts.avif b/README.d/10-install-scripts.avif
new file mode 100644
index 0000000..00225b1
--- /dev/null
+++ b/README.d/10-install-scripts.avif
Binary files differ
diff --git a/README.d/11-schedule-script.avif b/README.d/11-schedule-script.avif
new file mode 100644
index 0000000..d6eb0f8
--- /dev/null
+++ b/README.d/11-schedule-script.avif
Binary files differ
diff --git a/README.d/12-setup-lease-script.avif b/README.d/12-setup-lease-script.avif
new file mode 100644
index 0000000..fb4024e
--- /dev/null
+++ b/README.d/12-setup-lease-script.avif
Binary files differ
diff --git a/README.d/13-install-custom-script.avif b/README.d/13-install-custom-script.avif
new file mode 100644
index 0000000..2f01c43
--- /dev/null
+++ b/README.d/13-install-custom-script.avif
Binary files differ
diff --git a/README.d/14-remove-script.avif b/README.d/14-remove-script.avif
new file mode 100644
index 0000000..a5c7daf
--- /dev/null
+++ b/README.d/14-remove-script.avif
Binary files differ
diff --git a/README.d/hello-world.rsc b/README.d/hello-world.rsc
new file mode 100644
index 0000000..6404781
--- /dev/null
+++ b/README.d/hello-world.rsc
@@ -0,0 +1,3 @@
+#!rsc by RouterOS
+
+:put ("Hello World from " . [ /system/identity/get name ] . "!");
diff --git a/README.d/notification-news-and-changes.avif b/README.d/notification-news-and-changes.avif
new file mode 100644
index 0000000..d91b8a0
--- /dev/null
+++ b/README.d/notification-news-and-changes.avif
Binary files differ
diff --git a/README.d/telegram-group.avif b/README.d/telegram-group.avif
new file mode 100644
index 0000000..eb75d13
--- /dev/null
+++ b/README.d/telegram-group.avif
Binary files differ
diff --git a/README.d/upstream.png b/README.d/upstream.png
new file mode 100644
index 0000000..fd5e877
--- /dev/null
+++ b/README.d/upstream.png
Binary files differ
diff --git a/README.md b/README.md
index ec1654e..b6e529d 100644
--- a/README.md
+++ b/README.md
@@ -1,23 +1,46 @@
RouterOS Scripts
================
+[![GitHub stars](https://img.shields.io/github/stars/eworm-de/routeros-scripts?logo=GitHub&style=flat&color=red)](https://github.com/eworm-de/routeros-scripts/stargazers)
+[![GitHub forks](https://img.shields.io/github/forks/eworm-de/routeros-scripts?logo=GitHub&style=flat&color=green)](https://github.com/eworm-de/routeros-scripts/network)
+[![GitHub watchers](https://img.shields.io/github/watchers/eworm-de/routeros-scripts?logo=GitHub&style=flat&color=blue)](https://github.com/eworm-de/routeros-scripts/watchers)
+[![required RouterOS version](https://img.shields.io/badge/RouterOS-7.13-yellow?style=flat)](https://mikrotik.com/download/changelogs/)
+[![Telegram group @routeros_scripts](https://img.shields.io/badge/Telegram-%40routeros__scripts-%2326A5E4?logo=telegram&style=flat)](https://t.me/routeros_scripts)
+[![donate with PayPal](https://img.shields.io/badge/Like_it%3F-Donate!-orange?logo=githubsponsors&logoColor=orange&style=flat)](https://www.paypal.com/cgi-bin/webscr?cmd=_s-xclick&hosted_button_id=A4ZXBD6YS2W8J)
+
+![RouterOS Scripts Logo](logo.svg)
+
[RouterOS](https://mikrotik.com/software) is the operating system developed
by [MikroTik](https://mikrotik.com/aboutus) for networking tasks. This
repository holds a number of [scripts](https://wiki.mikrotik.com/wiki/Manual:Scripting)
to manage RouterOS devices or extend their functionality.
-*Use at your own risk!*
+*Use at your own risk*, pay attention to
+[license and warranty](#license-and-warranty)!
Requirements
------------
-Latest version of the scripts require at least **RouterOS 6.43** to function
-properly. The changelog lists the corresponding change as follows:
+### Software (RouterOS)
+
+Latest version of the scripts require recent RouterOS to function properly.
+Make sure to install latest updates before you begin. If new functionality
+or a breaking change in RouterOS `7.n` is used in my scripts I push my
+change some time after `7.(n+1)` was released. At any time you should have
+at least two minor and their bugfix releases to choose from.
+
+Specific scripts may require even newer RouterOS version.
+
+> ℹ️ **Info**: The `main` branch is now RouterOS v7 only. If you are still
+> running RouterOS v6 switch to `routeros-v6` branch!
-> *) fetch - added "as-value" output format;
+### Hardware
-Specific scripts may require even newer RouterOS version, for example cloud
-backup was added in 6.44.
+RouterOS packages increase in size with each release. This becomes a
+problem for devices with 16MB storage and below, those with an ARM CPU
+are specifically affected.
+
+Huge configuration and lots of scripts give an extra risk. **Take care!**
Initial setup
-------------
@@ -25,7 +48,7 @@ Initial setup
### Get me ready!
If you know how things work just copy and paste the
-[initial commands](initial-commands). Remember to edit and rerun
+[initial commands](INITIAL-COMMANDS.md). Remember to edit and rerun
`global-config-overlay`!
First time users should take the long way below.
@@ -36,6 +59,9 @@ RouterOS script distribution](https://www.youtube.com/watch?v=B9neG3oAhcY)
including demonstation recorded live at [MUM Europe
2019](https://mum.mikrotik.com/2019/EU/) in Vienna.
+> ⚠️ **Warning**: Some details changed. So see the presentation, then follow
+> the steps below for up-to-date commands.
+
### The long way in detail
The update script does server certificate verification, so first step is to
@@ -43,106 +69,276 @@ download the certificates. If you intend to download the scripts from a
different location (for example from github.com) install the corresponding
certificate chain.
- [admin@MikroTik] > / tool fetch "https://git.eworm.de/cgit/routeros-scripts/plain/certs/Let%27s%20Encrypt%20Authority%20X3.pem" dst-path="letsencrypt.pem"
- status: finished
- downloaded: 3KiBC-z pause]
- total: 3KiB
- duration: 1s
+ /tool/fetch "https://git.eworm.de/cgit/routeros-scripts/plain/certs/E1.pem" dst-path="letsencrypt-E1.pem";
+
+![screenshot: download certs](README.d/01-download-certs.avif)
Note that the commands above do *not* verify server certificate, so if you
want to be safe download with your workstations's browser and transfer the
files to your MikroTik device.
-* [ISRG Root X1](https://letsencrypt.org/certs/isrgrootx1.pem.txt)
-* [Let's Encrypt Authority X3](https://letsencrypt.org/certs/letsencryptauthorityx3.pem.txt)
+* [ISRG Root X2](https://letsencrypt.org/certs/isrg-root-x2.pem)
+* Let's Encrypt [E1](https://letsencrypt.org/certs/lets-encrypt-e1.pem)
Then we import the certificates.
- [admin@MikroTik] > / certificate import file-name=letsencrypt.pem passphrase=""
- certificates-imported: 3
- private-keys-imported: 0
- files-imported: 1
- decryption-failures: 0
- keys-with-no-certificate: 0
+ /certificate/import file-name=letsencrypt-E1.pem passphrase="";
+
+Do not worry that the command is not shown - that happens because it contains
+a sensitive property, the passphrase.
+
+![screenshot: import certs](README.d/02-import-certs.avif)
-For basic verification we rename the certifiactes and print their count. Make
-sure the certificate count is **three**.
+For basic verification we rename the certificates and print them by
+fingerprint. Make sure exactly these two certificates ("*E1*" and
+"*ISRG-Root-X2*") are shown. Also remove the left over file.
- [admin@MikroTik] > / certificate set name="ISRG-Root-X1" [ find where fingerprint="96bcec06264976f37460779acf28c5a7cfe8a3c0aae11a8ffcee05c0bddf08c6" ]
- [admin@MikroTik] > / certificate set name="Let-s-Encrypt-Authority-X3" [ find where fingerprint="731d3d9cfaa061487a1d71445a42f67df0afca2a6c2d2f98ff7b3ce112b1f568" ]
- [admin@MikroTik] > / certificate set name="DST-Root-CA-X3" [ find where fingerprint="0687260331a72403d909f105e69bcf0d32e1bd2493ffc6d9206d11bcd6770739" ]
- [admin@MikroTik] > / certificate print count-only where fingerprint="96bcec06264976f37460779acf28c5a7cfe8a3c0aae11a8ffcee05c0bddf08c6" or fingerprint="731d3d9cfaa061487a1d71445a42f67df0afca2a6c2d2f98ff7b3ce112b1f568" or fingerprint="0687260331a72403d909f105e69bcf0d32e1bd2493ffc6d9206d11bcd6770739"
- 3
+ /certificate/set name="E1" [ find where common-name="E1" ];
+ /certificate/set name="ISRG-Root-X2" [ find where common-name="ISRG Root X2" ];
+ /certificate/print proplist=name,fingerprint where fingerprint="46494e30379059df18be52124305e606fc59070e5b21076ce113954b60517cda" or fingerprint="69729b8e15a86efc177a57afb7171dfc64add28c2fca8cf1507e34453ccb1470";
+ /file/remove [ find where name="letsencrypt-E1.pem" ];
+
+![screenshot: check certs](README.d/03-check-certs.avif)
Always make sure there are no certificates installed you do not know or want!
-Actually we do not require the certificate named `DST Root CA X3`, but as it
-is used by `Let's Encrypt` to cross-sign we install it anyway - this makes
-sure things do not go wrong if the intermediate certificate is replaced.
-The IdenTrust certificate *should* be available from their
-[download page](https://www.identrust.com/support/downloads). The site is
-crap and a good example how to *not* do it.
+All following commands will verify the server certificate. For validity the
+certificate's lifetime is checked with local time, so make sure the device's
+date and time is set correctly!
Now let's download the main scripts and add them in configuration on the fly.
- [admin@MikroTik] > :foreach Script in={ "global-config"; "global-config-overlay"; "global-functions"; "script-updates" } do={ / system script add name=$Script source=([ / tool fetch check-certificate=yes-without-crl ("https://git.eworm.de/cgit/routeros-scripts/plain/" . $Script) output=user as-value]->"data"); }
-
-The configuration needs to be tweaked for your needs. Make sure not to send
-your mails to `mail@example.com`! Edit `global-config-overlay`, copy
-configuration from `global-config`.
+ :foreach Script in={ "global-config"; "global-config-overlay"; "global-functions" } do={ /system/script/add name=$Script owner=$Script source=([ /tool/fetch check-certificate=yes-without-crl ("https://git.eworm.de/cgit/routeros-scripts/plain/" . $Script . ".rsc") output=user as-value]->"data"); };
- [admin@MikroTik] > / system script edit global-config-overlay source
+![screenshot: import scripts](README.d/04-import-scripts.avif)
And finally load configuration and functions and add the scheduler.
- [admin@MikroTik] > / system script { run global-config; run global-config-overlay; run global-functions; }
- [admin@MikroTik] > / system scheduler add name="global-scripts" start-time=startup on-event="/ system script { run global-config; run global-config-overlay; run global-functions; }"
+ /system/script { run global-config; run global-functions; };
+ /system/scheduler/add name="global-scripts" start-time=startup on-event="/system/script { run global-config; run global-functions; }";
+
+![screenshot: run and schedule scripts](README.d/05-run-and-schedule-scripts.avif)
+
+### Scheduled automatic updates
+
+The last step is optional: Add this scheduler **only** if you want the
+scripts to be updated automatically!
+
+ /system/scheduler/add name="ScriptInstallUpdate" start-time=startup interval=1d on-event=":global ScriptInstallUpdate; \$ScriptInstallUpdate;";
+
+![screenshot: schedule update](README.d/06-schedule-update.avif)
+
+Editing configuration
+---------------------
+
+The configuration needs to be tweaked for your needs. Edit
+`global-config-overlay`, copy relevant configuration from
+[`global-config`](global-config.rsc) (the one without `-overlay`).
+Save changes and exit with `Ctrl-o`.
+
+ /system/script/edit global-config-overlay source;
+
+![screenshot: edit global-config-overlay](README.d/07-edit-global-config-overlay.avif)
+
+Additionally creating configuration snippets is supported. The script name
+of these snippets has to start with `global-config-overlay.d/` to make them
+being loaded automatically. This allows to split off parts of the
+configuration.
+
+To apply your changes run `global-config`, which will automatically load
+the overlay as well:
+
+ /system/script/run global-config;
+
+![screenshot: apply configuration](README.d/08-apply-configuration.avif)
+
+This last step is required when ever you make changes to your configuration.
+
+> ℹ️ **Info**: It is recommended to edit the configuration using the command
+> line interface. If using Winbox on Windows OS, the line endings may be
+> missing. To fix this run:
+> `/system/script/set source=[ $Unix2Dos [ get global-config-overlay source ] ] global-config-overlay;`
Updating scripts
----------------
-To update existing scripts just run `script-updates`.
+To update existing scripts just run function `$ScriptInstallUpdate`. If
+everything is up-to-date it will not produce any output.
+
+ $ScriptInstallUpdate;
+
+![screenshot: update scripts](README.d/09-update-scripts.avif)
- [admin@MikroTik] > / system script run script-updates
+If the update includes news or requires configuration changes a notification
+is sent - in addition to terminal output and log messages.
+
+![news and changes notification](README.d/notification-news-and-changes.avif)
Adding a script
---------------
-To add a script from the repository create a configuration item first, then
-update scripts to fetch the source.
+To add a script from the repository run function `$ScriptInstallUpdate` with
+a comma separated list of script names.
+
+ $ScriptInstallUpdate check-certificates,check-routeros-update;
- [admin@MikroTik] > / system script add name="check-routeros-update"
- [admin@MikroTik] > / system script run script-updates
+![screenshot: install scripts](README.d/10-install-scripts.avif)
Scheduler and events
--------------------
Most scripts are designed to run regularly from
[scheduler](https://wiki.mikrotik.com/wiki/Manual:System/Scheduler). We just
-added `check-routeros-update`, so let's run it every hour to make sure not to
+added `check-routeros-update`, so let's run it daily to make sure not to
miss an update.
- [admin@MikroTik] > / system scheduler add name="check-routeros-update" interval=1h on-event="/ system script run check-routeros-update;"
+ /system/scheduler/add name="check-routeros-update" interval=1d start-time=startup on-event="/system/script/run check-routeros-update;";
+
+![screenshot: schedule script](README.d/11-schedule-script.avif)
Some events can run a script. If you want your DHCP hostnames to be available
in DNS use `dhcp-to-dns` with the events from dhcp server. For a regular
cleanup add a scheduler entry.
- [admin@MikroTik] > / system script add name="dhcp-to-dns"
- [admin@MikroTik] > / system script run script-updates
- [admin@MikroTik] > / ip dhcp-server set lease-script=dhcp-to-dns [ find ]
- [admin@MikroTik] > / system scheduler add name="dhcp-to-dns" interval=5m on-event="/ system script run dhcp-to-dns;"
+ $ScriptInstallUpdate dhcp-to-dns,lease-script;
+ /ip/dhcp-server/set lease-script=lease-script [ find ];
+ /system/scheduler/add name="dhcp-to-dns" interval=5m on-event="/system/script/run dhcp-to-dns;";
+
+![screenshot: setup lease script](README.d/12-setup-lease-script.avif)
There's much more to explore... Have fun!
-## Contribute
+Available scripts
+-----------------
+
+* [Find and remove access list duplicates](doc/accesslist-duplicates.md)
+* [Upload backup to Mikrotik cloud](doc/backup-cloud.md)
+* [Send backup via e-mail](doc/backup-email.md)
+* [Save configuration to fallback partition](doc/backup-partition.md)
+* [Upload backup to server](doc/backup-upload.md)
+* [Download packages for CAP upgrade from CAPsMAN](doc/capsman-download-packages.md)
+* [Run rolling CAP upgrades from CAPsMAN](doc/capsman-rolling-upgrade.md)
+* [Renew locally issued certificates](doc/certificate-renew-issued.md)
+* [Renew certificates and notify on expiration](doc/check-certificates.md)
+* [Notify about health state](doc/check-health.md)
+* [Notify on LTE firmware upgrade](doc/check-lte-firmware-upgrade.md)
+* [Notify on RouterOS update](doc/check-routeros-update.md)
+* [Collect MAC addresses in wireless access list](doc/collect-wireless-mac.md)
+* [Use wireless network with daily psk](doc/daily-psk.md)
+* [Comment DHCP leases with info from access list](doc/dhcp-lease-comment.md)
+* [Create DNS records for DHCP leases](doc/dhcp-to-dns.md)
+* [Automatically upgrade firmware and reboot](doc/firmware-upgrade-reboot.md)
+* [Download, import and update firewall address-lists](doc/fw-addr-lists.md)
+* [Wait for global functions und modules](doc/global-wait.md)
+* [Send GPS position to server](doc/gps-track.md)
+* [Use WPA network with hotspot credentials](doc/hotspot-to-wpa.md)
+* [Create DNS records for IPSec peers](doc/ipsec-to-dns.md)
+* [Update configuration on IPv6 prefix change](doc/ipv6-update.md)
+* [Manage IP addresses with bridge status](doc/ip-addr-bridge.md)
+* [Run other scripts on DHCP lease](doc/lease-script.md)
+* [Manage LEDs dark mode](doc/leds-mode.md)
+* [Forward log messages via notification](doc/log-forward.md)
+* [Mode button with multiple presses](doc/mode-button.md)
+* [Manage DNS and DoH servers from netwatch](doc/netwatch-dns.md)
+* [Notify on host up and down](doc/netwatch-notify.md)
+* [Visualize OSPF state via LEDs](doc/ospf-to-leds.md)
+* [Manage system update](doc/packages-update.md)
+* [Run scripts on ppp connection](doc/ppp-on-up.md)
+* [Act on received SMS](doc/sms-action.md)
+* [Forward received SMS](doc/sms-forward.md)
+* [Play Super Mario theme](doc/super-mario-theme.md)
+* [Chat with your router and send commands via Telegram bot](doc/telegram-chat.md)
+* [Install LTE firmware upgrade](doc/unattended-lte-firmware-upgrade.md)
+* [Update GRE configuration with dynamic addresses](doc/update-gre-address.md)
+* [Update tunnelbroker configuration](doc/update-tunnelbroker.md)
+
+Available modules
+-----------------
+
+* [Manage ports in bridge](doc/mod/bridge-port-to.md)
+* [Manage VLANs on bridge ports](doc/mod/bridge-port-vlan.md)
+* [Inspect variables](doc/mod/inspectvar.md)
+* [IP address calculation](doc/mod/ipcalc.md)
+* [Send notifications via e-mail](doc/mod/notification-email.md)
+* [Send notifications via Matrix](doc/mod/notification-matrix.md)
+* [Send notifications via Ntfy](doc/mod/notification-ntfy.md)
+* [Send notifications via Telegram](doc/mod/notification-telegram.md)
+* [Download script and run it once](doc/mod/scriptrunonce.md)
+* [Import ssh keys for public key authentication](doc/mod/ssh-keys-import.md)
+
+Installing custom scripts & modules
+-----------------------------------
+
+My scripts cover a lot of use cases, but you may have your own ones. You can
+still use my scripts to manage and deploy yours, by specifying `base-url`
+(and `url-suffix`) for each script.
+
+This will fetch and install a script `hello-world.rsc` from the given url:
+
+ $ScriptInstallUpdate hello-world "base-url=https://git.eworm.de/cgit/routeros-scripts-custom/plain/";
+
+![screenshot: install custom script](README.d/13-install-custom-script.avif)
+
+For a script to be considered valid it has to begin with a *magic token*.
+Have a look at [any script](README.d/hello-world.rsc) and copy the first line
+without modification.
+
+Starting a script's name with `mod/` makes it a module and it is run
+automatically by `global-functions`.
+
+### Linked custom scripts & modules
+
+> ⚠️ **Warning**: These links are being provided for your convenience only;
+> they do not constitute an endorsement or an approval by me. I bear no
+> responsibility for the accuracy, legality or content of the external site
+> or for that of subsequent links. Contact the external site for answers to
+> questions regarding its content.
+
+* [Hello World](https://git.eworm.de/cgit/routeros-scripts-custom/about/doc/hello-world.md)
+ (This is a demo script to show how the linking to external documentation
+ will be done.)
+
+> ℹ️ **Info**: You have your own set of scripts and/or modules and want these
+> to be listed here? There should be a general info page that links here,
+> and documentation for each script. You can start by cloning my
+> [Custom RouterOS-Scripts](https://git.eworm.de/cgit/routeros-scripts-custom/)
+> (or fork on [GitHub](https://github.com/eworm-de/routeros-scripts-custom)
+> or [GitLab](https://gitlab.com/eworm-de/routeros-scripts-custom)) and make
+> your changes. Then please [get in contact](#patches-issues-and-whishlist)...
+
+Removing a script
+-----------------
+
+There is no specific function for script removal. Just remove it from
+configuration...
+
+ /system/script/remove to-be-removed;
+
+![screenshot: remove script](README.d/14-remove-script.avif)
+
+Possibly a scheduler and other configuration has to be removed as well.
+
+Contact
+-------
+
+We have a Telegram Group [RouterOS-Scripts](https://t.me/routeros_scripts)!
+
+[![RouterOS Scripts Telegram Group](README.d/telegram-group.avif)](https://t.me/routeros_scripts)
+
+Get help, give feedback or just chat - but do not expect free professional
+support!
+
+Contribute
+----------
-Thanks a lot for [past contributions](CONTRIBUTIONS.md)!
+Thanks a lot for [past contributions](CONTRIBUTIONS.md)! ❤️
### Patches, issues and whishlist
Feel free to contact me via e-mail or open an
-[issue at github](https://github.com/eworm-de/routeros-scripts/issues).
+[issue](https://github.com/eworm-de/routeros-scripts/issues) or
+[pull request](https://github.com/eworm-de/routeros-scripts/pulls)
+at github.
### Donate
@@ -151,11 +347,27 @@ for you. If you like the scripts and think this is of value for you or your
business please consider to
[donate with PayPal](https://www.paypal.com/cgi-bin/webscr?cmd=_s-xclick&hosted_button_id=A4ZXBD6YS2W8J).
-[![donate with PayPal](https://www.paypalobjects.com/en_US/i/btn/btn_donate_SM.gif)](https://www.paypal.com/cgi-bin/webscr?cmd=_s-xclick&hosted_button_id=A4ZXBD6YS2W8J)
+[![donate with PayPal](https://img.shields.io/badge/Like_it%3F-Donate!-orange?logo=githubsponsors&logoColor=orange&style=for-the-badge)](https://www.paypal.com/cgi-bin/webscr?cmd=_s-xclick&hosted_button_id=A4ZXBD6YS2W8J)
Thanks a lot for your support!
-## Upstream
+License and warranty
+--------------------
+
+This program is free software: you can redistribute it and/or modify
+it under the terms of the GNU General Public License as published by
+the Free Software Foundation, either version 3 of the License, or
+(at your option) any later version.
+
+This program is distributed in the hope that it will be useful,
+but WITHOUT ANY WARRANTY; without even the implied warranty of
+MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+[GNU General Public License](COPYING.md) for more details.
+
+Upstream
+--------
+
+![upstream](README.d/upstream.png)
URL:
[GitHub.com](https://github.com/eworm-de/routeros-scripts#routeros-scripts)
@@ -165,4 +377,4 @@ Mirror:
[GitLab.com](https://gitlab.com/eworm-de/routeros-scripts#routeros-scripts)
---
-[▲ Go back to top](#top)
+[⬆️ Go back to top](#top)
diff --git a/accesslist-duplicates.capsman b/accesslist-duplicates.capsman
deleted file mode 100644
index e506740..0000000
--- a/accesslist-duplicates.capsman
+++ /dev/null
@@ -1,34 +0,0 @@
-#!rsc
-# RouterOS script: accesslist-duplicates.capsman
-# Copyright (c) 2018-2020 Christian Hesse <mail@eworm.de>
-#
-# print duplicate antries in wireless access list
-#
-# !! Do not edit this file, it is generated from template!
-
-:local Seen [ :toarray "" ];
-:local Shown [ :toarray "" ];
-
-:foreach AccList in=[ / caps-man access-list find where mac-address!="00:00:00:00:00:00" ] do={
- :local Mac [ / caps-man access-list get $AccList mac-address ];
- :foreach SeenMac in=$Seen do={
- :if ($SeenMac = $Mac) do={
- :local Skip 0;
- :foreach ShownMac in=$Shown do={
- :if ($ShownMac = $Mac) do={ :set Skip 1; }
- }
- :if ($Skip = 0) do={
- / caps-man access-list print where mac-address=$Mac;
- :set Shown ($Shown, $Mac);
-
- :put "\nNumeric id to remove, any key to skip!";
- :local Remove ([ :terminal inkey ] - 48);
- :if ($Remove >= 0 && $Remove <= 9) do={
- :put ("Removing numeric id " . $Remove . "...\n");
- / caps-man access-list remove $Remove;
- }
- }
- }
- }
- :set Seen ($Seen, $Mac);
-}
diff --git a/accesslist-duplicates.capsman.rsc b/accesslist-duplicates.capsman.rsc
new file mode 100644
index 0000000..781ae78
--- /dev/null
+++ b/accesslist-duplicates.capsman.rsc
@@ -0,0 +1,34 @@
+#!rsc by RouterOS
+# RouterOS script: accesslist-duplicates.capsman
+# Copyright (c) 2018-2024 Christian Hesse <mail@eworm.de>
+# https://git.eworm.de/cgit/routeros-scripts/about/COPYING.md
+#
+# requires RouterOS, version=7.13
+#
+# print duplicate antries in wireless access list
+# https://git.eworm.de/cgit/routeros-scripts/about/doc/accesslist-duplicates.md
+#
+# !! Do not edit this file, it is generated from template!
+
+:global GlobalFunctionsReady;
+:while ($GlobalFunctionsReady != true) do={ :delay 500ms; }
+
+:do {
+ :local ScriptName [ :jobname ];
+
+ :local Seen ({});
+
+ :foreach AccList in=[ /caps-man/access-list/find where mac-address!="00:00:00:00:00:00" ] do={
+ :local Mac [ /caps-man/access-list/get $AccList mac-address ];
+ :if ($Seen->$Mac = 1) do={
+ /caps-man/access-list/print where mac-address=$Mac;
+ :local Remove [ :tonum [ /terminal/ask prompt="\nNumeric id to remove, any key to skip!" ] ];
+
+ :if ([ :typeof $Remove ] = "num") do={
+ :put ("Removing numeric id " . $Remove . "...\n");
+ /caps-man/access-list/remove $Remove;
+ }
+ }
+ :set ($Seen->$Mac) 1;
+ }
+} on-error={ }
diff --git a/accesslist-duplicates.local b/accesslist-duplicates.local
deleted file mode 100644
index 4b812a5..0000000
--- a/accesslist-duplicates.local
+++ /dev/null
@@ -1,34 +0,0 @@
-#!rsc
-# RouterOS script: accesslist-duplicates.local
-# Copyright (c) 2018-2020 Christian Hesse <mail@eworm.de>
-#
-# print duplicate antries in wireless access list
-#
-# !! Do not edit this file, it is generated from template!
-
-:local Seen [ :toarray "" ];
-:local Shown [ :toarray "" ];
-
-:foreach AccList in=[ / interface wireless access-list find where mac-address!="00:00:00:00:00:00" ] do={
- :local Mac [ / interface wireless access-list get $AccList mac-address ];
- :foreach SeenMac in=$Seen do={
- :if ($SeenMac = $Mac) do={
- :local Skip 0;
- :foreach ShownMac in=$Shown do={
- :if ($ShownMac = $Mac) do={ :set Skip 1; }
- }
- :if ($Skip = 0) do={
- / interface wireless access-list print where mac-address=$Mac;
- :set Shown ($Shown, $Mac);
-
- :put "\nNumeric id to remove, any key to skip!";
- :local Remove ([ :terminal inkey ] - 48);
- :if ($Remove >= 0 && $Remove <= 9) do={
- :put ("Removing numeric id " . $Remove . "...\n");
- / interface wireless access-list remove $Remove;
- }
- }
- }
- }
- :set Seen ($Seen, $Mac);
-}
diff --git a/accesslist-duplicates.local.rsc b/accesslist-duplicates.local.rsc
new file mode 100644
index 0000000..b79a724
--- /dev/null
+++ b/accesslist-duplicates.local.rsc
@@ -0,0 +1,34 @@
+#!rsc by RouterOS
+# RouterOS script: accesslist-duplicates.local
+# Copyright (c) 2018-2024 Christian Hesse <mail@eworm.de>
+# https://git.eworm.de/cgit/routeros-scripts/about/COPYING.md
+#
+# requires RouterOS, version=7.13
+#
+# print duplicate antries in wireless access list
+# https://git.eworm.de/cgit/routeros-scripts/about/doc/accesslist-duplicates.md
+#
+# !! Do not edit this file, it is generated from template!
+
+:global GlobalFunctionsReady;
+:while ($GlobalFunctionsReady != true) do={ :delay 500ms; }
+
+:do {
+ :local ScriptName [ :jobname ];
+
+ :local Seen ({});
+
+ :foreach AccList in=[ /interface/wireless/access-list/find where mac-address!="00:00:00:00:00:00" ] do={
+ :local Mac [ /interface/wireless/access-list/get $AccList mac-address ];
+ :if ($Seen->$Mac = 1) do={
+ /interface/wireless/access-list/print where mac-address=$Mac;
+ :local Remove [ :tonum [ /terminal/ask prompt="\nNumeric id to remove, any key to skip!" ] ];
+
+ :if ([ :typeof $Remove ] = "num") do={
+ :put ("Removing numeric id " . $Remove . "...\n");
+ /interface/wireless/access-list/remove $Remove;
+ }
+ }
+ :set ($Seen->$Mac) 1;
+ }
+} on-error={ }
diff --git a/accesslist-duplicates.template b/accesslist-duplicates.template
deleted file mode 100644
index 0d1c92c..0000000
--- a/accesslist-duplicates.template
+++ /dev/null
@@ -1,35 +0,0 @@
-#!rsc
-# RouterOS script: accesslist-duplicates%TEMPL%
-# Copyright (c) 2018-2020 Christian Hesse <mail@eworm.de>
-#
-# print duplicate antries in wireless access list
-#
-# !! This is just a template! Replace '%PATH%' with 'caps-man'
-# !! or 'interface wireless'!
-
-:local Seen [ :toarray "" ];
-:local Shown [ :toarray "" ];
-
-:foreach AccList in=[ / %PATH% access-list find where mac-address!="00:00:00:00:00:00" ] do={
- :local Mac [ / %PATH% access-list get $AccList mac-address ];
- :foreach SeenMac in=$Seen do={
- :if ($SeenMac = $Mac) do={
- :local Skip 0;
- :foreach ShownMac in=$Shown do={
- :if ($ShownMac = $Mac) do={ :set Skip 1; }
- }
- :if ($Skip = 0) do={
- / %PATH% access-list print where mac-address=$Mac;
- :set Shown ($Shown, $Mac);
-
- :put "\nNumeric id to remove, any key to skip!";
- :local Remove ([ :terminal inkey ] - 48);
- :if ($Remove >= 0 && $Remove <= 9) do={
- :put ("Removing numeric id " . $Remove . "...\n");
- / %PATH% access-list remove $Remove;
- }
- }
- }
- }
- :set Seen ($Seen, $Mac);
-}
diff --git a/accesslist-duplicates.template.rsc b/accesslist-duplicates.template.rsc
new file mode 100644
index 0000000..b8067c8
--- /dev/null
+++ b/accesslist-duplicates.template.rsc
@@ -0,0 +1,43 @@
+#!rsc by RouterOS
+# RouterOS script: accesslist-duplicates%TEMPL%
+# Copyright (c) 2018-2024 Christian Hesse <mail@eworm.de>
+# https://git.eworm.de/cgit/routeros-scripts/about/COPYING.md
+#
+# requires RouterOS, version=7.13
+#
+# print duplicate antries in wireless access list
+# https://git.eworm.de/cgit/routeros-scripts/about/doc/accesslist-duplicates.md
+#
+# !! This is just a template to generate the real script!
+# !! Pattern '%TEMPL%' is replaced, paths are filtered.
+
+:global GlobalFunctionsReady;
+:while ($GlobalFunctionsReady != true) do={ :delay 500ms; }
+
+:do {
+ :local ScriptName [ :jobname ];
+
+ :local Seen ({});
+
+ :foreach AccList in=[ /caps-man/access-list/find where mac-address!="00:00:00:00:00:00" ] do={
+ :foreach AccList in=[ /interface/wifi/access-list/find where mac-address!="00:00:00:00:00:00" ] do={
+ :foreach AccList in=[ /interface/wireless/access-list/find where mac-address!="00:00:00:00:00:00" ] do={
+ :local Mac [ /caps-man/access-list/get $AccList mac-address ];
+ :local Mac [ /interface/wifi/access-list/get $AccList mac-address ];
+ :local Mac [ /interface/wireless/access-list/get $AccList mac-address ];
+ :if ($Seen->$Mac = 1) do={
+ /caps-man/access-list/print where mac-address=$Mac;
+ /interface/wifi/access-list/print where mac-address=$Mac;
+ /interface/wireless/access-list/print where mac-address=$Mac;
+ :local Remove [ :tonum [ /terminal/ask prompt="\nNumeric id to remove, any key to skip!" ] ];
+
+ :if ([ :typeof $Remove ] = "num") do={
+ :put ("Removing numeric id " . $Remove . "...\n");
+ /caps-man/access-list/remove $Remove;
+ /interface/wifi/access-list/remove $Remove;
+ /interface/wireless/access-list/remove $Remove;
+ }
+ }
+ :set ($Seen->$Mac) 1;
+ }
+} on-error={ }
diff --git a/accesslist-duplicates.wifi.rsc b/accesslist-duplicates.wifi.rsc
new file mode 100644
index 0000000..c05e02c
--- /dev/null
+++ b/accesslist-duplicates.wifi.rsc
@@ -0,0 +1,34 @@
+#!rsc by RouterOS
+# RouterOS script: accesslist-duplicates.wifi
+# Copyright (c) 2018-2024 Christian Hesse <mail@eworm.de>
+# https://git.eworm.de/cgit/routeros-scripts/about/COPYING.md
+#
+# requires RouterOS, version=7.13
+#
+# print duplicate antries in wireless access list
+# https://git.eworm.de/cgit/routeros-scripts/about/doc/accesslist-duplicates.md
+#
+# !! Do not edit this file, it is generated from template!
+
+:global GlobalFunctionsReady;
+:while ($GlobalFunctionsReady != true) do={ :delay 500ms; }
+
+:do {
+ :local ScriptName [ :jobname ];
+
+ :local Seen ({});
+
+ :foreach AccList in=[ /interface/wifi/access-list/find where mac-address!="00:00:00:00:00:00" ] do={
+ :local Mac [ /interface/wifi/access-list/get $AccList mac-address ];
+ :if ($Seen->$Mac = 1) do={
+ /interface/wifi/access-list/print where mac-address=$Mac;
+ :local Remove [ :tonum [ /terminal/ask prompt="\nNumeric id to remove, any key to skip!" ] ];
+
+ :if ([ :typeof $Remove ] = "num") do={
+ :put ("Removing numeric id " . $Remove . "...\n");
+ /interface/wifi/access-list/remove $Remove;
+ }
+ }
+ :set ($Seen->$Mac) 1;
+ }
+} on-error={ }
diff --git a/backup-cloud.rsc b/backup-cloud.rsc
new file mode 100644
index 0000000..88dd345
--- /dev/null
+++ b/backup-cloud.rsc
@@ -0,0 +1,86 @@
+#!rsc by RouterOS
+# RouterOS script: backup-cloud
+# Copyright (c) 2013-2024 Christian Hesse <mail@eworm.de>
+# https://git.eworm.de/cgit/routeros-scripts/about/COPYING.md
+#
+# provides: backup-script, order=40
+# requires RouterOS, version=7.13
+#
+# upload backup to MikroTik cloud
+# https://git.eworm.de/cgit/routeros-scripts/about/doc/backup-cloud.md
+
+:global GlobalFunctionsReady;
+:while ($GlobalFunctionsReady != true) do={ :delay 500ms; }
+
+:do {
+ :local ScriptName [ :jobname ];
+
+ :global BackupRandomDelay;
+ :global Identity;
+ :global PackagesUpdateBackupFailure;
+
+ :global DeviceInfo;
+ :global FormatLine;
+ :global HumanReadableNum;
+ :global LogPrint;
+ :global MkDir;
+ :global RandomDelay;
+ :global ScriptFromTerminal;
+ :global ScriptLock;
+ :global SendNotification2;
+ :global SymbolForNotification;
+ :global WaitForFile;
+ :global WaitFullyConnected;
+
+ :if ([ $ScriptLock $ScriptName ] = false) do={
+ :set PackagesUpdateBackupFailure true;
+ :error false;
+ }
+ $WaitFullyConnected;
+
+ :if ([ $ScriptFromTerminal $ScriptName ] = false && $BackupRandomDelay > 0) do={
+ $RandomDelay $BackupRandomDelay;
+ }
+
+ :if ([ $MkDir ("tmpfs/backup-cloud") ] = false) do={
+ $LogPrint error $ScriptName ("Failed creating directory!");
+ :error false;
+ }
+
+ :local I 5;
+ :do {
+ :execute {
+ :global BackupPassword;
+
+ :local Backup ([ /system/backup/cloud/find ]->0);
+ :if ([ :typeof $Backup ] = "id") do={
+ /system/backup/cloud/upload-file action=create-and-upload \
+ password=$BackupPassword replace=$Backup;
+ } else={
+ /system/backup/cloud/upload-file action=create-and-upload \
+ password=$BackupPassword;
+ }
+ /file/add name="tmpfs/backup-cloud/done";
+ } as-string;
+ :set I ($I - 1);
+ } while=([ $WaitForFile "tmpfs/backup-cloud/done" 200ms ] = false && $I > 0);
+
+ :if ([ $WaitForFile "tmpfs/backup-cloud/done" ] = true) do={
+ :local Cloud [ /system/backup/cloud/get ([ find ]->0) ];
+
+ $SendNotification2 ({ origin=$ScriptName; \
+ subject=([ $SymbolForNotification "floppy-disk,cloud" ] . "Cloud backup"); \
+ message=("Uploaded backup for " . $Identity . " to cloud.\n\n" . \
+ [ $DeviceInfo ] . "\n\n" . \
+ [ $FormatLine "Name" ($Cloud->"name") ] . "\n" . \
+ [ $FormatLine "Size" ([ $HumanReadableNum ($Cloud->"size") 1024 ] . "B") ] . "\n" . \
+ [ $FormatLine "Download key" ($Cloud->"secret-download-key") ]); silent=true });
+ } else={
+ $SendNotification2 ({ origin=$ScriptName; \
+ subject=([ $SymbolForNotification "floppy-disk,warning-sign" ] . "Cloud backup failed"); \
+ message=("Failed uploading backup for " . $Identity . " to cloud!\n\n" . [ $DeviceInfo ]) });
+ $LogPrint error $ScriptName ("Failed uploading backup for " . $Identity . " to cloud!");
+ :set PackagesUpdateBackupFailure true;
+ }
+ /file/remove "tmpfs/backup-cloud";
+} on-error={ }
diff --git a/backup-email.rsc b/backup-email.rsc
new file mode 100644
index 0000000..c32eb27
--- /dev/null
+++ b/backup-email.rsc
@@ -0,0 +1,124 @@
+#!rsc by RouterOS
+# RouterOS script: backup-email
+# Copyright (c) 2013-2024 Christian Hesse <mail@eworm.de>
+# https://git.eworm.de/cgit/routeros-scripts/about/COPYING.md
+#
+# provides: backup-script, order=20
+# requires RouterOS, version=7.13
+#
+# create and email backup and config file
+# https://git.eworm.de/cgit/routeros-scripts/about/doc/backup-email.md
+
+:global GlobalFunctionsReady;
+:while ($GlobalFunctionsReady != true) do={ :delay 500ms; }
+
+:do {
+ :local ScriptName [ :jobname ];
+
+ :global BackupPassword;
+ :global BackupRandomDelay;
+ :global BackupSendBinary;
+ :global BackupSendExport;
+ :global BackupSendGlobalConfig;
+ :global Domain;
+ :global Identity;
+ :global PackagesUpdateBackupFailure;
+
+ :global CleanName;
+ :global DeviceInfo;
+ :global FormatLine;
+ :global LogPrint;
+ :global MkDir;
+ :global RandomDelay;
+ :global ScriptFromTerminal;
+ :global ScriptLock;
+ :global SendEMail2;
+ :global SymbolForNotification;
+ :global WaitForFile;
+ :global WaitFullyConnected;
+
+ :if ([ :typeof $SendEMail2 ] = "nothing") do={
+ $LogPrint error $ScriptName ("The module for sending notifications via e-mail is not installed.");
+ :error false;
+ }
+
+ :if ($BackupSendBinary != true && \
+ $BackupSendExport != true) do={
+ $LogPrint error $ScriptName ("Configured to send neither backup nor config export.");
+ :error false;
+ }
+
+ :if ([ $ScriptLock $ScriptName ] = false) do={
+ :set PackagesUpdateBackupFailure true;
+ :error false;
+ }
+ $WaitFullyConnected;
+
+ :if ([ $ScriptFromTerminal $ScriptName ] = false && $BackupRandomDelay > 0) do={
+ $RandomDelay $BackupRandomDelay;
+ }
+
+ # filename based on identity
+ :local DirName ("tmpfs/" . $ScriptName);
+ :local FileName [ $CleanName ($Identity . "." . $Domain) ];
+ :local FilePath ($DirName . "/" . $FileName);
+ :local BackupFile "none";
+ :local ExportFile "none";
+ :local ConfigFile "none";
+ :local Attach ({});
+
+ :if ([ $MkDir $DirName ] = false) do={
+ $LogPrint error $ScriptName ("Failed creating directory!");
+ :error false;
+ }
+
+ # binary backup
+ :if ($BackupSendBinary = true) do={
+ /system/backup/save encryption=aes-sha256 name=$FilePath password=$BackupPassword;
+ $WaitForFile ($FilePath . ".backup");
+ :set BackupFile ($FileName . ".backup");
+ :set Attach ($Attach, ($FilePath . ".backup"));
+ }
+
+ # create configuration export
+ :if ($BackupSendExport = true) do={
+ /export terse show-sensitive file=$FilePath;
+ $WaitForFile ($FilePath . ".rsc");
+ :set ExportFile ($FileName . ".rsc");
+ :set Attach ($Attach, ($FilePath . ".rsc"));
+ }
+
+ # global-config-overlay
+ :if ($BackupSendGlobalConfig = true) do={
+ # Do *NOT* use '/file/add ...' here, as it is limited to 4095 bytes!
+ :execute script={ :put [ /system/script/get global-config-overlay source ]; } \
+ file=($FilePath . ".conf\00");
+ $WaitForFile ($FilePath . ".conf");
+ :set ConfigFile ($FileName . ".conf");
+ :set Attach ($Attach, ($FilePath . ".conf"));
+ }
+
+ # send email with status and files
+ $SendEMail2 ({ origin=$ScriptName; \
+ subject=([ $SymbolForNotification "floppy-disk,incoming-envelope" ] . \
+ "Backup & Config"); \
+ message=("See attached files for backup and config export for " . \
+ $Identity . ".\n\n" . \
+ [ $DeviceInfo ] . "\n\n" . \
+ [ $FormatLine "Backup file" $BackupFile ] . "\n" . \
+ [ $FormatLine "Export file" $ExportFile ] . "\n" . \
+ [ $FormatLine "Config file" $ConfigFile ]); \
+ attach=$Attach; remove-attach=true });
+
+ # wait for the mail to be sent
+ :local I 0;
+ :while ([ :len [ /file/find where name ~ ($FilePath . "\\.(backup|rsc)\$") ] ] > 0) do={
+ :if ($I >= 120) do={
+ $LogPrint warning $ScriptName ("Files are still available, sending e-mail failed.");
+ :set PackagesUpdateBackupFailure true;
+ :error false;
+ }
+ :delay 1s;
+ :set I ($I + 1);
+ }
+} on-error={ }
diff --git a/backup-partition.rsc b/backup-partition.rsc
new file mode 100644
index 0000000..fc186c0
--- /dev/null
+++ b/backup-partition.rsc
@@ -0,0 +1,57 @@
+#!rsc by RouterOS
+# RouterOS script: backup-partition
+# Copyright (c) 2022-2024 Christian Hesse <mail@eworm.de>
+# https://git.eworm.de/cgit/routeros-scripts/about/COPYING.md
+#
+# provides: backup-script, order=70
+# requires RouterOS, version=7.13
+#
+# save configuration to fallback partition
+# https://git.eworm.de/cgit/routeros-scripts/about/doc/backup-partition.md
+
+:global GlobalFunctionsReady;
+:while ($GlobalFunctionsReady != true) do={ :delay 500ms; }
+
+:do {
+ :local ScriptName [ :jobname ];
+
+ :global PackagesUpdateBackupFailure;
+
+ :global LogPrint;
+ :global ScriptLock;
+
+ :if ([ $ScriptLock $ScriptName ] = false) do={
+ :set PackagesUpdateBackupFailure true;
+ :error false;
+ }
+
+ :if ([ :len [ /partitions/find ] ] < 2) do={
+ $LogPrint error $ScriptName ("Device does not have a fallback partition.");
+ :set PackagesUpdateBackupFailure true;
+ :error false;
+ }
+
+ :local ActiveRunning [ /partitions/find where active running ];
+
+ :if ([ :len $ActiveRunning ] < 1) do={
+ $LogPrint error $ScriptName ("Device is not running from active partition.");
+ :set PackagesUpdateBackupFailure true;
+ :error false;
+ }
+
+ :local FallbackTo [ /partitions/get $ActiveRunning fallback-to ];
+
+ :do {
+ /system/scheduler/add start-time=startup name="running-from-backup-partition" \
+ on-event=(":log warning (\"Running from partition '\" . " . \
+ "[ /partitions/get [ find where running ] name ] . \"'!\")");
+ /partitions/save-config-to $FallbackTo;
+ /system/scheduler/remove "running-from-backup-partition";
+ $LogPrint info $ScriptName ("Saved configuration to partition '" . $FallbackTo . "'.");
+ } on-error={
+ /system/scheduler/remove [ find where name="running-from-backup-partition" ];
+ $LogPrint error $ScriptName ("Failed saving configuration to partition '" . $FallbackTo . "'!");
+ :set PackagesUpdateBackupFailure true;
+ :error false;
+ }
+} on-error={ }
diff --git a/backup-upload.rsc b/backup-upload.rsc
new file mode 100644
index 0000000..1dc98d5
--- /dev/null
+++ b/backup-upload.rsc
@@ -0,0 +1,161 @@
+#!rsc by RouterOS
+# RouterOS script: backup-upload
+# Copyright (c) 2013-2024 Christian Hesse <mail@eworm.de>
+# https://git.eworm.de/cgit/routeros-scripts/about/COPYING.md
+#
+# provides: backup-script, order=50
+# requires RouterOS, version=7.13
+#
+# create and upload backup and config file
+# https://git.eworm.de/cgit/routeros-scripts/about/doc/backup-upload.md
+
+:global GlobalFunctionsReady;
+:while ($GlobalFunctionsReady != true) do={ :delay 500ms; }
+
+:do {
+ :local ScriptName [ :jobname ];
+
+ :global BackupPassword;
+ :global BackupRandomDelay;
+ :global BackupSendBinary;
+ :global BackupSendExport;
+ :global BackupSendGlobalConfig;
+ :global BackupUploadPass;
+ :global BackupUploadUrl;
+ :global BackupUploadUser;
+ :global Domain;
+ :global Identity;
+ :global PackagesUpdateBackupFailure;
+
+ :global CleanName;
+ :global DeviceInfo;
+ :global IfThenElse;
+ :global LogPrint;
+ :global MkDir;
+ :global RandomDelay;
+ :global ScriptFromTerminal;
+ :global ScriptLock;
+ :global SendNotification2;
+ :global SymbolForNotification;
+ :global WaitForFile;
+ :global WaitFullyConnected;
+
+ :if ($BackupSendBinary != true && \
+ $BackupSendExport != true) do={
+ $LogPrint error $ScriptName ("Configured to send neither backup nor config export.");
+ :error false;
+ }
+
+ :if ([ $ScriptLock $ScriptName ] = false) do={
+ :set PackagesUpdateBackupFailure true;
+ :error false;
+ }
+ $WaitFullyConnected;
+
+ :if ([ $ScriptFromTerminal $ScriptName ] = false && $BackupRandomDelay > 0) do={
+ $RandomDelay $BackupRandomDelay;
+ }
+
+ # filename based on identity
+ :local DirName ("tmpfs/" . $ScriptName);
+ :local FileName [ $CleanName ($Identity . "." . $Domain) ];
+ :local FilePath ($DirName . "/" . $FileName);
+ :local BackupFile "none";
+ :local ExportFile "none";
+ :local ConfigFile "none";
+ :local Failed 0;
+
+ :if ([ $MkDir $DirName ] = false) do={
+ $LogPrint error $ScriptName ("Failed creating directory!");
+ :error false;
+ }
+
+ # binary backup
+ :if ($BackupSendBinary = true) do={
+ /system/backup/save encryption=aes-sha256 name=$FilePath password=$BackupPassword;
+ $WaitForFile ($FilePath . ".backup");
+
+ :do {
+ /tool/fetch upload=yes url=($BackupUploadUrl . "/" . $FileName . ".backup") \
+ user=$BackupUploadUser password=$BackupUploadPass src-path=($FilePath . ".backup");
+ :set BackupFile [ /file/get ($FilePath . ".backup") ];
+ :set ($BackupFile->"name") ($FileName . ".backup");
+ } on-error={
+ $LogPrint error $ScriptName ("Uploading backup file failed!");
+ :set BackupFile "failed";
+ :set Failed 1;
+ }
+
+ /file/remove ($FilePath . ".backup");
+ }
+
+ # create configuration export
+ :if ($BackupSendExport = true) do={
+ /export terse show-sensitive file=$FilePath;
+ $WaitForFile ($FilePath . ".rsc");
+
+ :do {
+ /tool/fetch upload=yes url=($BackupUploadUrl . "/" . $FileName . ".rsc") \
+ user=$BackupUploadUser password=$BackupUploadPass src-path=($FilePath . ".rsc");
+ :set ExportFile [ /file/get ($FilePath . ".rsc") ];
+ :set ($ExportFile->"name") ($FileName . ".rsc");
+ } on-error={
+ $LogPrint error $ScriptName ("Uploading configuration export failed!");
+ :set ExportFile "failed";
+ :set Failed 1;
+ }
+
+ /file/remove ($FilePath . ".rsc");
+ }
+
+ # global-config-overlay
+ :if ($BackupSendGlobalConfig = true) do={
+ # Do *NOT* use '/file/add ...' here, as it is limited to 4095 bytes!
+ :execute script={ :put [ /system/script/get global-config-overlay source ]; } \
+ file=($FilePath . ".conf\00");
+ $WaitForFile ($FilePath . ".conf");
+
+ :do {
+ /tool/fetch upload=yes url=($BackupUploadUrl . "/" . $FileName . ".conf") \
+ user=$BackupUploadUser password=$BackupUploadPass src-path=($FilePath . ".conf");
+ :set ConfigFile [ /file/get ($FilePath . ".conf") ];
+ :set ($ConfigFile->"name") ($FileName . ".conf");
+ } on-error={
+ $LogPrint error $ScriptName ("Uploading global-config-overlay failed!");
+ :set ConfigFile "failed";
+ :set Failed 1;
+ }
+
+ /file/remove ($FilePath . ".conf");
+ }
+
+ :local FileInfo do={
+ :local Name $1;
+ :local File $2;
+
+ :global FormatLine;
+ :global HumanReadableNum;
+ :global IfThenElse;
+
+ :return \
+ [ $IfThenElse ([ :typeof $File ] = "array") \
+ ($Name . ":\n" . [ $FormatLine " name" ($File->"name") ] . "\n" . \
+ [ $FormatLine " size" ([ $HumanReadableNum ($File->"size") 1024 ] . "B") ]) \
+ [ $FormatLine $Name $File ] ];
+ }
+
+ $SendNotification2 ({ origin=$ScriptName; \
+ subject=[ $IfThenElse ($Failed > 0) \
+ ([ $SymbolForNotification "floppy-disk,warning-sign" ] . "Backup & Config upload with failure") \
+ ([ $SymbolForNotification "floppy-disk,arrow-up" ] . "Backup & Config upload") ]; \
+ message=("Backup and config export upload for " . $Identity . ".\n\n" . \
+ [ $DeviceInfo ] . "\n\n" . \
+ [ $FileInfo "Backup file" $BackupFile ] . "\n" . \
+ [ $FileInfo "Export file" $ExportFile ] . "\n" . \
+ [ $FileInfo "Config file" $ConfigFile ]); silent=true });
+
+ :if ($Failed = 1) do={
+ :set PackagesUpdateBackupFailure true;
+ }
+ /file/remove $DirName;
+} on-error={ }
diff --git a/bridge-port-to-default b/bridge-port-to-default
deleted file mode 100644
index 5b7fa7e..0000000
--- a/bridge-port-to-default
+++ /dev/null
@@ -1,42 +0,0 @@
-#!rsc
-# RouterOS script: bridge-port-to-default
-# Copyright (c) 2013-2020 Christian Hesse <mail@eworm.de>
-#
-# reset bridge ports to default bridge
-
-:global BridgePortTo;
-
-:local Len ([ :len $BridgePortTo ] + 1);
-
-:if ($Len = 1) do={
- :delay 1s;
- :set Len ([ :len $BridgePortTo ] + 1);
-}
-
-:foreach BridgePort in=[ / interface bridge port find where comment!="" ] do={
- :local BridgePortVal [ / interface bridge port get $BridgePort ];
- :foreach Comment in=[ :toarray ($BridgePortVal->"comment") ] do={
- :if ([ :pick $Comment 0 $Len ] = ($BridgePortTo . ":")) do={
- :local BridgeDefault [ :pick $Comment $Len [ :len $Comment ] ];
- :if ($BridgeDefault = "dhcp-client") do={
- :if ($BridgePortVal->"disabled" = false) do={
- :log info ("Disabling bridge port for interface " . $BridgePortVal->"interface" . ", enabling dhcp client.");
- / interface bridge port disable $BridgePort;
- / ip dhcp-client enable [ find where interface=$BridgePortVal->"interface" comment="toggle with bridge port" disabled=yes ];
- }
- } else={
- :if ($BridgePortVal->"disabled" = true) do={
- :log info ("Enabling bridge port for interface " . $BridgePortVal->"interface" . ", disabling dhcp client.");
- / ip dhcp-client disable [ find where interface=$BridgePortVal->"interface" comment="toggle with bridge port" disabled=no ];
- / interface bridge port enable $BridgePort;
- }
- :if ($BridgeDefault != $BridgePortVal->"bridge") do={
- :log info ("Changing interface " . $BridgePortVal->"interface" . " to " . $BridgePortTo . " bridge " . $BridgeDefault);
- / interface bridge port set bridge=$BridgeDefault $BridgePort;
- } else={
- :log debug ("Interface " . $BridgePortVal->"interface" . " already connected to " . $BridgePortTo . " bridge " . $BridgeDefault);
- }
- }
- }
- }
-}
diff --git a/bridge-port-toggle b/bridge-port-toggle
deleted file mode 100644
index b6ca4cd..0000000
--- a/bridge-port-toggle
+++ /dev/null
@@ -1,15 +0,0 @@
-#!rsc
-# RouterOS script: bridge-port-toggle
-# Copyright (c) 2013-2020 Christian Hesse <mail@eworm.de>
-#
-# toggle bridge ports between default and alt bridge
-
-:global BridgePortTo;
-
-:if ($BridgePortTo != "default") do={
- :set BridgePortTo "default";
-} else={
- :set BridgePortTo "alt";
-}
-
-/ system script run bridge-port-to-default;
diff --git a/capsman-download-packages b/capsman-download-packages
deleted file mode 100644
index c76ceee..0000000
--- a/capsman-download-packages
+++ /dev/null
@@ -1,39 +0,0 @@
-#!rsc
-# RouterOS script: capsman-download-packages
-# Copyright (c) 2018-2020 Christian Hesse <mail@eworm.de>
-# Michael Gisbers <michael@gisbers.de>
-#
-# download and cleanup packages for CAP installation from CAPsMAN
-
-:global CleanFilePath;
-:global DownloadPackage;
-:global ScriptLock;
-
-$ScriptLock "capsman-download-packages";
-
-:local PackagePath [ $CleanFilePath [ / caps-man manager get package-path ] ];
-:local InstalledVersion [ / system package update get installed-version ];
-:local Updated false;
-
-:foreach Package in=[ / file find where type=package \
- package-version!=$InstalledVersion name~("^" . $PackagePath) ] do={
- :local File [ / file get $Package ];
- :if ($File->"package-architecture" = "mips") do={
- :set ($File->"package-architecture") "mipsbe";
- }
- :if ($File->"package-name" = "wireless@") do={
- :set ($File->"package-name") "wireless";
- }
- :if ([ $DownloadPackage ($File->"package-name") $InstalledVersion ($File->"package-architecture") $PackagePath ] = true) do={
- :set Updated true;
- / file remove $Package;
- }
-}
-
-:if ($Updated = true) do={
- :if ([ / system script print count-only where name="capsman-rolling-upgrade" ] > 0) do={
- / system script run capsman-rolling-upgrade;
- } else={
- / caps-man remote-cap upgrade [ find where version!=$InstalledVersion ];
- }
-}
diff --git a/capsman-download-packages.capsman.rsc b/capsman-download-packages.capsman.rsc
new file mode 100644
index 0000000..f5695f4
--- /dev/null
+++ b/capsman-download-packages.capsman.rsc
@@ -0,0 +1,83 @@
+#!rsc by RouterOS
+# RouterOS script: capsman-download-packages.capsman
+# Copyright (c) 2018-2024 Christian Hesse <mail@eworm.de>
+# Michael Gisbers <michael@gisbers.de>
+# https://git.eworm.de/cgit/routeros-scripts/about/COPYING.md
+#
+# requires RouterOS, version=7.13
+#
+# download and cleanup packages for CAP installation from CAPsMAN
+# https://git.eworm.de/cgit/routeros-scripts/about/doc/capsman-download-packages.md
+#
+# !! Do not edit this file, it is generated from template!
+
+:global GlobalFunctionsReady;
+:while ($GlobalFunctionsReady != true) do={ :delay 500ms; }
+
+:do {
+ :local ScriptName [ :jobname ];
+
+ :global CleanFilePath;
+ :global DownloadPackage;
+ :global LogPrint;
+ :global MkDir;
+ :global ScriptLock;
+ :global WaitFullyConnected;
+
+ :if ([ $ScriptLock $ScriptName ] = false) do={
+ :error false;
+ }
+ $WaitFullyConnected;
+
+ :local PackagePath [ $CleanFilePath [ /caps-man/manager/get package-path ] ];
+ :local InstalledVersion [ /system/package/update/get installed-version ];
+ :local Updated false;
+
+ :if ([ :len $PackagePath ] = 0) do={
+ $LogPrint warning $ScriptName ("The CAPsMAN package path is not defined, can not download packages.");
+ :error false;
+ }
+
+ :if ([ :len [ /file/find where name=$PackagePath type="directory" ] ] = 0) do={
+ :if ([ $MkDir $PackagePath ] = false) do={
+ $LogPrint warning $ScriptName ("Creating directory at CAPsMAN package path (" . \
+ $PackagePath . ") failed!");
+ :error false;
+ }
+ $LogPrint info $ScriptName ("Created directory at CAPsMAN package path (" . $PackagePath . \
+ "). Please place your packages!");
+ }
+
+ :foreach Package in=[ /file/find where type=package \
+ package-version!=$InstalledVersion name~("^" . $PackagePath) ] do={
+ :local File [ /file/get $Package ];
+ :if ($File->"package-architecture" = "mips") do={
+ :set ($File->"package-architecture") "mipsbe";
+ }
+ :if ([ $DownloadPackage ($File->"package-name") $InstalledVersion \
+ ($File->"package-architecture") $PackagePath ] = true) do={
+ :set Updated true;
+ /file/remove $Package;
+ }
+ }
+
+ :if ([ :len [ /file/find where type=package name~("^" . $PackagePath) ] ] = 0) do={
+ $LogPrint info $ScriptName ("No packages available, downloading default set.");
+ :foreach Arch in={ "arm"; "mipsbe" } do={
+ :foreach Package in={ "routeros"; "wireless" } do={
+ :if ([ $DownloadPackage $Package $InstalledVersion $Arch $PackagePath ] = true) do={
+ :set Updated true;
+ }
+ }
+ }
+ }
+
+ :if ($Updated = true) do={
+ :local Script ([ /system/script/find where source~"\n# provides: capsman-rolling-upgrade\n" ]->0);
+ :if ([ :len $Script ] > 0) do={
+ /system/script/run $Script;
+ } else={
+ /caps-man/remote-cap/upgrade [ find where version!=$InstalledVersion ];
+ }
+ }
+} on-error={ }
diff --git a/capsman-download-packages.template.rsc b/capsman-download-packages.template.rsc
new file mode 100644
index 0000000..762dbb6
--- /dev/null
+++ b/capsman-download-packages.template.rsc
@@ -0,0 +1,94 @@
+#!rsc by RouterOS
+# RouterOS script: capsman-download-packages%TEMPL%
+# Copyright (c) 2018-2024 Christian Hesse <mail@eworm.de>
+# Michael Gisbers <michael@gisbers.de>
+# https://git.eworm.de/cgit/routeros-scripts/about/COPYING.md
+#
+# requires RouterOS, version=7.13
+#
+# download and cleanup packages for CAP installation from CAPsMAN
+# https://git.eworm.de/cgit/routeros-scripts/about/doc/capsman-download-packages.md
+#
+# !! This is just a template to generate the real script!
+# !! Pattern '%TEMPL%' is replaced, paths are filtered.
+
+:global GlobalFunctionsReady;
+:while ($GlobalFunctionsReady != true) do={ :delay 500ms; }
+
+:do {
+ :local ScriptName [ :jobname ];
+
+ :global CleanFilePath;
+ :global DownloadPackage;
+ :global LogPrint;
+ :global MkDir;
+ :global ScriptLock;
+ :global WaitFullyConnected;
+
+ :if ([ $ScriptLock $ScriptName ] = false) do={
+ :error false;
+ }
+ $WaitFullyConnected;
+
+ :local PackagePath [ $CleanFilePath [ /caps-man/manager/get package-path ] ];
+ :local PackagePath [ $CleanFilePath [ /interface/wifi/capsman/get package-path ] ];
+ :local InstalledVersion [ /system/package/update/get installed-version ];
+ :local Updated false;
+
+ :if ([ :len $PackagePath ] = 0) do={
+ $LogPrint warning $ScriptName ("The CAPsMAN package path is not defined, can not download packages.");
+ :error false;
+ }
+
+ :if ([ :len [ /file/find where name=$PackagePath type="directory" ] ] = 0) do={
+ :if ([ $MkDir $PackagePath ] = false) do={
+ $LogPrint warning $ScriptName ("Creating directory at CAPsMAN package path (" . \
+ $PackagePath . ") failed!");
+ :error false;
+ }
+ $LogPrint info $ScriptName ("Created directory at CAPsMAN package path (" . $PackagePath . \
+ "). Please place your packages!");
+ }
+
+ :foreach Package in=[ /file/find where type=package \
+ package-version!=$InstalledVersion name~("^" . $PackagePath) ] do={
+ :local File [ /file/get $Package ];
+ :if ($File->"package-architecture" = "mips") do={
+ :set ($File->"package-architecture") "mipsbe";
+ }
+ :if ([ $DownloadPackage ($File->"package-name") $InstalledVersion \
+ ($File->"package-architecture") $PackagePath ] = true) do={
+ :set Updated true;
+ /file/remove $Package;
+ }
+ }
+
+ :if ([ :len [ /file/find where type=package name~("^" . $PackagePath) ] ] = 0) do={
+ $LogPrint info $ScriptName ("No packages available, downloading default set.");
+# NOT /interface/wifi/ #
+ :foreach Arch in={ "arm"; "mipsbe" } do={
+ :foreach Package in={ "routeros"; "wireless" } do={
+# NOT /interface/wifi/ #
+# NOT /caps-man/ #
+ :foreach Arch in={ "arm"; "arm64" } do={
+ :local Packages { "arm"={ "routeros"; "wifi-qcom"; "wifi-qcom-ac" };
+ "arm64"={ "routeros"; "wifi-qcom" } };
+ :foreach Package in=($Packages->$Arch) do={
+# NOT /caps-man/ #
+ :if ([ $DownloadPackage $Package $InstalledVersion $Arch $PackagePath ] = true) do={
+ :set Updated true;
+ }
+ }
+ }
+ }
+
+ :if ($Updated = true) do={
+ :local Script ([ /system/script/find where source~"\n# provides: capsman-rolling-upgrade\n" ]->0);
+ :if ([ :len $Script ] > 0) do={
+ /system/script/run $Script;
+ } else={
+ /caps-man/remote-cap/upgrade [ find where version!=$InstalledVersion ];
+ /interface/wifi/capsman/remote-cap/upgrade [ find where version!=$InstalledVersion ];
+ }
+ }
+} on-error={ }
diff --git a/capsman-download-packages.wifi.rsc b/capsman-download-packages.wifi.rsc
new file mode 100644
index 0000000..79aa9a7
--- /dev/null
+++ b/capsman-download-packages.wifi.rsc
@@ -0,0 +1,85 @@
+#!rsc by RouterOS
+# RouterOS script: capsman-download-packages.wifi
+# Copyright (c) 2018-2024 Christian Hesse <mail@eworm.de>
+# Michael Gisbers <michael@gisbers.de>
+# https://git.eworm.de/cgit/routeros-scripts/about/COPYING.md
+#
+# requires RouterOS, version=7.13
+#
+# download and cleanup packages for CAP installation from CAPsMAN
+# https://git.eworm.de/cgit/routeros-scripts/about/doc/capsman-download-packages.md
+#
+# !! Do not edit this file, it is generated from template!
+
+:global GlobalFunctionsReady;
+:while ($GlobalFunctionsReady != true) do={ :delay 500ms; }
+
+:do {
+ :local ScriptName [ :jobname ];
+
+ :global CleanFilePath;
+ :global DownloadPackage;
+ :global LogPrint;
+ :global MkDir;
+ :global ScriptLock;
+ :global WaitFullyConnected;
+
+ :if ([ $ScriptLock $ScriptName ] = false) do={
+ :error false;
+ }
+ $WaitFullyConnected;
+
+ :local PackagePath [ $CleanFilePath [ /interface/wifi/capsman/get package-path ] ];
+ :local InstalledVersion [ /system/package/update/get installed-version ];
+ :local Updated false;
+
+ :if ([ :len $PackagePath ] = 0) do={
+ $LogPrint warning $ScriptName ("The CAPsMAN package path is not defined, can not download packages.");
+ :error false;
+ }
+
+ :if ([ :len [ /file/find where name=$PackagePath type="directory" ] ] = 0) do={
+ :if ([ $MkDir $PackagePath ] = false) do={
+ $LogPrint warning $ScriptName ("Creating directory at CAPsMAN package path (" . \
+ $PackagePath . ") failed!");
+ :error false;
+ }
+ $LogPrint info $ScriptName ("Created directory at CAPsMAN package path (" . $PackagePath . \
+ "). Please place your packages!");
+ }
+
+ :foreach Package in=[ /file/find where type=package \
+ package-version!=$InstalledVersion name~("^" . $PackagePath) ] do={
+ :local File [ /file/get $Package ];
+ :if ($File->"package-architecture" = "mips") do={
+ :set ($File->"package-architecture") "mipsbe";
+ }
+ :if ([ $DownloadPackage ($File->"package-name") $InstalledVersion \
+ ($File->"package-architecture") $PackagePath ] = true) do={
+ :set Updated true;
+ /file/remove $Package;
+ }
+ }
+
+ :if ([ :len [ /file/find where type=package name~("^" . $PackagePath) ] ] = 0) do={
+ $LogPrint info $ScriptName ("No packages available, downloading default set.");
+ :foreach Arch in={ "arm"; "arm64" } do={
+ :local Packages { "arm"={ "routeros"; "wifi-qcom"; "wifi-qcom-ac" };
+ "arm64"={ "routeros"; "wifi-qcom" } };
+ :foreach Package in=($Packages->$Arch) do={
+ :if ([ $DownloadPackage $Package $InstalledVersion $Arch $PackagePath ] = true) do={
+ :set Updated true;
+ }
+ }
+ }
+ }
+
+ :if ($Updated = true) do={
+ :local Script ([ /system/script/find where source~"\n# provides: capsman-rolling-upgrade\n" ]->0);
+ :if ([ :len $Script ] > 0) do={
+ /system/script/run $Script;
+ } else={
+ /interface/wifi/capsman/remote-cap/upgrade [ find where version!=$InstalledVersion ];
+ }
+ }
+} on-error={ }
diff --git a/capsman-rolling-upgrade b/capsman-rolling-upgrade
deleted file mode 100644
index cedfc47..0000000
--- a/capsman-rolling-upgrade
+++ /dev/null
@@ -1,25 +0,0 @@
-#!rsc
-# RouterOS script: capsman-rolling-upgrade
-# Copyright (c) 2018-2020 Christian Hesse <mail@eworm.de>
-# Michael Gisbers <michael@gisbers.de>
-#
-# upgrade CAPs one after another
-
-:global ScriptLock;
-
-$ScriptLock "capsman-rolling-upgrade";
-
-:local InstalledVersion [ / system package update get installed-version ];
-
-:local RemoteCapCount [ /caps-man remote-cap print count-only ];
-:if ($RemoteCapCount > 0) do={
- :local Delay (600 / $RemoteCapCount);
- :if ($Delay > 120) do={ :set Delay 120; }
- :foreach RemoteCap in=[ / caps-man remote-cap find where version!=$InstalledVersion ] do={
- :local RemoteCapVal [ / caps-man remote-cap get $RemoteCap ];
- :log info ("Starting upgrade for " . $RemoteCapVal->"name" . \
- " (" . $RemoteCapVal->"identity" . ")...");
- / caps-man remote-cap upgrade [ find where name=$RemoteCapVal->"name" ];
- :delay ($Delay . "s");
- }
-}
diff --git a/capsman-rolling-upgrade.capsman.rsc b/capsman-rolling-upgrade.capsman.rsc
new file mode 100644
index 0000000..2c9ae3d
--- /dev/null
+++ b/capsman-rolling-upgrade.capsman.rsc
@@ -0,0 +1,46 @@
+#!rsc by RouterOS
+# RouterOS script: capsman-rolling-upgrade.capsman
+# Copyright (c) 2018-2024 Christian Hesse <mail@eworm.de>
+# Michael Gisbers <michael@gisbers.de>
+# https://git.eworm.de/cgit/routeros-scripts/about/COPYING.md
+#
+# provides: capsman-rolling-upgrade
+# requires RouterOS, version=7.13
+#
+# upgrade CAPs one after another
+# https://git.eworm.de/cgit/routeros-scripts/about/doc/capsman-rolling-upgrade.md
+#
+# !! Do not edit this file, it is generated from template!
+
+:global GlobalFunctionsReady;
+:while ($GlobalFunctionsReady != true) do={ :delay 500ms; }
+
+:do {
+ :local ScriptName [ :jobname ];
+
+ :global LogPrint;
+ :global ScriptLock;
+
+ :if ([ $ScriptLock $ScriptName ] = false) do={
+ :error false;
+ }
+
+ :local InstalledVersion [ /system/package/update/get installed-version ];
+
+ :local RemoteCapCount [ :len [ /caps-man/remote-cap/find ] ];
+ :if ($RemoteCapCount > 0) do={
+ :local Delay (600 / $RemoteCapCount);
+ :if ($Delay > 120) do={ :set Delay 120; }
+ :foreach RemoteCap in=[ /caps-man/remote-cap/find where version!=$InstalledVersion ] do={
+ :local RemoteCapVal [ /caps-man/remote-cap/get $RemoteCap ];
+ :if ([ :len $RemoteCapVal ] > 1) do={
+ $LogPrint info $ScriptName ("Starting upgrade for " . $RemoteCapVal->"name" . \
+ " (" . $RemoteCapVal->"identity" . ")...");
+ /caps-man/remote-cap/upgrade $RemoteCap;
+ } else={
+ $LogPrint warning $ScriptName ("Remote CAP vanished, skipping upgrade.");
+ }
+ :delay ($Delay . "s");
+ }
+ }
+} on-error={ }
diff --git a/capsman-rolling-upgrade.template.rsc b/capsman-rolling-upgrade.template.rsc
new file mode 100644
index 0000000..2098bc0
--- /dev/null
+++ b/capsman-rolling-upgrade.template.rsc
@@ -0,0 +1,54 @@
+#!rsc by RouterOS
+# RouterOS script: capsman-rolling-upgrade%TEMPL%
+# Copyright (c) 2018-2024 Christian Hesse <mail@eworm.de>
+# Michael Gisbers <michael@gisbers.de>
+# https://git.eworm.de/cgit/routeros-scripts/about/COPYING.md
+#
+# provides: capsman-rolling-upgrade
+# requires RouterOS, version=7.13
+#
+# upgrade CAPs one after another
+# https://git.eworm.de/cgit/routeros-scripts/about/doc/capsman-rolling-upgrade.md
+#
+# !! This is just a template to generate the real script!
+# !! Pattern '%TEMPL%' is replaced, paths are filtered.
+
+:global GlobalFunctionsReady;
+:while ($GlobalFunctionsReady != true) do={ :delay 500ms; }
+
+:do {
+ :local ScriptName [ :jobname ];
+
+ :global LogPrint;
+ :global ScriptLock;
+
+ :if ([ $ScriptLock $ScriptName ] = false) do={
+ :error false;
+ }
+
+ :local InstalledVersion [ /system/package/update/get installed-version ];
+
+ :local RemoteCapCount [ :len [ /caps-man/remote-cap/find ] ];
+ :local RemoteCapCount [ :len [ /interface/wifi/capsman/remote-cap/find ] ];
+ :if ($RemoteCapCount > 0) do={
+ :local Delay (600 / $RemoteCapCount);
+ :if ($Delay > 120) do={ :set Delay 120; }
+ :foreach RemoteCap in=[ /caps-man/remote-cap/find where version!=$InstalledVersion ] do={
+ :foreach RemoteCap in=[ /interface/wifi/capsman/remote-cap/find where version!=$InstalledVersion ] do={
+ :local RemoteCapVal [ /caps-man/remote-cap/get $RemoteCap ];
+ :local RemoteCapVal [ /interface/wifi/capsman/remote-cap/get $RemoteCap ];
+ :if ([ :len $RemoteCapVal ] > 1) do={
+# NOT /caps-man/ #
+ :set ($RemoteCapVal->"name") ($RemoteCapVal->"common-name");
+# NOT /caps-man/ #
+ $LogPrint info $ScriptName ("Starting upgrade for " . $RemoteCapVal->"name" . \
+ " (" . $RemoteCapVal->"identity" . ")...");
+ /caps-man/remote-cap/upgrade $RemoteCap;
+ /interface/wifi/capsman/remote-cap/upgrade $RemoteCap;
+ } else={
+ $LogPrint warning $ScriptName ("Remote CAP vanished, skipping upgrade.");
+ }
+ :delay ($Delay . "s");
+ }
+ }
+} on-error={ }
diff --git a/capsman-rolling-upgrade.wifi.rsc b/capsman-rolling-upgrade.wifi.rsc
new file mode 100644
index 0000000..36b8c0f
--- /dev/null
+++ b/capsman-rolling-upgrade.wifi.rsc
@@ -0,0 +1,47 @@
+#!rsc by RouterOS
+# RouterOS script: capsman-rolling-upgrade.wifi
+# Copyright (c) 2018-2024 Christian Hesse <mail@eworm.de>
+# Michael Gisbers <michael@gisbers.de>
+# https://git.eworm.de/cgit/routeros-scripts/about/COPYING.md
+#
+# provides: capsman-rolling-upgrade
+# requires RouterOS, version=7.13
+#
+# upgrade CAPs one after another
+# https://git.eworm.de/cgit/routeros-scripts/about/doc/capsman-rolling-upgrade.md
+#
+# !! Do not edit this file, it is generated from template!
+
+:global GlobalFunctionsReady;
+:while ($GlobalFunctionsReady != true) do={ :delay 500ms; }
+
+:do {
+ :local ScriptName [ :jobname ];
+
+ :global LogPrint;
+ :global ScriptLock;
+
+ :if ([ $ScriptLock $ScriptName ] = false) do={
+ :error false;
+ }
+
+ :local InstalledVersion [ /system/package/update/get installed-version ];
+
+ :local RemoteCapCount [ :len [ /interface/wifi/capsman/remote-cap/find ] ];
+ :if ($RemoteCapCount > 0) do={
+ :local Delay (600 / $RemoteCapCount);
+ :if ($Delay > 120) do={ :set Delay 120; }
+ :foreach RemoteCap in=[ /interface/wifi/capsman/remote-cap/find where version!=$InstalledVersion ] do={
+ :local RemoteCapVal [ /interface/wifi/capsman/remote-cap/get $RemoteCap ];
+ :if ([ :len $RemoteCapVal ] > 1) do={
+ :set ($RemoteCapVal->"name") ($RemoteCapVal->"common-name");
+ $LogPrint info $ScriptName ("Starting upgrade for " . $RemoteCapVal->"name" . \
+ " (" . $RemoteCapVal->"identity" . ")...");
+ /interface/wifi/capsman/remote-cap/upgrade $RemoteCap;
+ } else={
+ $LogPrint warning $ScriptName ("Remote CAP vanished, skipping upgrade.");
+ }
+ :delay ($Delay . "s");
+ }
+ }
+} on-error={ }
diff --git a/certificate-renew-issued b/certificate-renew-issued
deleted file mode 100644
index 3dc50d3..0000000
--- a/certificate-renew-issued
+++ /dev/null
@@ -1,14 +0,0 @@
-#!rsc
-# RouterOS script: certificate-renew-issued
-# Copyright (c) 2019-2020 Christian Hesse <mail@eworm.de>
-#
-# renew locally issued certificates
-
-:foreach Cert in=[ / certificate find where issued expires-after<3w ] do={
- :local CertVal [ / certificate get $Cert ];
- / certificate issued-revoke $Cert;
- / certificate set name=($CertVal->"name" . "-revoked-" . [ / system clock get date ]) $Cert;
- / certificate add name=($CertVal->"name") common-name=($CertVal->"common-name") \
- key-usage=($CertVal->"key-usage") subject-alt-name=($CertVal->"subject-alt-name");
- / certificate sign ($CertVal->"name") ca=($CertVal->"ca");
-}
diff --git a/certificate-renew-issued.rsc b/certificate-renew-issued.rsc
new file mode 100644
index 0000000..7815443
--- /dev/null
+++ b/certificate-renew-issued.rsc
@@ -0,0 +1,48 @@
+#!rsc by RouterOS
+# RouterOS script: certificate-renew-issued
+# Copyright (c) 2019-2024 Christian Hesse <mail@eworm.de>
+# https://git.eworm.de/cgit/routeros-scripts/about/COPYING.md
+#
+# requires RouterOS, version=7.13
+#
+# renew locally issued certificates
+# https://git.eworm.de/cgit/routeros-scripts/about/doc/certificate-renew-issued.md
+
+:global GlobalFunctionsReady;
+:while ($GlobalFunctionsReady != true) do={ :delay 500ms; }
+
+:do {
+ :local ScriptName [ :jobname ];
+
+ :global CertIssuedExportPass;
+
+ :global LogPrint;
+ :global MkDir;
+ :global ScriptLock;
+
+ :if ([ $ScriptLock $ScriptName ] = false) do={
+ :error false;
+ }
+
+ :foreach Cert in=[ /certificate/find where issued expires-after<3w ] do={
+ :local CertVal [ /certificate/get $Cert ];
+ /certificate/issued-revoke $Cert;
+ /certificate/set name=($CertVal->"name" . "-revoked-" . [ /system/clock/get date ]) $Cert;
+ /certificate/add name=($CertVal->"name") common-name=($CertVal->"common-name") \
+ key-usage=($CertVal->"key-usage") subject-alt-name=($CertVal->"subject-alt-name");
+ /certificate/sign ($CertVal->"name") ca=($CertVal->"ca");
+ :if ([ :typeof ($CertIssuedExportPass->($CertVal->"common-name")) ] = "str") do={
+ :if ([ $MkDir "cert-issued" ] = true) do={
+ /certificate/export-certificate ($CertVal->"name") type=pkcs12 \
+ file-name=("cert-issued/" . $CertVal->"common-name") \
+ export-passphrase=($CertIssuedExportPass->($CertVal->"common-name"));
+ $LogPrint info $ScriptName ("Issued a new certificate for '" . $CertVal->"common-name" . \
+ "', exported to 'cert-issued/" . $CertVal->"common-name" . ".p12'.");
+ } else={
+ $LogPrint warning $ScriptName ("Failed creating directory, not exporting certificate.");
+ }
+ } else={
+ $LogPrint info $ScriptName ("Issued a new certificate for '" . $CertVal->"common-name" . "'.");
+ }
+ }
+} on-error={ }
diff --git a/certs/Cloudflare-Inc-ECC-CA-3.pem b/certs/Cloudflare-Inc-ECC-CA-3.pem
new file mode 100644
index 0000000..fa91603
--- /dev/null
+++ b/certs/Cloudflare-Inc-ECC-CA-3.pem
@@ -0,0 +1,163 @@
+Certificate:
+ Data:
+ Version: 3 (0x2)
+ Serial Number:
+ 0a:37:87:64:5e:5f:b4:8c:22:4e:fd:1b:ed:14:0c:3c
+ Signature Algorithm: sha256WithRSAEncryption
+ Issuer: C = IE, O = Baltimore, OU = CyberTrust, CN = Baltimore CyberTrust Root
+ Validity
+ Not Before: Jan 27 12:48:08 2020 GMT
+ Not After : Dec 31 23:59:59 2024 GMT
+ Subject: C = US, O = "Cloudflare, Inc.", CN = Cloudflare Inc ECC CA-3
+ Subject Public Key Info:
+ Public Key Algorithm: id-ecPublicKey
+ Public-Key: (256 bit)
+ pub:
+ 04:b9:ad:4d:66:99:14:0b:46:ec:1f:81:d1:2a:50:
+ 1e:9d:03:15:2f:34:12:7d:2d:96:b8:88:38:9b:85:
+ 5f:8f:bf:bb:4d:ef:61:46:c4:c9:73:d4:24:4f:e0:
+ ee:1c:ce:6c:b3:51:71:2f:6a:ee:4c:05:09:77:d3:
+ 72:62:a4:9b:d7
+ ASN1 OID: prime256v1
+ NIST CURVE: P-256
+ X509v3 extensions:
+ X509v3 Subject Key Identifier:
+ A5:CE:37:EA:EB:B0:75:0E:94:67:88:B4:45:FA:D9:24:10:87:96:1F
+ X509v3 Authority Key Identifier:
+ E5:9D:59:30:82:47:58:CC:AC:FA:08:54:36:86:7B:3A:B5:04:4D:F0
+ X509v3 Key Usage: critical
+ Digital Signature, Certificate Sign, CRL Sign
+ X509v3 Extended Key Usage:
+ TLS Web Server Authentication, TLS Web Client Authentication
+ X509v3 Basic Constraints: critical
+ CA:TRUE, pathlen:0
+ Authority Information Access:
+ OCSP - URI:http://ocsp.digicert.com
+ X509v3 CRL Distribution Points:
+ Full Name:
+ URI:http://crl3.digicert.com/Omniroot2025.crl
+ X509v3 Certificate Policies:
+ Policy: 2.16.840.1.114412.1.1
+ CPS: https://www.digicert.com/CPS
+ Policy: 2.16.840.1.114412.1.2
+ Policy: 2.23.140.1.2.1
+ Policy: 2.23.140.1.2.2
+ Policy: 2.23.140.1.2.3
+ Signature Algorithm: sha256WithRSAEncryption
+ Signature Value:
+ 05:24:1d:dd:1b:b0:2a:eb:98:d6:85:e3:39:4d:5e:6b:57:9d:
+ 82:57:fc:eb:e8:31:a2:57:90:65:05:be:16:44:38:5a:77:02:
+ b9:cf:10:42:c6:e1:92:a4:e3:45:27:f8:00:47:2c:68:a8:56:
+ 99:53:54:8f:ad:9e:40:c1:d0:0f:b6:d7:0d:0b:38:48:6c:50:
+ 2c:49:90:06:5b:64:1d:8b:cc:48:30:2e:de:08:e2:9b:49:22:
+ c0:92:0c:11:5e:96:92:94:d5:fc:20:dc:56:6c:e5:92:93:bf:
+ 7a:1c:c0:37:e3:85:49:15:fa:2b:e1:74:39:18:0f:b7:da:f3:
+ a2:57:58:60:4f:cc:8e:94:00:fc:46:7b:34:31:3e:4d:47:82:
+ 81:3a:cb:f4:89:5d:0e:ef:4d:0d:6e:9c:1b:82:24:dd:32:25:
+ 5d:11:78:51:10:3d:a0:35:23:04:2f:65:6f:9c:c1:d1:43:d7:
+ d0:1e:f3:31:67:59:27:dd:6b:d2:75:09:93:11:24:24:14:cf:
+ 29:be:e6:23:c3:b8:8f:72:3f:e9:07:c8:24:44:53:7a:b3:b9:
+ 61:65:a1:4c:0e:c6:48:00:c9:75:63:05:87:70:45:52:83:d3:
+ 95:9d:45:ea:f0:e8:31:1d:7e:09:1f:0a:fe:3e:dd:aa:3c:5e:
+ 74:d2:ac:b1
+-----BEGIN CERTIFICATE-----
+MIIDzTCCArWgAwIBAgIQCjeHZF5ftIwiTv0b7RQMPDANBgkqhkiG9w0BAQsFADBa
+MQswCQYDVQQGEwJJRTESMBAGA1UEChMJQmFsdGltb3JlMRMwEQYDVQQLEwpDeWJl
+clRydXN0MSIwIAYDVQQDExlCYWx0aW1vcmUgQ3liZXJUcnVzdCBSb290MB4XDTIw
+MDEyNzEyNDgwOFoXDTI0MTIzMTIzNTk1OVowSjELMAkGA1UEBhMCVVMxGTAXBgNV
+BAoTEENsb3VkZmxhcmUsIEluYy4xIDAeBgNVBAMTF0Nsb3VkZmxhcmUgSW5jIEVD
+QyBDQS0zMFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEua1NZpkUC0bsH4HRKlAe
+nQMVLzQSfS2WuIg4m4Vfj7+7Te9hRsTJc9QkT+DuHM5ss1FxL2ruTAUJd9NyYqSb
+16OCAWgwggFkMB0GA1UdDgQWBBSlzjfq67B1DpRniLRF+tkkEIeWHzAfBgNVHSME
+GDAWgBTlnVkwgkdYzKz6CFQ2hns6tQRN8DAOBgNVHQ8BAf8EBAMCAYYwHQYDVR0l
+BBYwFAYIKwYBBQUHAwEGCCsGAQUFBwMCMBIGA1UdEwEB/wQIMAYBAf8CAQAwNAYI
+KwYBBQUHAQEEKDAmMCQGCCsGAQUFBzABhhhodHRwOi8vb2NzcC5kaWdpY2VydC5j
+b20wOgYDVR0fBDMwMTAvoC2gK4YpaHR0cDovL2NybDMuZGlnaWNlcnQuY29tL09t
+bmlyb290MjAyNS5jcmwwbQYDVR0gBGYwZDA3BglghkgBhv1sAQEwKjAoBggrBgEF
+BQcCARYcaHR0cHM6Ly93d3cuZGlnaWNlcnQuY29tL0NQUzALBglghkgBhv1sAQIw
+CAYGZ4EMAQIBMAgGBmeBDAECAjAIBgZngQwBAgMwDQYJKoZIhvcNAQELBQADggEB
+AAUkHd0bsCrrmNaF4zlNXmtXnYJX/OvoMaJXkGUFvhZEOFp3ArnPEELG4ZKk40Un
++ABHLGioVplTVI+tnkDB0A+21w0LOEhsUCxJkAZbZB2LzEgwLt4I4ptJIsCSDBFe
+lpKU1fwg3FZs5ZKTv3ocwDfjhUkV+ivhdDkYD7fa86JXWGBPzI6UAPxGezQxPk1H
+goE6y/SJXQ7vTQ1unBuCJN0yJV0ReFEQPaA1IwQvZW+cwdFD19Ae8zFnWSfda9J1
+CZMRJCQUzym+5iPDuI9yP+kHyCREU3qzuWFloUwOxkgAyXVjBYdwRVKD05WdRerw
+6DEdfgkfCv4+3ao8XnTSrLE=
+-----END CERTIFICATE-----
+Certificate:
+ Data:
+ Version: 3 (0x2)
+ Serial Number: 33554617 (0x20000b9)
+ Signature Algorithm: sha1WithRSAEncryption
+ Issuer: C = IE, O = Baltimore, OU = CyberTrust, CN = Baltimore CyberTrust Root
+ Validity
+ Not Before: May 12 18:46:00 2000 GMT
+ Not After : May 12 23:59:00 2025 GMT
+ Subject: C = IE, O = Baltimore, OU = CyberTrust, CN = Baltimore CyberTrust Root
+ Subject Public Key Info:
+ Public Key Algorithm: rsaEncryption
+ Public-Key: (2048 bit)
+ Modulus:
+ 00:a3:04:bb:22:ab:98:3d:57:e8:26:72:9a:b5:79:
+ d4:29:e2:e1:e8:95:80:b1:b0:e3:5b:8e:2b:29:9a:
+ 64:df:a1:5d:ed:b0:09:05:6d:db:28:2e:ce:62:a2:
+ 62:fe:b4:88:da:12:eb:38:eb:21:9d:c0:41:2b:01:
+ 52:7b:88:77:d3:1c:8f:c7:ba:b9:88:b5:6a:09:e7:
+ 73:e8:11:40:a7:d1:cc:ca:62:8d:2d:e5:8f:0b:a6:
+ 50:d2:a8:50:c3:28:ea:f5:ab:25:87:8a:9a:96:1c:
+ a9:67:b8:3f:0c:d5:f7:f9:52:13:2f:c2:1b:d5:70:
+ 70:f0:8f:c0:12:ca:06:cb:9a:e1:d9:ca:33:7a:77:
+ d6:f8:ec:b9:f1:68:44:42:48:13:d2:c0:c2:a4:ae:
+ 5e:60:fe:b6:a6:05:fc:b4:dd:07:59:02:d4:59:18:
+ 98:63:f5:a5:63:e0:90:0c:7d:5d:b2:06:7a:f3:85:
+ ea:eb:d4:03:ae:5e:84:3e:5f:ff:15:ed:69:bc:f9:
+ 39:36:72:75:cf:77:52:4d:f3:c9:90:2c:b9:3d:e5:
+ c9:23:53:3f:1f:24:98:21:5c:07:99:29:bd:c6:3a:
+ ec:e7:6e:86:3a:6b:97:74:63:33:bd:68:18:31:f0:
+ 78:8d:76:bf:fc:9e:8e:5d:2a:86:a7:4d:90:dc:27:
+ 1a:39
+ Exponent: 65537 (0x10001)
+ X509v3 extensions:
+ X509v3 Subject Key Identifier:
+ E5:9D:59:30:82:47:58:CC:AC:FA:08:54:36:86:7B:3A:B5:04:4D:F0
+ X509v3 Basic Constraints: critical
+ CA:TRUE, pathlen:3
+ X509v3 Key Usage: critical
+ Certificate Sign, CRL Sign
+ Signature Algorithm: sha1WithRSAEncryption
+ Signature Value:
+ 85:0c:5d:8e:e4:6f:51:68:42:05:a0:dd:bb:4f:27:25:84:03:
+ bd:f7:64:fd:2d:d7:30:e3:a4:10:17:eb:da:29:29:b6:79:3f:
+ 76:f6:19:13:23:b8:10:0a:f9:58:a4:d4:61:70:bd:04:61:6a:
+ 12:8a:17:d5:0a:bd:c5:bc:30:7c:d6:e9:0c:25:8d:86:40:4f:
+ ec:cc:a3:7e:38:c6:37:11:4f:ed:dd:68:31:8e:4c:d2:b3:01:
+ 74:ee:be:75:5e:07:48:1a:7f:70:ff:16:5c:84:c0:79:85:b8:
+ 05:fd:7f:be:65:11:a3:0f:c0:02:b4:f8:52:37:39:04:d5:a9:
+ 31:7a:18:bf:a0:2a:f4:12:99:f7:a3:45:82:e3:3c:5e:f5:9d:
+ 9e:b5:c8:9e:7c:2e:c8:a4:9e:4e:08:14:4b:6d:fd:70:6d:6b:
+ 1a:63:bd:64:e6:1f:b7:ce:f0:f2:9f:2e:bb:1b:b7:f2:50:88:
+ 73:92:c2:e2:e3:16:8d:9a:32:02:ab:8e:18:dd:e9:10:11:ee:
+ 7e:35:ab:90:af:3e:30:94:7a:d0:33:3d:a7:65:0f:f5:fc:8e:
+ 9e:62:cf:47:44:2c:01:5d:bb:1d:b5:32:d2:47:d2:38:2e:d0:
+ fe:81:dc:32:6a:1e:b5:ee:3c:d5:fc:e7:81:1d:19:c3:24:42:
+ ea:63:39:a9
+-----BEGIN CERTIFICATE-----
+MIIDdzCCAl+gAwIBAgIEAgAAuTANBgkqhkiG9w0BAQUFADBaMQswCQYDVQQGEwJJ
+RTESMBAGA1UEChMJQmFsdGltb3JlMRMwEQYDVQQLEwpDeWJlclRydXN0MSIwIAYD
+VQQDExlCYWx0aW1vcmUgQ3liZXJUcnVzdCBSb290MB4XDTAwMDUxMjE4NDYwMFoX
+DTI1MDUxMjIzNTkwMFowWjELMAkGA1UEBhMCSUUxEjAQBgNVBAoTCUJhbHRpbW9y
+ZTETMBEGA1UECxMKQ3liZXJUcnVzdDEiMCAGA1UEAxMZQmFsdGltb3JlIEN5YmVy
+VHJ1c3QgUm9vdDCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAKMEuyKr
+mD1X6CZymrV51Cni4eiVgLGw41uOKymaZN+hXe2wCQVt2yguzmKiYv60iNoS6zjr
+IZ3AQSsBUnuId9Mcj8e6uYi1agnnc+gRQKfRzMpijS3ljwumUNKoUMMo6vWrJYeK
+mpYcqWe4PwzV9/lSEy/CG9VwcPCPwBLKBsua4dnKM3p31vjsufFoREJIE9LAwqSu
+XmD+tqYF/LTdB1kC1FkYmGP1pWPgkAx9XbIGevOF6uvUA65ehD5f/xXtabz5OTZy
+dc93Uk3zyZAsuT3lySNTPx8kmCFcB5kpvcY67Oduhjprl3RjM71oGDHweI12v/ye
+jl0qhqdNkNwnGjkCAwEAAaNFMEMwHQYDVR0OBBYEFOWdWTCCR1jMrPoIVDaGezq1
+BE3wMBIGA1UdEwEB/wQIMAYBAf8CAQMwDgYDVR0PAQH/BAQDAgEGMA0GCSqGSIb3
+DQEBBQUAA4IBAQCFDF2O5G9RaEIFoN27TyclhAO992T9Ldcw46QQF+vaKSm2eT92
+9hkTI7gQCvlYpNRhcL0EYWoSihfVCr3FvDB81ukMJY2GQE/szKN+OMY3EU/t3Wgx
+jkzSswF07r51XgdIGn9w/xZchMB5hbgF/X++ZRGjD8ACtPhSNzkE1akxehi/oCr0
+Epn3o0WC4zxe9Z2etciefC7IpJ5OCBRLbf1wbWsaY71k5h+3zvDyny67G7fyUIhz
+ksLi4xaNmjICq44Y3ekQEe5+NauQrz4wlHrQMz2nZQ/1/I6eYs9HRCwBXbsdtTLS
+R9I4LtD+gdwyah617jzV/OeBHRnDJELqYzmp
+-----END CERTIFICATE-----
diff --git a/certs/DigiCert-Global-G2-TLS-RSA-SHA256-2020-CA1.pem b/certs/DigiCert-Global-G2-TLS-RSA-SHA256-2020-CA1.pem
new file mode 100644
index 0000000..12084ee
--- /dev/null
+++ b/certs/DigiCert-Global-G2-TLS-RSA-SHA256-2020-CA1.pem
@@ -0,0 +1,182 @@
+Certificate:
+ Data:
+ Version: 3 (0x2)
+ Serial Number:
+ 0c:f5:bd:06:2b:56:02:f4:7a:b8:50:2c:23:cc:f0:66
+ Signature Algorithm: sha256WithRSAEncryption
+ Issuer: C=US, O=DigiCert Inc, OU=www.digicert.com, CN=DigiCert Global Root G2
+ Validity
+ Not Before: Mar 30 00:00:00 2021 GMT
+ Not After : Mar 29 23:59:59 2031 GMT
+ Subject: C=US, O=DigiCert Inc, CN=DigiCert Global G2 TLS RSA SHA256 2020 CA1
+ Subject Public Key Info:
+ Public Key Algorithm: rsaEncryption
+ Public-Key: (2048 bit)
+ Modulus:
+ 00:cc:f7:10:62:4f:a6:bb:63:6f:ed:90:52:56:c5:
+ 6d:27:7b:7a:12:56:8a:f1:f4:f9:d6:e7:e1:8f:bd:
+ 95:ab:f2:60:41:15:70:db:12:00:fa:27:0a:b5:57:
+ 38:5b:7d:b2:51:93:71:95:0e:6a:41:94:5b:35:1b:
+ fa:7b:fa:bb:c5:be:24:30:fe:56:ef:c4:f3:7d:97:
+ e3:14:f5:14:4d:cb:a7:10:f2:16:ea:ab:22:f0:31:
+ 22:11:61:69:90:26:ba:78:d9:97:1f:e3:7d:66:ab:
+ 75:44:95:73:c8:ac:ff:ef:5d:0a:8a:59:43:e1:ac:
+ b2:3a:0f:f3:48:fc:d7:6b:37:c1:63:dc:de:46:d6:
+ db:45:fe:7d:23:fd:90:e8:51:07:1e:51:a3:5f:ed:
+ 49:46:54:7f:2c:88:c5:f4:13:9c:97:15:3c:03:e8:
+ a1:39:dc:69:0c:32:c1:af:16:57:4c:94:47:42:7c:
+ a2:c8:9c:7d:e6:d4:4d:54:af:42:99:a8:c1:04:c2:
+ 77:9c:d6:48:e4:ce:11:e0:2a:80:99:f0:43:70:cf:
+ 3f:76:6b:d1:4c:49:ab:24:5e:c2:0d:82:fd:46:a8:
+ ab:6c:93:cc:62:52:42:75:92:f8:9a:fa:5e:5e:b2:
+ b0:61:e5:1f:1f:b9:7f:09:98:e8:3d:fa:83:7f:47:
+ 69:a1
+ Exponent: 65537 (0x10001)
+ X509v3 extensions:
+ X509v3 Basic Constraints: critical
+ CA:TRUE, pathlen:0
+ X509v3 Subject Key Identifier:
+ 74:85:80:C0:66:C7:DF:37:DE:CF:BD:29:37:AA:03:1D:BE:ED:CD:17
+ X509v3 Authority Key Identifier:
+ 4E:22:54:20:18:95:E6:E3:6E:E6:0F:FA:FA:B9:12:ED:06:17:8F:39
+ X509v3 Key Usage: critical
+ Digital Signature, Certificate Sign, CRL Sign
+ X509v3 Extended Key Usage:
+ TLS Web Server Authentication, TLS Web Client Authentication
+ Authority Information Access:
+ OCSP - URI:http://ocsp.digicert.com
+ CA Issuers - URI:http://cacerts.digicert.com/DigiCertGlobalRootG2.crt
+ X509v3 CRL Distribution Points:
+ Full Name:
+ URI:http://crl3.digicert.com/DigiCertGlobalRootG2.crl
+ X509v3 Certificate Policies:
+ Policy: 2.16.840.1.114412.2.1
+ Policy: 2.23.140.1.1
+ Policy: 2.23.140.1.2.1
+ Policy: 2.23.140.1.2.2
+ Policy: 2.23.140.1.2.3
+ Signature Algorithm: sha256WithRSAEncryption
+ Signature Value:
+ 90:f1:70:cb:28:97:69:97:7c:74:fd:c0:fa:26:7b:53:ab:ad:
+ cd:65:fd:ba:9c:06:9c:8a:d7:5a:43:87:ed:4d:4c:56:5f:ad:
+ c1:c5:b5:05:20:2e:59:d1:ff:4a:f5:a0:2a:d8:b0:95:ad:c9:
+ 2e:4a:3b:d7:a7:f6:6f:88:29:fc:30:3f:24:84:bb:c3:b7:7b:
+ 93:07:2c:af:87:6b:76:33:ed:00:55:52:b2:59:9e:e4:b9:d0:
+ f3:df:e7:0f:fe:dd:f8:c4:b9:10:72:81:09:04:5f:cf:97:9e:
+ 2e:32:75:8e:cf:9a:58:d2:57:31:7e:37:01:81:b2:66:6d:29:
+ 1a:b1:66:09:6d:d1:6e:90:f4:b9:fa:2f:01:14:c5:5c:56:64:
+ 01:d9:7d:87:a8:38:53:9f:8b:5d:46:6d:5c:c6:27:84:81:d4:
+ 7e:8c:8c:a3:9b:52:e7:c6:88:ec:37:7c:2a:fb:f0:55:5a:38:
+ 72:10:d8:00:13:cf:4c:73:db:aa:37:35:a8:29:81:69:9c:76:
+ bc:de:18:7b:90:d4:ca:cf:ef:67:03:fd:04:5a:21:16:b1:ff:
+ ea:3f:df:dc:82:f5:eb:f4:59:92:23:0d:24:2a:95:25:4c:ca:
+ a1:91:e6:d4:b7:ac:87:74:b3:f1:6d:a3:99:db:f9:d5:bd:84:
+ 40:9f:07:98
+-----BEGIN CERTIFICATE-----
+MIIEyDCCA7CgAwIBAgIQDPW9BitWAvR6uFAsI8zwZjANBgkqhkiG9w0BAQsFADBh
+MQswCQYDVQQGEwJVUzEVMBMGA1UEChMMRGlnaUNlcnQgSW5jMRkwFwYDVQQLExB3
+d3cuZGlnaWNlcnQuY29tMSAwHgYDVQQDExdEaWdpQ2VydCBHbG9iYWwgUm9vdCBH
+MjAeFw0yMTAzMzAwMDAwMDBaFw0zMTAzMjkyMzU5NTlaMFkxCzAJBgNVBAYTAlVT
+MRUwEwYDVQQKEwxEaWdpQ2VydCBJbmMxMzAxBgNVBAMTKkRpZ2lDZXJ0IEdsb2Jh
+bCBHMiBUTFMgUlNBIFNIQTI1NiAyMDIwIENBMTCCASIwDQYJKoZIhvcNAQEBBQAD
+ggEPADCCAQoCggEBAMz3EGJPprtjb+2QUlbFbSd7ehJWivH0+dbn4Y+9lavyYEEV
+cNsSAPonCrVXOFt9slGTcZUOakGUWzUb+nv6u8W+JDD+Vu/E832X4xT1FE3LpxDy
+FuqrIvAxIhFhaZAmunjZlx/jfWardUSVc8is/+9dCopZQ+GssjoP80j812s3wWPc
+3kbW20X+fSP9kOhRBx5Ro1/tSUZUfyyIxfQTnJcVPAPooTncaQwywa8WV0yUR0J8
+osicfebUTVSvQpmowQTCd5zWSOTOEeAqgJnwQ3DPP3Zr0UxJqyRewg2C/Uaoq2yT
+zGJSQnWS+Jr6Xl6ysGHlHx+5fwmY6D36g39HaaECAwEAAaOCAYIwggF+MBIGA1Ud
+EwEB/wQIMAYBAf8CAQAwHQYDVR0OBBYEFHSFgMBmx9833s+9KTeqAx2+7c0XMB8G
+A1UdIwQYMBaAFE4iVCAYlebjbuYP+vq5Eu0GF485MA4GA1UdDwEB/wQEAwIBhjAd
+BgNVHSUEFjAUBggrBgEFBQcDAQYIKwYBBQUHAwIwdgYIKwYBBQUHAQEEajBoMCQG
+CCsGAQUFBzABhhhodHRwOi8vb2NzcC5kaWdpY2VydC5jb20wQAYIKwYBBQUHMAKG
+NGh0dHA6Ly9jYWNlcnRzLmRpZ2ljZXJ0LmNvbS9EaWdpQ2VydEdsb2JhbFJvb3RH
+Mi5jcnQwQgYDVR0fBDswOTA3oDWgM4YxaHR0cDovL2NybDMuZGlnaWNlcnQuY29t
+L0RpZ2lDZXJ0R2xvYmFsUm9vdEcyLmNybDA9BgNVHSAENjA0MAsGCWCGSAGG/WwC
+ATAHBgVngQwBATAIBgZngQwBAgEwCAYGZ4EMAQICMAgGBmeBDAECAzANBgkqhkiG
+9w0BAQsFAAOCAQEAkPFwyyiXaZd8dP3A+iZ7U6utzWX9upwGnIrXWkOH7U1MVl+t
+wcW1BSAuWdH/SvWgKtiwla3JLko716f2b4gp/DA/JIS7w7d7kwcsr4drdjPtAFVS
+slme5LnQ89/nD/7d+MS5EHKBCQRfz5eeLjJ1js+aWNJXMX43AYGyZm0pGrFmCW3R
+bpD0ufovARTFXFZkAdl9h6g4U5+LXUZtXMYnhIHUfoyMo5tS58aI7Dd8KvvwVVo4
+chDYABPPTHPbqjc1qCmBaZx2vN4Ye5DUys/vZwP9BFohFrH/6j/f3IL16/RZkiMN
+JCqVJUzKoZHm1Lesh3Sz8W2jmdv51b2EQJ8HmA==
+-----END CERTIFICATE-----
+Certificate:
+ Data:
+ Version: 3 (0x2)
+ Serial Number:
+ 03:3a:f1:e6:a7:11:a9:a0:bb:28:64:b1:1d:09:fa:e5
+ Signature Algorithm: sha256WithRSAEncryption
+ Issuer: C=US, O=DigiCert Inc, OU=www.digicert.com, CN=DigiCert Global Root G2
+ Validity
+ Not Before: Aug 1 12:00:00 2013 GMT
+ Not After : Jan 15 12:00:00 2038 GMT
+ Subject: C=US, O=DigiCert Inc, OU=www.digicert.com, CN=DigiCert Global Root G2
+ Subject Public Key Info:
+ Public Key Algorithm: rsaEncryption
+ Public-Key: (2048 bit)
+ Modulus:
+ 00:bb:37:cd:34:dc:7b:6b:c9:b2:68:90:ad:4a:75:
+ ff:46:ba:21:0a:08:8d:f5:19:54:c9:fb:88:db:f3:
+ ae:f2:3a:89:91:3c:7a:e6:ab:06:1a:6b:cf:ac:2d:
+ e8:5e:09:24:44:ba:62:9a:7e:d6:a3:a8:7e:e0:54:
+ 75:20:05:ac:50:b7:9c:63:1a:6c:30:dc:da:1f:19:
+ b1:d7:1e:de:fd:d7:e0:cb:94:83:37:ae:ec:1f:43:
+ 4e:dd:7b:2c:d2:bd:2e:a5:2f:e4:a9:b8:ad:3a:d4:
+ 99:a4:b6:25:e9:9b:6b:00:60:92:60:ff:4f:21:49:
+ 18:f7:67:90:ab:61:06:9c:8f:f2:ba:e9:b4:e9:92:
+ 32:6b:b5:f3:57:e8:5d:1b:cd:8c:1d:ab:95:04:95:
+ 49:f3:35:2d:96:e3:49:6d:dd:77:e3:fb:49:4b:b4:
+ ac:55:07:a9:8f:95:b3:b4:23:bb:4c:6d:45:f0:f6:
+ a9:b2:95:30:b4:fd:4c:55:8c:27:4a:57:14:7c:82:
+ 9d:cd:73:92:d3:16:4a:06:0c:8c:50:d1:8f:1e:09:
+ be:17:a1:e6:21:ca:fd:83:e5:10:bc:83:a5:0a:c4:
+ 67:28:f6:73:14:14:3d:46:76:c3:87:14:89:21:34:
+ 4d:af:0f:45:0c:a6:49:a1:ba:bb:9c:c5:b1:33:83:
+ 29:85
+ Exponent: 65537 (0x10001)
+ X509v3 extensions:
+ X509v3 Basic Constraints: critical
+ CA:TRUE
+ X509v3 Key Usage: critical
+ Digital Signature, Certificate Sign, CRL Sign
+ X509v3 Subject Key Identifier:
+ 4E:22:54:20:18:95:E6:E3:6E:E6:0F:FA:FA:B9:12:ED:06:17:8F:39
+ Signature Algorithm: sha256WithRSAEncryption
+ Signature Value:
+ 60:67:28:94:6f:0e:48:63:eb:31:dd:ea:67:18:d5:89:7d:3c:
+ c5:8b:4a:7f:e9:be:db:2b:17:df:b0:5f:73:77:2a:32:13:39:
+ 81:67:42:84:23:f2:45:67:35:ec:88:bf:f8:8f:b0:61:0c:34:
+ a4:ae:20:4c:84:c6:db:f8:35:e1:76:d9:df:a6:42:bb:c7:44:
+ 08:86:7f:36:74:24:5a:da:6c:0d:14:59:35:bd:f2:49:dd:b6:
+ 1f:c9:b3:0d:47:2a:3d:99:2f:bb:5c:bb:b5:d4:20:e1:99:5f:
+ 53:46:15:db:68:9b:f0:f3:30:d5:3e:31:e2:8d:84:9e:e3:8a:
+ da:da:96:3e:35:13:a5:5f:f0:f9:70:50:70:47:41:11:57:19:
+ 4e:c0:8f:ae:06:c4:95:13:17:2f:1b:25:9f:75:f2:b1:8e:99:
+ a1:6f:13:b1:41:71:fe:88:2a:c8:4f:10:20:55:d7:f3:14:45:
+ e5:e0:44:f4:ea:87:95:32:93:0e:fe:53:46:fa:2c:9d:ff:8b:
+ 22:b9:4b:d9:09:45:a4:de:a4:b8:9a:58:dd:1b:7d:52:9f:8e:
+ 59:43:88:81:a4:9e:26:d5:6f:ad:dd:0d:c6:37:7d:ed:03:92:
+ 1b:e5:77:5f:76:ee:3c:8d:c4:5d:56:5b:a2:d9:66:6e:b3:35:
+ 37:e5:32:b6
+-----BEGIN CERTIFICATE-----
+MIIDjjCCAnagAwIBAgIQAzrx5qcRqaC7KGSxHQn65TANBgkqhkiG9w0BAQsFADBh
+MQswCQYDVQQGEwJVUzEVMBMGA1UEChMMRGlnaUNlcnQgSW5jMRkwFwYDVQQLExB3
+d3cuZGlnaWNlcnQuY29tMSAwHgYDVQQDExdEaWdpQ2VydCBHbG9iYWwgUm9vdCBH
+MjAeFw0xMzA4MDExMjAwMDBaFw0zODAxMTUxMjAwMDBaMGExCzAJBgNVBAYTAlVT
+MRUwEwYDVQQKEwxEaWdpQ2VydCBJbmMxGTAXBgNVBAsTEHd3dy5kaWdpY2VydC5j
+b20xIDAeBgNVBAMTF0RpZ2lDZXJ0IEdsb2JhbCBSb290IEcyMIIBIjANBgkqhkiG
+9w0BAQEFAAOCAQ8AMIIBCgKCAQEAuzfNNNx7a8myaJCtSnX/RrohCgiN9RlUyfuI
+2/Ou8jqJkTx65qsGGmvPrC3oXgkkRLpimn7Wo6h+4FR1IAWsULecYxpsMNzaHxmx
+1x7e/dfgy5SDN67sH0NO3Xss0r0upS/kqbitOtSZpLYl6ZtrAGCSYP9PIUkY92eQ
+q2EGnI/yuum06ZIya7XzV+hdG82MHauVBJVJ8zUtluNJbd134/tJS7SsVQepj5Wz
+tCO7TG1F8PapspUwtP1MVYwnSlcUfIKdzXOS0xZKBgyMUNGPHgm+F6HmIcr9g+UQ
+vIOlCsRnKPZzFBQ9RnbDhxSJITRNrw9FDKZJobq7nMWxM4MphQIDAQABo0IwQDAP
+BgNVHRMBAf8EBTADAQH/MA4GA1UdDwEB/wQEAwIBhjAdBgNVHQ4EFgQUTiJUIBiV
+5uNu5g/6+rkS7QYXjzkwDQYJKoZIhvcNAQELBQADggEBAGBnKJRvDkhj6zHd6mcY
+1Yl9PMWLSn/pvtsrF9+wX3N3KjITOYFnQoQj8kVnNeyIv/iPsGEMNKSuIEyExtv4
+NeF22d+mQrvHRAiGfzZ0JFrabA0UWTW98kndth/Jsw1HKj2ZL7tcu7XUIOGZX1NG
+Fdtom/DzMNU+MeKNhJ7jitralj41E6Vf8PlwUHBHQRFXGU7Aj64GxJUTFy8bJZ91
+8rGOmaFvE7FBcf6IKshPECBV1/MUReXgRPTqh5Uykw7+U0b6LJ3/iyK5S9kJRaTe
+pLiaWN0bfVKfjllDiIGknibVb63dDcY3fe0Dkhvld1927jyNxF1WW6LZZm6zNTfl
+MrY=
+-----END CERTIFICATE-----
diff --git a/certs/DigiCert-TLS-Hybrid-ECC-SHA384-2020-CA1.pem b/certs/DigiCert-TLS-Hybrid-ECC-SHA384-2020-CA1.pem
new file mode 100644
index 0000000..446f56f
--- /dev/null
+++ b/certs/DigiCert-TLS-Hybrid-ECC-SHA384-2020-CA1.pem
@@ -0,0 +1,174 @@
+Certificate:
+ Data:
+ Version: 3 (0x2)
+ Serial Number:
+ 07:f2:f3:5c:87:a8:77:af:7a:ef:e9:47:99:35:25:bd
+ Signature Algorithm: sha384WithRSAEncryption
+ Issuer: C = US, O = DigiCert Inc, OU = www.digicert.com, CN = DigiCert Global Root CA
+ Validity
+ Not Before: Apr 14 00:00:00 2021 GMT
+ Not After : Apr 13 23:59:59 2031 GMT
+ Subject: C = US, O = DigiCert Inc, CN = DigiCert TLS Hybrid ECC SHA384 2020 CA1
+ Subject Public Key Info:
+ Public Key Algorithm: id-ecPublicKey
+ Public-Key: (384 bit)
+ pub:
+ 04:c1:1b:c6:9a:5b:98:d9:a4:29:a0:e9:d4:04:b5:
+ db:eb:a6:b2:6c:55:c0:ff:ed:98:c6:49:2f:06:27:
+ 51:cb:bf:70:c1:05:7a:c3:b1:9d:87:89:ba:ad:b4:
+ 13:17:c9:a8:b4:83:c8:b8:90:d1:cc:74:35:36:3c:
+ 83:72:b0:b5:d0:f7:22:69:c8:f1:80:c4:7b:40:8f:
+ cf:68:87:26:5c:39:89:f1:4d:91:4d:da:89:8b:e4:
+ 03:c3:43:e5:bf:2f:73
+ ASN1 OID: secp384r1
+ NIST CURVE: P-384
+ X509v3 extensions:
+ X509v3 Basic Constraints: critical
+ CA:TRUE, pathlen:0
+ X509v3 Subject Key Identifier:
+ 0A:BC:08:29:17:8C:A5:39:6D:7A:0E:CE:33:C7:2E:B3:ED:FB:C3:7A
+ X509v3 Authority Key Identifier:
+ keyid:03:DE:50:35:56:D1:4C:BB:66:F0:A3:E2:1B:1B:C3:97:B2:3D:D1:55
+
+ X509v3 Key Usage: critical
+ Digital Signature, Certificate Sign, CRL Sign
+ X509v3 Extended Key Usage:
+ TLS Web Server Authentication, TLS Web Client Authentication
+ Authority Information Access:
+ OCSP - URI:http://ocsp.digicert.com
+ CA Issuers - URI:http://cacerts.digicert.com/DigiCertGlobalRootCA.crt
+
+ X509v3 CRL Distribution Points:
+
+ Full Name:
+ URI:http://crl3.digicert.com/DigiCertGlobalRootCA.crl
+
+ X509v3 Certificate Policies:
+ Policy: 2.16.840.1.114412.2.1
+ Policy: 2.23.140.1.1
+ Policy: 2.23.140.1.2.1
+ Policy: 2.23.140.1.2.2
+ Policy: 2.23.140.1.2.3
+
+ Signature Algorithm: sha384WithRSAEncryption
+ 47:59:81:7f:d4:1b:1f:b0:71:f6:98:5d:18:ba:98:47:98:b0:
+ 7e:76:2b:ea:ff:1a:8b:ac:26:b3:42:8d:31:e6:4a:e8:19:d0:
+ ef:da:14:e7:d7:14:92:a1:92:f2:a7:2e:2d:af:fb:1d:f6:fb:
+ 53:b0:8a:3f:fc:d8:16:0a:e9:b0:2e:b6:a5:0b:18:90:35:26:
+ a2:da:f6:a8:b7:32:fc:95:23:4b:c6:45:b9:c4:cf:e4:7c:ee:
+ e6:c9:f8:90:bd:72:e3:99:c3:1d:0b:05:7c:6a:97:6d:b2:ab:
+ 02:36:d8:c2:bc:2c:01:92:3f:04:a3:8b:75:11:c7:b9:29:bc:
+ 11:d0:86:ba:92:bc:26:f9:65:c8:37:cd:26:f6:86:13:0c:04:
+ aa:89:e5:78:b1:c1:4e:79:bc:76:a3:0b:51:e4:c5:d0:9e:6a:
+ fe:1a:2c:56:ae:06:36:27:a3:73:1c:08:7d:93:32:d0:c2:44:
+ 19:da:8d:f4:0e:7b:1d:28:03:2b:09:8a:76:ca:77:dc:87:7a:
+ ac:7b:52:26:55:a7:72:0f:9d:d2:88:4f:fe:b1:21:c5:1a:a1:
+ aa:39:f5:56:db:c2:84:c4:35:1f:70:da:bb:46:f0:86:bf:64:
+ 00:c4:3e:f7:9f:46:1b:9d:23:05:b9:7d:b3:4f:0f:a9:45:3a:
+ e3:74:30:98
+-----BEGIN CERTIFICATE-----
+MIIEFzCCAv+gAwIBAgIQB/LzXIeod6967+lHmTUlvTANBgkqhkiG9w0BAQwFADBh
+MQswCQYDVQQGEwJVUzEVMBMGA1UEChMMRGlnaUNlcnQgSW5jMRkwFwYDVQQLExB3
+d3cuZGlnaWNlcnQuY29tMSAwHgYDVQQDExdEaWdpQ2VydCBHbG9iYWwgUm9vdCBD
+QTAeFw0yMTA0MTQwMDAwMDBaFw0zMTA0MTMyMzU5NTlaMFYxCzAJBgNVBAYTAlVT
+MRUwEwYDVQQKEwxEaWdpQ2VydCBJbmMxMDAuBgNVBAMTJ0RpZ2lDZXJ0IFRMUyBI
+eWJyaWQgRUNDIFNIQTM4NCAyMDIwIENBMTB2MBAGByqGSM49AgEGBSuBBAAiA2IA
+BMEbxppbmNmkKaDp1AS12+umsmxVwP/tmMZJLwYnUcu/cMEFesOxnYeJuq20ExfJ
+qLSDyLiQ0cx0NTY8g3KwtdD3ImnI8YDEe0CPz2iHJlw5ifFNkU3aiYvkA8ND5b8v
+c6OCAYIwggF+MBIGA1UdEwEB/wQIMAYBAf8CAQAwHQYDVR0OBBYEFAq8CCkXjKU5
+bXoOzjPHLrPt+8N6MB8GA1UdIwQYMBaAFAPeUDVW0Uy7ZvCj4hsbw5eyPdFVMA4G
+A1UdDwEB/wQEAwIBhjAdBgNVHSUEFjAUBggrBgEFBQcDAQYIKwYBBQUHAwIwdgYI
+KwYBBQUHAQEEajBoMCQGCCsGAQUFBzABhhhodHRwOi8vb2NzcC5kaWdpY2VydC5j
+b20wQAYIKwYBBQUHMAKGNGh0dHA6Ly9jYWNlcnRzLmRpZ2ljZXJ0LmNvbS9EaWdp
+Q2VydEdsb2JhbFJvb3RDQS5jcnQwQgYDVR0fBDswOTA3oDWgM4YxaHR0cDovL2Ny
+bDMuZGlnaWNlcnQuY29tL0RpZ2lDZXJ0R2xvYmFsUm9vdENBLmNybDA9BgNVHSAE
+NjA0MAsGCWCGSAGG/WwCATAHBgVngQwBATAIBgZngQwBAgEwCAYGZ4EMAQICMAgG
+BmeBDAECAzANBgkqhkiG9w0BAQwFAAOCAQEAR1mBf9QbH7Bx9phdGLqYR5iwfnYr
+6v8ai6wms0KNMeZK6BnQ79oU59cUkqGS8qcuLa/7Hfb7U7CKP/zYFgrpsC62pQsY
+kDUmotr2qLcy/JUjS8ZFucTP5Hzu5sn4kL1y45nDHQsFfGqXbbKrAjbYwrwsAZI/
+BKOLdRHHuSm8EdCGupK8JvllyDfNJvaGEwwEqonleLHBTnm8dqMLUeTF0J5q/hos
+Vq4GNiejcxwIfZMy0MJEGdqN9A57HSgDKwmKdsp33Id6rHtSJlWncg+d0ohP/rEh
+xRqhqjn1VtvChMQ1H3Dau0bwhr9kAMQ+959GG50jBbl9s08PqUU643QwmA==
+-----END CERTIFICATE-----
+Certificate:
+ Data:
+ Version: 3 (0x2)
+ Serial Number:
+ 08:3b:e0:56:90:42:46:b1:a1:75:6a:c9:59:91:c7:4a
+ Signature Algorithm: sha1WithRSAEncryption
+ Issuer: C = US, O = DigiCert Inc, OU = www.digicert.com, CN = DigiCert Global Root CA
+ Validity
+ Not Before: Nov 10 00:00:00 2006 GMT
+ Not After : Nov 10 00:00:00 2031 GMT
+ Subject: C = US, O = DigiCert Inc, OU = www.digicert.com, CN = DigiCert Global Root CA
+ Subject Public Key Info:
+ Public Key Algorithm: rsaEncryption
+ RSA Public-Key: (2048 bit)
+ Modulus:
+ 00:e2:3b:e1:11:72:de:a8:a4:d3:a3:57:aa:50:a2:
+ 8f:0b:77:90:c9:a2:a5:ee:12:ce:96:5b:01:09:20:
+ cc:01:93:a7:4e:30:b7:53:f7:43:c4:69:00:57:9d:
+ e2:8d:22:dd:87:06:40:00:81:09:ce:ce:1b:83:bf:
+ df:cd:3b:71:46:e2:d6:66:c7:05:b3:76:27:16:8f:
+ 7b:9e:1e:95:7d:ee:b7:48:a3:08:da:d6:af:7a:0c:
+ 39:06:65:7f:4a:5d:1f:bc:17:f8:ab:be:ee:28:d7:
+ 74:7f:7a:78:99:59:85:68:6e:5c:23:32:4b:bf:4e:
+ c0:e8:5a:6d:e3:70:bf:77:10:bf:fc:01:f6:85:d9:
+ a8:44:10:58:32:a9:75:18:d5:d1:a2:be:47:e2:27:
+ 6a:f4:9a:33:f8:49:08:60:8b:d4:5f:b4:3a:84:bf:
+ a1:aa:4a:4c:7d:3e:cf:4f:5f:6c:76:5e:a0:4b:37:
+ 91:9e:dc:22:e6:6d:ce:14:1a:8e:6a:cb:fe:cd:b3:
+ 14:64:17:c7:5b:29:9e:32:bf:f2:ee:fa:d3:0b:42:
+ d4:ab:b7:41:32:da:0c:d4:ef:f8:81:d5:bb:8d:58:
+ 3f:b5:1b:e8:49:28:a2:70:da:31:04:dd:f7:b2:16:
+ f2:4c:0a:4e:07:a8:ed:4a:3d:5e:b5:7f:a3:90:c3:
+ af:27
+ Exponent: 65537 (0x10001)
+ X509v3 extensions:
+ X509v3 Key Usage: critical
+ Digital Signature, Certificate Sign, CRL Sign
+ X509v3 Basic Constraints: critical
+ CA:TRUE
+ X509v3 Subject Key Identifier:
+ 03:DE:50:35:56:D1:4C:BB:66:F0:A3:E2:1B:1B:C3:97:B2:3D:D1:55
+ X509v3 Authority Key Identifier:
+ keyid:03:DE:50:35:56:D1:4C:BB:66:F0:A3:E2:1B:1B:C3:97:B2:3D:D1:55
+
+ Signature Algorithm: sha1WithRSAEncryption
+ cb:9c:37:aa:48:13:12:0a:fa:dd:44:9c:4f:52:b0:f4:df:ae:
+ 04:f5:79:79:08:a3:24:18:fc:4b:2b:84:c0:2d:b9:d5:c7:fe:
+ f4:c1:1f:58:cb:b8:6d:9c:7a:74:e7:98:29:ab:11:b5:e3:70:
+ a0:a1:cd:4c:88:99:93:8c:91:70:e2:ab:0f:1c:be:93:a9:ff:
+ 63:d5:e4:07:60:d3:a3:bf:9d:5b:09:f1:d5:8e:e3:53:f4:8e:
+ 63:fa:3f:a7:db:b4:66:df:62:66:d6:d1:6e:41:8d:f2:2d:b5:
+ ea:77:4a:9f:9d:58:e2:2b:59:c0:40:23:ed:2d:28:82:45:3e:
+ 79:54:92:26:98:e0:80:48:a8:37:ef:f0:d6:79:60:16:de:ac:
+ e8:0e:cd:6e:ac:44:17:38:2f:49:da:e1:45:3e:2a:b9:36:53:
+ cf:3a:50:06:f7:2e:e8:c4:57:49:6c:61:21:18:d5:04:ad:78:
+ 3c:2c:3a:80:6b:a7:eb:af:15:14:e9:d8:89:c1:b9:38:6c:e2:
+ 91:6c:8a:ff:64:b9:77:25:57:30:c0:1b:24:a3:e1:dc:e9:df:
+ 47:7c:b5:b4:24:08:05:30:ec:2d:bd:0b:bf:45:bf:50:b9:a9:
+ f3:eb:98:01:12:ad:c8:88:c6:98:34:5f:8d:0a:3c:c6:e9:d5:
+ 95:95:6d:de
+-----BEGIN CERTIFICATE-----
+MIIDrzCCApegAwIBAgIQCDvgVpBCRrGhdWrJWZHHSjANBgkqhkiG9w0BAQUFADBh
+MQswCQYDVQQGEwJVUzEVMBMGA1UEChMMRGlnaUNlcnQgSW5jMRkwFwYDVQQLExB3
+d3cuZGlnaWNlcnQuY29tMSAwHgYDVQQDExdEaWdpQ2VydCBHbG9iYWwgUm9vdCBD
+QTAeFw0wNjExMTAwMDAwMDBaFw0zMTExMTAwMDAwMDBaMGExCzAJBgNVBAYTAlVT
+MRUwEwYDVQQKEwxEaWdpQ2VydCBJbmMxGTAXBgNVBAsTEHd3dy5kaWdpY2VydC5j
+b20xIDAeBgNVBAMTF0RpZ2lDZXJ0IEdsb2JhbCBSb290IENBMIIBIjANBgkqhkiG
+9w0BAQEFAAOCAQ8AMIIBCgKCAQEA4jvhEXLeqKTTo1eqUKKPC3eQyaKl7hLOllsB
+CSDMAZOnTjC3U/dDxGkAV53ijSLdhwZAAIEJzs4bg7/fzTtxRuLWZscFs3YnFo97
+nh6Vfe63SKMI2tavegw5BmV/Sl0fvBf4q77uKNd0f3p4mVmFaG5cIzJLv07A6Fpt
+43C/dxC//AH2hdmoRBBYMql1GNXRor5H4idq9Joz+EkIYIvUX7Q6hL+hqkpMfT7P
+T19sdl6gSzeRntwi5m3OFBqOasv+zbMUZBfHWymeMr/y7vrTC0LUq7dBMtoM1O/4
+gdW7jVg/tRvoSSiicNoxBN33shbyTApOB6jtSj1etX+jkMOvJwIDAQABo2MwYTAO
+BgNVHQ8BAf8EBAMCAYYwDwYDVR0TAQH/BAUwAwEB/zAdBgNVHQ4EFgQUA95QNVbR
+TLtm8KPiGxvDl7I90VUwHwYDVR0jBBgwFoAUA95QNVbRTLtm8KPiGxvDl7I90VUw
+DQYJKoZIhvcNAQEFBQADggEBAMucN6pIExIK+t1EnE9SsPTfrgT1eXkIoyQY/Esr
+hMAtudXH/vTBH1jLuG2cenTnmCmrEbXjcKChzUyImZOMkXDiqw8cvpOp/2PV5Adg
+06O/nVsJ8dWO41P0jmP6P6fbtGbfYmbW0W5BjfIttep3Sp+dWOIrWcBAI+0tKIJF
+PnlUkiaY4IBIqDfv8NZ5YBberOgOzW6sRBc4L0na4UU+Krk2U886UAb3LujEV0ls
+YSEY1QSteDwsOoBrp+uvFRTp2InBuThs4pFsiv9kuXclVzDAGySj4dzp30d8tbQk
+CAUw7C29C79Fv1C5qfPrmAESrciIxpg0X40KPMbp1ZWVbd4=
+-----END CERTIFICATE-----
diff --git a/certs/E1.pem b/certs/E1.pem
new file mode 100644
index 0000000..a62fc03
--- /dev/null
+++ b/certs/E1.pem
@@ -0,0 +1,124 @@
+Certificate:
+ Data:
+ Version: 3 (0x2)
+ Serial Number:
+ b3:bd:df:f8:a7:84:5b:bc:e9:03:a0:41:35:b3:4a:45
+ Signature Algorithm: ecdsa-with-SHA384
+ Issuer: C = US, O = Internet Security Research Group, CN = ISRG Root X2
+ Validity
+ Not Before: Sep 4 00:00:00 2020 GMT
+ Not After : Sep 15 16:00:00 2025 GMT
+ Subject: C = US, O = Let's Encrypt, CN = E1
+ Subject Public Key Info:
+ Public Key Algorithm: id-ecPublicKey
+ Public-Key: (384 bit)
+ pub:
+ 04:24:5c:2d:a2:2a:fd:1c:4b:a6:5d:97:73:27:31:
+ ac:b2:a0:69:62:ef:65:e8:a6:b0:f0:ac:4b:9f:ff:
+ 1c:0b:70:0f:d3:98:2f:4d:fc:0f:00:9b:37:f0:74:
+ 05:57:32:97:2e:05:ef:2a:43:25:a3:fb:6e:34:27:
+ 13:f6:4f:7e:69:d3:02:99:5e:eb:24:47:92:c1:24:
+ 9b:e6:b1:21:8f:c1:24:81:fc:68:cc:1f:69:ba:58:
+ f5:19:22:f7:74:c6:16
+ ASN1 OID: secp384r1
+ NIST CURVE: P-384
+ X509v3 extensions:
+ X509v3 Key Usage: critical
+ Digital Signature, Certificate Sign, CRL Sign
+ X509v3 Extended Key Usage:
+ TLS Web Client Authentication, TLS Web Server Authentication
+ X509v3 Basic Constraints: critical
+ CA:TRUE, pathlen:0
+ X509v3 Subject Key Identifier:
+ 5A:F3:ED:2B:FC:36:C2:37:79:B9:52:30:EA:54:6F:CF:55:CB:2E:AC
+ X509v3 Authority Key Identifier:
+ keyid:7C:42:96:AE:DE:4B:48:3B:FA:92:F8:9E:8C:CF:6D:8B:A9:72:37:95
+
+ Authority Information Access:
+ CA Issuers - URI:http://x2.i.lencr.org/
+
+ X509v3 CRL Distribution Points:
+
+ Full Name:
+ URI:http://x2.c.lencr.org/
+
+ X509v3 Certificate Policies:
+ Policy: 2.23.140.1.2.1
+ Policy: 1.3.6.1.4.1.44947.1.1.1
+
+ Signature Algorithm: ecdsa-with-SHA384
+ 30:64:02:30:7b:74:d5:52:13:8d:61:fe:0d:ba:3f:03:00:9d:
+ f3:d7:98:84:d9:57:2e:bd:e9:0f:9c:5c:48:04:21:f2:cb:b3:
+ 60:72:8e:97:d6:12:4f:ca:44:f6:42:c9:d3:7b:86:a9:02:30:
+ 5a:b1:b1:b4:ed:ea:60:99:20:b1:38:03:ca:3d:a0:26:b8:ee:
+ 6e:2d:4a:f6:c6:66:1f:33:9a:db:92:4a:d5:f5:29:13:c6:70:
+ 62:28:ba:23:8c:cf:3d:2f:cb:82:e9:7f
+-----BEGIN CERTIFICATE-----
+MIICxjCCAk2gAwIBAgIRALO93/inhFu86QOgQTWzSkUwCgYIKoZIzj0EAwMwTzEL
+MAkGA1UEBhMCVVMxKTAnBgNVBAoTIEludGVybmV0IFNlY3VyaXR5IFJlc2VhcmNo
+IEdyb3VwMRUwEwYDVQQDEwxJU1JHIFJvb3QgWDIwHhcNMjAwOTA0MDAwMDAwWhcN
+MjUwOTE1MTYwMDAwWjAyMQswCQYDVQQGEwJVUzEWMBQGA1UEChMNTGV0J3MgRW5j
+cnlwdDELMAkGA1UEAxMCRTEwdjAQBgcqhkjOPQIBBgUrgQQAIgNiAAQkXC2iKv0c
+S6Zdl3MnMayyoGli72XoprDwrEuf/xwLcA/TmC9N/A8AmzfwdAVXMpcuBe8qQyWj
++240JxP2T35p0wKZXuskR5LBJJvmsSGPwSSB/GjMH2m6WPUZIvd0xhajggEIMIIB
+BDAOBgNVHQ8BAf8EBAMCAYYwHQYDVR0lBBYwFAYIKwYBBQUHAwIGCCsGAQUFBwMB
+MBIGA1UdEwEB/wQIMAYBAf8CAQAwHQYDVR0OBBYEFFrz7Sv8NsI3eblSMOpUb89V
+yy6sMB8GA1UdIwQYMBaAFHxClq7eS0g7+pL4nozPbYupcjeVMDIGCCsGAQUFBwEB
+BCYwJDAiBggrBgEFBQcwAoYWaHR0cDovL3gyLmkubGVuY3Iub3JnLzAnBgNVHR8E
+IDAeMBygGqAYhhZodHRwOi8veDIuYy5sZW5jci5vcmcvMCIGA1UdIAQbMBkwCAYG
+Z4EMAQIBMA0GCysGAQQBgt8TAQEBMAoGCCqGSM49BAMDA2cAMGQCMHt01VITjWH+
+Dbo/AwCd89eYhNlXLr3pD5xcSAQh8suzYHKOl9YST8pE9kLJ03uGqQIwWrGxtO3q
+YJkgsTgDyj2gJrjubi1K9sZmHzOa25JK1fUpE8ZwYii6I4zPPS/Lgul/
+-----END CERTIFICATE-----
+Certificate:
+ Data:
+ Version: 3 (0x2)
+ Serial Number:
+ 41:d2:9d:d1:72:ea:ee:a7:80:c1:2c:6c:e9:2f:87:52
+ Signature Algorithm: ecdsa-with-SHA384
+ Issuer: C = US, O = Internet Security Research Group, CN = ISRG Root X2
+ Validity
+ Not Before: Sep 4 00:00:00 2020 GMT
+ Not After : Sep 17 16:00:00 2040 GMT
+ Subject: C = US, O = Internet Security Research Group, CN = ISRG Root X2
+ Subject Public Key Info:
+ Public Key Algorithm: id-ecPublicKey
+ Public-Key: (384 bit)
+ pub:
+ 04:cd:9b:d5:9f:80:83:0a:ec:09:4a:f3:16:4a:3e:
+ 5c:cf:77:ac:de:67:05:0d:1d:07:b6:dc:16:fb:5a:
+ 8b:14:db:e2:71:60:c4:ba:45:95:11:89:8e:ea:06:
+ df:f7:2a:16:1c:a4:b9:c5:c5:32:e0:03:e0:1e:82:
+ 18:38:8b:d7:45:d8:0a:6a:6e:e6:00:77:fb:02:51:
+ 7d:22:d8:0a:6e:9a:5b:77:df:f0:fa:41:ec:39:dc:
+ 75:ca:68:07:0c:1f:ea
+ ASN1 OID: secp384r1
+ NIST CURVE: P-384
+ X509v3 extensions:
+ X509v3 Key Usage: critical
+ Certificate Sign, CRL Sign
+ X509v3 Basic Constraints: critical
+ CA:TRUE
+ X509v3 Subject Key Identifier:
+ 7C:42:96:AE:DE:4B:48:3B:FA:92:F8:9E:8C:CF:6D:8B:A9:72:37:95
+ Signature Algorithm: ecdsa-with-SHA384
+ 30:65:02:30:7b:79:4e:46:50:84:c2:44:87:46:1b:45:70:ff:
+ 58:99:de:f4:fd:a4:d2:55:a6:20:2d:74:d6:34:bc:41:a3:50:
+ 5f:01:27:56:b4:be:27:75:06:af:12:2e:75:98:8d:fc:02:31:
+ 00:8b:f5:77:6c:d4:c8:65:aa:e0:0b:2c:ee:14:9d:27:37:a4:
+ f9:53:a5:51:e4:29:83:d7:f8:90:31:5b:42:9f:0a:f5:fe:ae:
+ 00:68:e7:8c:49:0f:b6:6f:5b:5b:15:f2:e7
+-----BEGIN CERTIFICATE-----
+MIICGzCCAaGgAwIBAgIQQdKd0XLq7qeAwSxs6S+HUjAKBggqhkjOPQQDAzBPMQsw
+CQYDVQQGEwJVUzEpMCcGA1UEChMgSW50ZXJuZXQgU2VjdXJpdHkgUmVzZWFyY2gg
+R3JvdXAxFTATBgNVBAMTDElTUkcgUm9vdCBYMjAeFw0yMDA5MDQwMDAwMDBaFw00
+MDA5MTcxNjAwMDBaME8xCzAJBgNVBAYTAlVTMSkwJwYDVQQKEyBJbnRlcm5ldCBT
+ZWN1cml0eSBSZXNlYXJjaCBHcm91cDEVMBMGA1UEAxMMSVNSRyBSb290IFgyMHYw
+EAYHKoZIzj0CAQYFK4EEACIDYgAEzZvVn4CDCuwJSvMWSj5cz3es3mcFDR0HttwW
++1qLFNvicWDEukWVEYmO6gbf9yoWHKS5xcUy4APgHoIYOIvXRdgKam7mAHf7AlF9
+ItgKbppbd9/w+kHsOdx1ymgHDB/qo0IwQDAOBgNVHQ8BAf8EBAMCAQYwDwYDVR0T
+AQH/BAUwAwEB/zAdBgNVHQ4EFgQUfEKWrt5LSDv6kviejM9ti6lyN5UwCgYIKoZI
+zj0EAwMDaAAwZQIwe3lORlCEwkSHRhtFcP9Ymd70/aTSVaYgLXTWNLxBo1BfASdW
+tL4ndQavEi51mI38AjEAi/V3bNTIZargCyzuFJ0nN6T5U6VR5CmD1/iQMVtCnwr1
+/q4AaOeMSQ+2b1tbFfLn
+-----END CERTIFICATE-----
diff --git a/certs/GTS-CA-1C3.pem b/certs/GTS-CA-1C3.pem
new file mode 100644
index 0000000..a8432d2
--- /dev/null
+++ b/certs/GTS-CA-1C3.pem
@@ -0,0 +1,242 @@
+Certificate:
+ Data:
+ Version: 3 (0x2)
+ Serial Number:
+ 02:03:bc:53:59:6b:34:c7:18:f5:01:50:66
+ Signature Algorithm: sha256WithRSAEncryption
+ Issuer: C = US, O = Google Trust Services LLC, CN = GTS Root R1
+ Validity
+ Not Before: Aug 13 00:00:42 2020 GMT
+ Not After : Sep 30 00:00:42 2027 GMT
+ Subject: C = US, O = Google Trust Services LLC, CN = GTS CA 1C3
+ Subject Public Key Info:
+ Public Key Algorithm: rsaEncryption
+ RSA Public-Key: (2048 bit)
+ Modulus:
+ 00:f5:88:df:e7:62:8c:1e:37:f8:37:42:90:7f:6c:
+ 87:d0:fb:65:82:25:fd:e8:cb:6b:a4:ff:6d:e9:5a:
+ 23:e2:99:f6:1c:e9:92:03:99:13:7c:09:0a:8a:fa:
+ 42:d6:5e:56:24:aa:7a:33:84:1f:d1:e9:69:bb:b9:
+ 74:ec:57:4c:66:68:93:77:37:55:53:fe:39:10:4d:
+ b7:34:bb:5f:25:77:37:3b:17:94:ea:3c:e5:9d:d5:
+ bc:c3:b4:43:eb:2e:a7:47:ef:b0:44:11:63:d8:b4:
+ 41:85:dd:41:30:48:93:1b:bf:b7:f6:e0:45:02:21:
+ e0:96:42:17:cf:d9:2b:65:56:34:07:26:04:0d:a8:
+ fd:7d:ca:2e:ef:ea:48:7c:37:4d:3f:00:9f:83:df:
+ ef:75:84:2e:79:57:5c:fc:57:6e:1a:96:ff:fc:8c:
+ 9a:a6:99:be:25:d9:7f:96:2c:06:f7:11:2a:02:80:
+ 80:eb:63:18:3c:50:49:87:e5:8a:ca:5f:19:2b:59:
+ 96:81:00:a0:fb:51:db:ca:77:0b:0b:c9:96:4f:ef:
+ 70:49:c7:5c:6d:20:fd:99:b4:b4:e2:ca:2e:77:fd:
+ 2d:dc:0b:b6:6b:13:0c:8c:19:2b:17:96:98:b9:f0:
+ 8b:f6:a0:27:bb:b6:e3:8d:51:8f:bd:ae:c7:9b:b1:
+ 89:9d
+ Exponent: 65537 (0x10001)
+ X509v3 extensions:
+ X509v3 Key Usage: critical
+ Digital Signature, Certificate Sign, CRL Sign
+ X509v3 Extended Key Usage:
+ TLS Web Server Authentication, TLS Web Client Authentication
+ X509v3 Basic Constraints: critical
+ CA:TRUE, pathlen:0
+ X509v3 Subject Key Identifier:
+ 8A:74:7F:AF:85:CD:EE:95:CD:3D:9C:D0:E2:46:14:F3:71:35:1D:27
+ X509v3 Authority Key Identifier:
+ keyid:E4:AF:2B:26:71:1A:2B:48:27:85:2F:52:66:2C:EF:F0:89:13:71:3E
+
+ Authority Information Access:
+ OCSP - URI:http://ocsp.pki.goog/gtsr1
+ CA Issuers - URI:http://pki.goog/repo/certs/gtsr1.der
+
+ X509v3 CRL Distribution Points:
+
+ Full Name:
+ URI:http://crl.pki.goog/gtsr1/gtsr1.crl
+
+ X509v3 Certificate Policies:
+ Policy: 1.3.6.1.4.1.11129.2.5.3
+ CPS: https://pki.goog/repository/
+ Policy: 2.23.140.1.2.1
+ Policy: 2.23.140.1.2.2
+
+ Signature Algorithm: sha256WithRSAEncryption
+ 89:7d:ac:20:5c:0c:3c:be:9a:a8:57:95:1b:b4:ae:fa:ab:a5:
+ 72:71:b4:36:95:fd:df:40:11:03:4c:c2:46:14:bb:14:24:ab:
+ f0:50:71:22:db:ad:c4:6e:7f:cf:f1:6a:6f:c8:83:1b:d8:ce:
+ 89:5f:87:6c:87:b8:a9:0c:a3:9b:a1:62:94:93:95:df:5b:ae:
+ 66:19:0b:02:96:9e:fc:b5:e7:10:69:3e:7a:cb:46:49:5f:46:
+ e1:41:b1:d7:98:4d:65:34:00:80:1a:3f:4f:9f:6c:7f:49:00:
+ 81:53:41:a4:92:21:82:82:1a:f1:a3:44:5b:2a:50:12:13:4d:
+ c1:53:36:f3:42:08:af:54:fa:8e:77:53:1b:64:38:27:17:09:
+ bd:58:c9:1b:7c:39:2d:5b:f3:ce:d4:ed:97:db:14:03:bf:09:
+ 53:24:1f:c2:0c:04:79:98:26:f2:61:f1:53:52:fd:42:8c:1b:
+ 66:2b:3f:15:a1:bb:ff:f6:9b:e3:81:9a:01:06:71:89:35:28:
+ 24:dd:e1:bd:eb:19:2d:e1:48:cb:3d:59:83:51:b4:74:c6:9d:
+ 7c:c6:b1:86:5b:af:cc:34:c4:d3:cc:d4:81:11:95:00:a1:f4:
+ 12:22:01:fa:b4:83:71:af:8c:b7:8c:73:24:ac:37:53:c2:00:
+ 90:3f:11:fe:5c:ed:36:94:10:3b:bd:29:ae:e2:c7:3a:62:3b:
+ 6c:63:d9:80:bf:59:71:ac:63:27:b9:4c:17:a0:da:f6:73:15:
+ bf:2a:de:8f:f3:a5:6c:32:81:33:03:d0:86:51:71:99:34:ba:
+ 93:8d:5d:b5:51:58:f7:b2:93:e8:01:f6:59:be:71:9b:fd:4d:
+ 28:ce:cf:6d:c7:16:dc:f7:d1:d6:46:9b:a7:ca:6b:e9:77:0f:
+ fd:a0:b6:1b:23:83:1d:10:1a:d9:09:00:84:e0:44:d3:a2:75:
+ 23:b3:34:86:f6:20:b0:a4:5e:10:1d:e0:52:46:00:9d:b1:0f:
+ 1f:21:70:51:f5:9a:dd:06:fc:55:f4:2b:0e:33:77:c3:4b:42:
+ c2:f1:77:13:fc:73:80:94:eb:1f:bb:37:3f:ce:02:2a:66:b0:
+ 73:1d:32:a5:32:6c:32:b0:8e:e0:c4:23:ff:5b:7d:4d:65:70:
+ ac:2b:9b:3d:ce:db:e0:6d:8e:32:80:be:96:9f:92:63:bc:97:
+ bb:5d:b9:f4:e1:71:5e:2a:e4:ef:03:22:b1:8a:65:3a:8f:c0:
+ 93:65:d4:85:cd:0f:0f:5b:83:59:16:47:16:2d:9c:24:3a:c8:
+ 80:a6:26:14:85:9b:f6:37:9b:ac:6f:f9:c5:c3:06:51:f3:e2:
+ 7f:c5:b1:10:ba:51:f4:dd
+-----BEGIN CERTIFICATE-----
+MIIFljCCA36gAwIBAgINAgO8U1lrNMcY9QFQZjANBgkqhkiG9w0BAQsFADBHMQsw
+CQYDVQQGEwJVUzEiMCAGA1UEChMZR29vZ2xlIFRydXN0IFNlcnZpY2VzIExMQzEU
+MBIGA1UEAxMLR1RTIFJvb3QgUjEwHhcNMjAwODEzMDAwMDQyWhcNMjcwOTMwMDAw
+MDQyWjBGMQswCQYDVQQGEwJVUzEiMCAGA1UEChMZR29vZ2xlIFRydXN0IFNlcnZp
+Y2VzIExMQzETMBEGA1UEAxMKR1RTIENBIDFDMzCCASIwDQYJKoZIhvcNAQEBBQAD
+ggEPADCCAQoCggEBAPWI3+dijB43+DdCkH9sh9D7ZYIl/ejLa6T/belaI+KZ9hzp
+kgOZE3wJCor6QtZeViSqejOEH9Hpabu5dOxXTGZok3c3VVP+ORBNtzS7XyV3NzsX
+lOo85Z3VvMO0Q+sup0fvsEQRY9i0QYXdQTBIkxu/t/bgRQIh4JZCF8/ZK2VWNAcm
+BA2o/X3KLu/qSHw3TT8An4Pf73WELnlXXPxXbhqW//yMmqaZviXZf5YsBvcRKgKA
+gOtjGDxQSYflispfGStZloEAoPtR28p3CwvJlk/vcEnHXG0g/Zm0tOLKLnf9LdwL
+tmsTDIwZKxeWmLnwi/agJ7u2441Rj72ux5uxiZ0CAwEAAaOCAYAwggF8MA4GA1Ud
+DwEB/wQEAwIBhjAdBgNVHSUEFjAUBggrBgEFBQcDAQYIKwYBBQUHAwIwEgYDVR0T
+AQH/BAgwBgEB/wIBADAdBgNVHQ4EFgQUinR/r4XN7pXNPZzQ4kYU83E1HScwHwYD
+VR0jBBgwFoAU5K8rJnEaK0gnhS9SZizv8IkTcT4waAYIKwYBBQUHAQEEXDBaMCYG
+CCsGAQUFBzABhhpodHRwOi8vb2NzcC5wa2kuZ29vZy9ndHNyMTAwBggrBgEFBQcw
+AoYkaHR0cDovL3BraS5nb29nL3JlcG8vY2VydHMvZ3RzcjEuZGVyMDQGA1UdHwQt
+MCswKaAnoCWGI2h0dHA6Ly9jcmwucGtpLmdvb2cvZ3RzcjEvZ3RzcjEuY3JsMFcG
+A1UdIARQME4wOAYKKwYBBAHWeQIFAzAqMCgGCCsGAQUFBwIBFhxodHRwczovL3Br
+aS5nb29nL3JlcG9zaXRvcnkvMAgGBmeBDAECATAIBgZngQwBAgIwDQYJKoZIhvcN
+AQELBQADggIBAIl9rCBcDDy+mqhXlRu0rvqrpXJxtDaV/d9AEQNMwkYUuxQkq/BQ
+cSLbrcRuf8/xam/IgxvYzolfh2yHuKkMo5uhYpSTld9brmYZCwKWnvy15xBpPnrL
+RklfRuFBsdeYTWU0AIAaP0+fbH9JAIFTQaSSIYKCGvGjRFsqUBITTcFTNvNCCK9U
++o53UxtkOCcXCb1YyRt8OS1b887U7ZfbFAO/CVMkH8IMBHmYJvJh8VNS/UKMG2Yr
+PxWhu//2m+OBmgEGcYk1KCTd4b3rGS3hSMs9WYNRtHTGnXzGsYZbr8w0xNPM1IER
+lQCh9BIiAfq0g3GvjLeMcySsN1PCAJA/Ef5c7TaUEDu9Ka7ixzpiO2xj2YC/WXGs
+Yye5TBeg2vZzFb8q3o/zpWwygTMD0IZRcZk0upONXbVRWPeyk+gB9lm+cZv9TSjO
+z23HFtz30dZGm6fKa+l3D/2gthsjgx0QGtkJAITgRNOidSOzNIb2ILCkXhAd4FJG
+AJ2xDx8hcFH1mt0G/FX0Kw4zd8NLQsLxdxP8c4CU6x+7Nz/OAipmsHMdMqUybDKw
+juDEI/9bfU1lcKwrmz3O2+BtjjKAvpafkmO8l7tdufThcV4q5O8DIrGKZTqPwJNl
+1IXNDw9bg1kWRxYtnCQ6yICmJhSFm/Y3m6xv+cXDBlHz4n/FsRC6UfTd
+-----END CERTIFICATE-----
+Certificate:
+ Data:
+ Version: 3 (0x2)
+ Serial Number:
+ 6e:47:a9:c5:4b:47:0c:0d:ec:33:d0:89:b9:1c:f4:e1
+ Signature Algorithm: sha384WithRSAEncryption
+ Issuer: C = US, O = Google Trust Services LLC, CN = GTS Root R1
+ Validity
+ Not Before: Jun 22 00:00:00 2016 GMT
+ Not After : Jun 22 00:00:00 2036 GMT
+ Subject: C = US, O = Google Trust Services LLC, CN = GTS Root R1
+ Subject Public Key Info:
+ Public Key Algorithm: rsaEncryption
+ RSA Public-Key: (4096 bit)
+ Modulus:
+ 00:b6:11:02:8b:1e:e3:a1:77:9b:3b:dc:bf:94:3e:
+ b7:95:a7:40:3c:a1:fd:82:f9:7d:32:06:82:71:f6:
+ f6:8c:7f:fb:e8:db:bc:6a:2e:97:97:a3:8c:4b:f9:
+ 2b:f6:b1:f9:ce:84:1d:b1:f9:c5:97:de:ef:b9:f2:
+ a3:e9:bc:12:89:5e:a7:aa:52:ab:f8:23:27:cb:a4:
+ b1:9c:63:db:d7:99:7e:f0:0a:5e:eb:68:a6:f4:c6:
+ 5a:47:0d:4d:10:33:e3:4e:b1:13:a3:c8:18:6c:4b:
+ ec:fc:09:90:df:9d:64:29:25:23:07:a1:b4:d2:3d:
+ 2e:60:e0:cf:d2:09:87:bb:cd:48:f0:4d:c2:c2:7a:
+ 88:8a:bb:ba:cf:59:19:d6:af:8f:b0:07:b0:9e:31:
+ f1:82:c1:c0:df:2e:a6:6d:6c:19:0e:b5:d8:7e:26:
+ 1a:45:03:3d:b0:79:a4:94:28:ad:0f:7f:26:e5:a8:
+ 08:fe:96:e8:3c:68:94:53:ee:83:3a:88:2b:15:96:
+ 09:b2:e0:7a:8c:2e:75:d6:9c:eb:a7:56:64:8f:96:
+ 4f:68:ae:3d:97:c2:84:8f:c0:bc:40:c0:0b:5c:bd:
+ f6:87:b3:35:6c:ac:18:50:7f:84:e0:4c:cd:92:d3:
+ 20:e9:33:bc:52:99:af:32:b5:29:b3:25:2a:b4:48:
+ f9:72:e1:ca:64:f7:e6:82:10:8d:e8:9d:c2:8a:88:
+ fa:38:66:8a:fc:63:f9:01:f9:78:fd:7b:5c:77:fa:
+ 76:87:fa:ec:df:b1:0e:79:95:57:b4:bd:26:ef:d6:
+ 01:d1:eb:16:0a:bb:8e:0b:b5:c5:c5:8a:55:ab:d3:
+ ac:ea:91:4b:29:cc:19:a4:32:25:4e:2a:f1:65:44:
+ d0:02:ce:aa:ce:49:b4:ea:9f:7c:83:b0:40:7b:e7:
+ 43:ab:a7:6c:a3:8f:7d:89:81:fa:4c:a5:ff:d5:8e:
+ c3:ce:4b:e0:b5:d8:b3:8e:45:cf:76:c0:ed:40:2b:
+ fd:53:0f:b0:a7:d5:3b:0d:b1:8a:a2:03:de:31:ad:
+ cc:77:ea:6f:7b:3e:d6:df:91:22:12:e6:be:fa:d8:
+ 32:fc:10:63:14:51:72:de:5d:d6:16:93:bd:29:68:
+ 33:ef:3a:66:ec:07:8a:26:df:13:d7:57:65:78:27:
+ de:5e:49:14:00:a2:00:7f:9a:a8:21:b6:a9:b1:95:
+ b0:a5:b9:0d:16:11:da:c7:6c:48:3c:40:e0:7e:0d:
+ 5a:cd:56:3c:d1:97:05:b9:cb:4b:ed:39:4b:9c:c4:
+ 3f:d2:55:13:6e:24:b0:d6:71:fa:f4:c1:ba:cc:ed:
+ 1b:f5:fe:81:41:d8:00:98:3d:3a:c8:ae:7a:98:37:
+ 18:05:95
+ Exponent: 65537 (0x10001)
+ X509v3 extensions:
+ X509v3 Key Usage: critical
+ Certificate Sign, CRL Sign
+ X509v3 Basic Constraints: critical
+ CA:TRUE
+ X509v3 Subject Key Identifier:
+ E4:AF:2B:26:71:1A:2B:48:27:85:2F:52:66:2C:EF:F0:89:13:71:3E
+ Signature Algorithm: sha384WithRSAEncryption
+ 38:96:0a:ee:3d:b4:96:1e:5f:ef:9d:9c:0b:33:9f:2b:e0:ca:
+ fd:d2:8e:0a:1f:41:74:a5:7c:aa:84:d4:e5:f2:1e:e6:37:52:
+ 32:9c:0b:d1:61:1d:bf:28:c1:b6:44:29:35:75:77:98:b2:7c:
+ d9:bd:74:ac:8a:68:e3:a9:31:09:29:01:60:73:e3:47:7c:53:
+ a8:90:4a:27:ef:4b:d7:9f:93:e7:82:36:ce:9a:68:0c:82:e7:
+ cf:d4:10:16:6f:5f:0e:99:5c:f6:1f:71:7d:ef:ef:7b:2f:7e:
+ ea:36:d6:97:70:0b:15:ee:d7:5c:56:6a:33:a5:e3:49:38:0c:
+ b8:7d:fb:8d:85:a4:b1:59:5e:f4:6a:e1:dd:a1:f6:64:44:ae:
+ e6:51:83:21:66:c6:11:3e:f3:ce:47:ee:9c:28:1f:25:da:ff:
+ ac:66:95:dd:35:0f:5c:ef:20:2c:62:fd:91:ba:a9:cc:fc:5a:
+ 9c:93:81:83:29:97:4a:7c:5a:72:b4:39:d0:b7:77:cb:79:fd:
+ 69:3a:92:37:ed:6e:38:65:46:7e:e9:60:bd:79:88:97:5f:38:
+ 12:f4:ee:af:5b:82:c8:86:d5:e1:99:6d:8c:04:f2:76:ba:49:
+ f6:6e:e9:6d:1e:5f:a0:ef:27:82:76:40:f8:a6:d3:58:5c:0f:
+ 2c:42:da:42:c6:7b:88:34:c7:c1:d8:45:9b:c1:3e:c5:61:1d:
+ d9:63:50:49:f6:34:85:6a:e0:18:c5:6e:47:ab:41:42:29:9b:
+ f6:60:0d:d2:31:d3:63:98:23:93:5a:00:81:48:b4:ef:cd:8a:
+ cd:c9:cf:99:ee:d9:9e:aa:36:e1:68:4b:71:49:14:36:28:3a:
+ 3d:1d:ce:9a:8f:25:e6:80:71:61:2b:b5:7b:cc:f9:25:16:81:
+ e1:31:5f:a1:a3:7e:16:a4:9c:16:6a:97:18:bd:76:72:a5:0b:
+ 9e:1d:36:e6:2f:a1:2f:be:70:91:0f:a8:e6:da:f8:c4:92:40:
+ 6c:25:7e:7b:b3:09:dc:b2:17:ad:80:44:f0:68:a5:8f:94:75:
+ ff:74:5a:e8:a8:02:7c:0c:09:e2:a9:4b:0b:a0:85:0b:62:b9:
+ ef:a1:31:92:fb:ef:f6:51:04:89:6c:e8:a9:74:a1:bb:17:b3:
+ b5:fd:49:0f:7c:3c:ec:83:18:20:43:4e:d5:93:ba:b4:34:b1:
+ 1f:16:36:1f:0c:e6:64:39:16:4c:dc:e0:fe:1d:c8:a9:62:3d:
+ 40:ea:ca:c5:34:02:b4:ae:89:88:33:35:dc:2c:13:73:d8:27:
+ f1:d0:72:ee:75:3b:22:de:98:68:66:5b:f1:c6:63:47:55:1c:
+ ba:a5:08:51:75:a6:48:25
+-----BEGIN CERTIFICATE-----
+MIIFWjCCA0KgAwIBAgIQbkepxUtHDA3sM9CJuRz04TANBgkqhkiG9w0BAQwFADBH
+MQswCQYDVQQGEwJVUzEiMCAGA1UEChMZR29vZ2xlIFRydXN0IFNlcnZpY2VzIExM
+QzEUMBIGA1UEAxMLR1RTIFJvb3QgUjEwHhcNMTYwNjIyMDAwMDAwWhcNMzYwNjIy
+MDAwMDAwWjBHMQswCQYDVQQGEwJVUzEiMCAGA1UEChMZR29vZ2xlIFRydXN0IFNl
+cnZpY2VzIExMQzEUMBIGA1UEAxMLR1RTIFJvb3QgUjEwggIiMA0GCSqGSIb3DQEB
+AQUAA4ICDwAwggIKAoICAQC2EQKLHuOhd5s73L+UPreVp0A8of2C+X0yBoJx9vaM
+f/vo27xqLpeXo4xL+Sv2sfnOhB2x+cWX3u+58qPpvBKJXqeqUqv4IyfLpLGcY9vX
+mX7wCl7raKb0xlpHDU0QM+NOsROjyBhsS+z8CZDfnWQpJSMHobTSPS5g4M/SCYe7
+zUjwTcLCeoiKu7rPWRnWr4+wB7CeMfGCwcDfLqZtbBkOtdh+JhpFAz2weaSUKK0P
+fyblqAj+lug8aJRT7oM6iCsVlgmy4HqMLnXWnOunVmSPlk9orj2XwoSPwLxAwAtc
+vfaHszVsrBhQf4TgTM2S0yDpM7xSma8ytSmzJSq0SPly4cpk9+aCEI3oncKKiPo4
+Zor8Y/kB+Xj9e1x3+naH+uzfsQ55lVe0vSbv1gHR6xYKu44LtcXFilWr06zqkUsp
+zBmkMiVOKvFlRNACzqrOSbTqn3yDsEB750Orp2yjj32JgfpMpf/VjsPOS+C12LOO
+Rc92wO1AK/1TD7Cn1TsNsYqiA94xrcx36m97PtbfkSIS5r762DL8EGMUUXLeXdYW
+k70paDPvOmbsB4om3xPXV2V4J95eSRQAogB/mqghtqmxlbCluQ0WEdrHbEg8QOB+
+DVrNVjzRlwW5y0vtOUucxD/SVRNuJLDWcfr0wbrM7Rv1/oFB2ACYPTrIrnqYNxgF
+lQIDAQABo0IwQDAOBgNVHQ8BAf8EBAMCAQYwDwYDVR0TAQH/BAUwAwEB/zAdBgNV
+HQ4EFgQU5K8rJnEaK0gnhS9SZizv8IkTcT4wDQYJKoZIhvcNAQEMBQADggIBADiW
+Cu49tJYeX++dnAsznyvgyv3SjgofQXSlfKqE1OXyHuY3UjKcC9FhHb8owbZEKTV1
+d5iyfNm9dKyKaOOpMQkpAWBz40d8U6iQSifvS9efk+eCNs6aaAyC58/UEBZvXw6Z
+XPYfcX3v73svfuo21pdwCxXu11xWajOl40k4DLh9+42FpLFZXvRq4d2h9mREruZR
+gyFmxhE+885H7pwoHyXa/6xmld01D1zvICxi/ZG6qcz8WpyTgYMpl0p8WnK0OdC3
+d8t5/Wk6kjftbjhlRn7pYL15iJdfOBL07q9bgsiG1eGZbYwE8na6SfZu6W0eX6Dv
+J4J2QPim01hcDyxC2kLGe4g0x8HYRZvBPsVhHdljUEn2NIVq4BjFbkerQUIpm/Zg
+DdIx02OYI5NaAIFItO/Nis3Jz5nu2Z6qNuFoS3FJFDYoOj0dzpqPJeaAcWErtXvM
++SUWgeExX6GjfhaknBZqlxi9dnKlC54dNuYvoS++cJEPqOba+MSSQGwlfnuzCdyy
+F62ARPBopY+Udf90WuioAnwMCeKpSwughQtiue+hMZL77/ZRBIls6Kl0obsXs7X9
+SQ98POyDGCBDTtWTurQ0sR8WNh8M5mQ5Fkzc4P4dyKliPUDqysU0ArSuiYgzNdws
+E3PYJ/HQcu51OyLemGhmW/HGY0dVHLqlCFF1pkgl
+-----END CERTIFICATE-----
diff --git a/certs/GTS-CA-1P5.pem b/certs/GTS-CA-1P5.pem
new file mode 100644
index 0000000..5be738d
--- /dev/null
+++ b/certs/GTS-CA-1P5.pem
@@ -0,0 +1,238 @@
+Certificate:
+ Data:
+ Version: 3 (0x2)
+ Serial Number:
+ 02:03:bc:50:a3:27:53:f0:91:80:22:ed:f1
+ Signature Algorithm: sha256WithRSAEncryption
+ Issuer: C=US, O=Google Trust Services LLC, CN=GTS Root R1
+ Validity
+ Not Before: Aug 13 00:00:42 2020 GMT
+ Not After : Sep 30 00:00:42 2027 GMT
+ Subject: C=US, O=Google Trust Services LLC, CN=GTS CA 1P5
+ Subject Public Key Info:
+ Public Key Algorithm: rsaEncryption
+ Public-Key: (2048 bit)
+ Modulus:
+ 00:b3:82:f0:24:8c:bf:2d:87:af:b2:d9:a7:ae:fa:
+ ca:ba:44:d6:5b:3e:fe:b2:f7:b2:65:16:dc:de:10:
+ e8:4f:2d:10:58:5a:28:86:87:a1:ee:6a:b3:a0:d9:
+ 75:4f:7f:a1:52:01:8b:55:a8:4a:5b:06:48:c8:36:
+ 12:25:ab:89:f9:f2:23:5f:9d:60:65:f9:5c:da:be:
+ 3a:e8:5c:6d:7d:9c:d0:84:18:85:30:cd:4e:9b:ec:
+ 3c:d8:b3:e1:96:d4:f3:c5:0b:65:db:8f:b0:74:cb:
+ f6:1e:f3:78:f1:ac:95:c5:dd:73:c3:31:88:81:af:
+ 74:aa:6f:fd:0c:e3:05:95:f0:c5:10:4f:65:63:fa:
+ a0:af:c6:18:3d:c5:a1:df:97:79:d7:05:89:b3:30:
+ b0:74:ae:3d:92:10:6b:8c:15:77:dd:0b:04:57:fb:
+ 81:03:dd:ea:22:34:d5:e5:56:b2:f0:c4:8d:41:b1:
+ c3:02:db:62:ec:80:d0:ff:76:d4:86:e4:04:1a:b6:
+ b6:0c:2b:62:71:7d:d9:af:d9:f1:5e:fa:c0:1e:ca:
+ a0:19:5c:55:f0:80:d1:2a:0c:07:86:90:9f:35:e3:
+ 28:2b:5b:ef:23:c8:a3:1d:a4:a3:3a:ee:fe:83:dc:
+ 82:4c:25:b0:4d:c5:51:ad:9e:9b:d3:5b:84:c2:1a:
+ 5a:e9
+ Exponent: 65537 (0x10001)
+ X509v3 extensions:
+ X509v3 Key Usage: critical
+ Digital Signature, Certificate Sign, CRL Sign
+ X509v3 Extended Key Usage:
+ TLS Web Server Authentication, TLS Web Client Authentication
+ X509v3 Basic Constraints: critical
+ CA:TRUE, pathlen:0
+ X509v3 Subject Key Identifier:
+ D5:FC:9E:0D:DF:1E:CA:DD:08:97:97:6E:2B:C5:5F:C5:2B:F5:EC:B8
+ X509v3 Authority Key Identifier:
+ E4:AF:2B:26:71:1A:2B:48:27:85:2F:52:66:2C:EF:F0:89:13:71:3E
+ Authority Information Access:
+ OCSP - URI:http://ocsp.pki.goog/gtsr1
+ CA Issuers - URI:http://pki.goog/repo/certs/gtsr1.der
+ X509v3 CRL Distribution Points:
+ Full Name:
+ URI:http://crl.pki.goog/gtsr1/gtsr1.crl
+ X509v3 Certificate Policies:
+ Policy: 1.3.6.1.4.1.11129.2.5.3
+ CPS: https://pki.goog/repository/
+ Policy: 2.23.140.1.2.1
+ Signature Algorithm: sha256WithRSAEncryption
+ Signature Value:
+ 6c:63:27:ee:23:df:e5:52:68:4d:81:66:91:85:df:7d:65:e5:
+ 5b:37:31:08:26:b2:07:5d:9a:be:b1:ca:01:b9:ad:bf:9d:77:
+ f6:51:1d:d7:98:c5:0b:49:a1:7b:a1:d7:d3:68:e5:44:0f:8b:
+ ba:36:dd:42:82:77:d2:8d:dd:f5:3f:fb:eb:c8:07:98:93:ee:
+ 5a:d0:b5:3d:de:4b:1c:2d:8c:4d:ec:7e:8c:7b:fe:4e:40:fd:
+ f0:b4:b3:59:02:10:51:5c:e3:c0:2b:fd:b7:06:48:51:7e:09:
+ 5e:3f:0f:dc:a7:fe:97:e7:79:c5:0e:44:89:78:c5:69:59:29:
+ a0:9a:3a:48:36:29:a6:94:93:55:2d:b8:47:b5:e9:96:b5:9f:
+ 07:cd:a6:ab:3e:32:8a:c0:86:83:c5:c1:41:c8:9f:2f:35:8e:
+ 0d:c0:07:7a:e1:ac:c9:65:b5:cb:8a:a7:dd:71:d8:61:65:39:
+ 84:ac:32:3e:f7:7a:36:f1:56:9f:57:a9:41:6d:5a:90:a7:db:
+ 3a:ea:75:80:0c:63:0b:69:74:6f:07:4c:15:f3:37:28:a5:19:
+ a4:6e:f5:f6:20:cd:63:b2:7e:c4:2b:09:75:89:da:d1:3c:2e:
+ 72:4f:36:1a:a1:9e:44:d0:cd:9b:a6:23:08:3f:97:a1:a7:9e:
+ 5a:a5:f7:09:94:ad:5d:76:5d:28:56:d1:1a:66:51:51:07:7b:
+ de:3d:b0:c8:ef:30:7a:24:2d:be:b8:b3:86:f6:4b:f7:f0:b5:
+ 4f:ff:ce:c6:f9:f6:3f:2a:27:08:0f:09:3e:23:5a:c7:e3:42:
+ 2d:7a:36:e4:3d:98:96:60:39:98:ea:d1:db:63:2a:eb:78:09:
+ b1:4e:21:b3:8e:b7:ce:3e:92:f1:95:5c:a4:39:d0:c0:2b:c8:
+ 53:15:f5:d2:2f:82:cd:06:74:67:99:90:77:37:0a:97:2d:c5:
+ 1c:1e:f4:d0:5b:e9:15:e3:ea:02:09:c8:13:d7:13:70:65:bf:
+ fb:88:9b:5a:25:be:77:09:e1:a7:6a:4e:11:75:b9:1e:4d:f1:
+ 00:1b:6a:66:79:8e:c3:6e:d8:6d:a2:22:a2:6d:05:fb:2c:f2:
+ f1:50:e5:a0:d1:d8:9f:35:7d:fc:70:ab:59:2a:02:f1:be:b0:
+ d3:f1:f8:cd:12:b9:6a:25:90:5b:e3:85:20:e6:f5:da:cb:40:
+ 1c:19:34:20:03:61:77:ba:7f:48:0f:49:0b:29:eb:e7:61:64:
+ c7:63:d1:47:eb:1c:e1:ee:94:46:ef:39:73:cc:ee:4f:2b:8d:
+ dc:fb:58:a7:b3:65:20:99:95:b9:fb:55:6f:d7:96:6e:94:3d:
+ f4:7a:92:8e:63:1d:df:6d
+-----BEGIN CERTIFICATE-----
+MIIFjDCCA3SgAwIBAgINAgO8UKMnU/CRgCLt8TANBgkqhkiG9w0BAQsFADBHMQsw
+CQYDVQQGEwJVUzEiMCAGA1UEChMZR29vZ2xlIFRydXN0IFNlcnZpY2VzIExMQzEU
+MBIGA1UEAxMLR1RTIFJvb3QgUjEwHhcNMjAwODEzMDAwMDQyWhcNMjcwOTMwMDAw
+MDQyWjBGMQswCQYDVQQGEwJVUzEiMCAGA1UEChMZR29vZ2xlIFRydXN0IFNlcnZp
+Y2VzIExMQzETMBEGA1UEAxMKR1RTIENBIDFQNTCCASIwDQYJKoZIhvcNAQEBBQAD
+ggEPADCCAQoCggEBALOC8CSMvy2Hr7LZp676yrpE1ls+/rL3smUW3N4Q6E8tEFha
+KIaHoe5qs6DZdU9/oVIBi1WoSlsGSMg2EiWrifnyI1+dYGX5XNq+OuhcbX2c0IQY
+hTDNTpvsPNiz4ZbU88ULZduPsHTL9h7zePGslcXdc8MxiIGvdKpv/QzjBZXwxRBP
+ZWP6oK/GGD3Fod+XedcFibMwsHSuPZIQa4wVd90LBFf7gQPd6iI01eVWsvDEjUGx
+wwLbYuyA0P921IbkBBq2tgwrYnF92a/Z8V76wB7KoBlcVfCA0SoMB4aQnzXjKCtb
+7yPIox2kozru/oPcgkwlsE3FUa2em9NbhMIaWukCAwEAAaOCAXYwggFyMA4GA1Ud
+DwEB/wQEAwIBhjAdBgNVHSUEFjAUBggrBgEFBQcDAQYIKwYBBQUHAwIwEgYDVR0T
+AQH/BAgwBgEB/wIBADAdBgNVHQ4EFgQU1fyeDd8eyt0Il5duK8VfxSv17LgwHwYD
+VR0jBBgwFoAU5K8rJnEaK0gnhS9SZizv8IkTcT4waAYIKwYBBQUHAQEEXDBaMCYG
+CCsGAQUFBzABhhpodHRwOi8vb2NzcC5wa2kuZ29vZy9ndHNyMTAwBggrBgEFBQcw
+AoYkaHR0cDovL3BraS5nb29nL3JlcG8vY2VydHMvZ3RzcjEuZGVyMDQGA1UdHwQt
+MCswKaAnoCWGI2h0dHA6Ly9jcmwucGtpLmdvb2cvZ3RzcjEvZ3RzcjEuY3JsME0G
+A1UdIARGMEQwOAYKKwYBBAHWeQIFAzAqMCgGCCsGAQUFBwIBFhxodHRwczovL3Br
+aS5nb29nL3JlcG9zaXRvcnkvMAgGBmeBDAECATANBgkqhkiG9w0BAQsFAAOCAgEA
+bGMn7iPf5VJoTYFmkYXffWXlWzcxCCayB12avrHKAbmtv5139lEd15jFC0mhe6HX
+02jlRA+LujbdQoJ30o3d9T/768gHmJPuWtC1Pd5LHC2MTex+jHv+TkD98LSzWQIQ
+UVzjwCv9twZIUX4JXj8P3Kf+l+d5xQ5EiXjFaVkpoJo6SDYpppSTVS24R7XplrWf
+B82mqz4yisCGg8XBQcifLzWODcAHeuGsyWW1y4qn3XHYYWU5hKwyPvd6NvFWn1ep
+QW1akKfbOup1gAxjC2l0bwdMFfM3KKUZpG719iDNY7J+xCsJdYna0Twuck82GqGe
+RNDNm6YjCD+XoaeeWqX3CZStXXZdKFbRGmZRUQd73j2wyO8weiQtvrizhvZL9/C1
+T//Oxvn2PyonCA8JPiNax+NCLXo25D2YlmA5mOrR22Mq63gJsU4hs463zj6S8ZVc
+pDnQwCvIUxX10i+CzQZ0Z5mQdzcKly3FHB700FvpFePqAgnIE9cTcGW/+4ibWiW+
+dwnhp2pOEXW5Hk3xABtqZnmOw27YbaIiom0F+yzy8VDloNHYnzV9/HCrWSoC8b6w
+0/H4zRK5aiWQW+OFIOb12stAHBk0IANhd7p/SA9JCynr52Fkx2PRR+sc4e6URu85
+c8zuTyuN3PtYp7NlIJmVuftVb9eWbpQ99HqSjmMd320=
+-----END CERTIFICATE-----
+Certificate:
+ Data:
+ Version: 3 (0x2)
+ Serial Number:
+ 02:03:e5:93:6f:31:b0:13:49:88:6b:a2:17
+ Signature Algorithm: sha384WithRSAEncryption
+ Issuer: C=US, O=Google Trust Services LLC, CN=GTS Root R1
+ Validity
+ Not Before: Jun 22 00:00:00 2016 GMT
+ Not After : Jun 22 00:00:00 2036 GMT
+ Subject: C=US, O=Google Trust Services LLC, CN=GTS Root R1
+ Subject Public Key Info:
+ Public Key Algorithm: rsaEncryption
+ Public-Key: (4096 bit)
+ Modulus:
+ 00:b6:11:02:8b:1e:e3:a1:77:9b:3b:dc:bf:94:3e:
+ b7:95:a7:40:3c:a1:fd:82:f9:7d:32:06:82:71:f6:
+ f6:8c:7f:fb:e8:db:bc:6a:2e:97:97:a3:8c:4b:f9:
+ 2b:f6:b1:f9:ce:84:1d:b1:f9:c5:97:de:ef:b9:f2:
+ a3:e9:bc:12:89:5e:a7:aa:52:ab:f8:23:27:cb:a4:
+ b1:9c:63:db:d7:99:7e:f0:0a:5e:eb:68:a6:f4:c6:
+ 5a:47:0d:4d:10:33:e3:4e:b1:13:a3:c8:18:6c:4b:
+ ec:fc:09:90:df:9d:64:29:25:23:07:a1:b4:d2:3d:
+ 2e:60:e0:cf:d2:09:87:bb:cd:48:f0:4d:c2:c2:7a:
+ 88:8a:bb:ba:cf:59:19:d6:af:8f:b0:07:b0:9e:31:
+ f1:82:c1:c0:df:2e:a6:6d:6c:19:0e:b5:d8:7e:26:
+ 1a:45:03:3d:b0:79:a4:94:28:ad:0f:7f:26:e5:a8:
+ 08:fe:96:e8:3c:68:94:53:ee:83:3a:88:2b:15:96:
+ 09:b2:e0:7a:8c:2e:75:d6:9c:eb:a7:56:64:8f:96:
+ 4f:68:ae:3d:97:c2:84:8f:c0:bc:40:c0:0b:5c:bd:
+ f6:87:b3:35:6c:ac:18:50:7f:84:e0:4c:cd:92:d3:
+ 20:e9:33:bc:52:99:af:32:b5:29:b3:25:2a:b4:48:
+ f9:72:e1:ca:64:f7:e6:82:10:8d:e8:9d:c2:8a:88:
+ fa:38:66:8a:fc:63:f9:01:f9:78:fd:7b:5c:77:fa:
+ 76:87:fa:ec:df:b1:0e:79:95:57:b4:bd:26:ef:d6:
+ 01:d1:eb:16:0a:bb:8e:0b:b5:c5:c5:8a:55:ab:d3:
+ ac:ea:91:4b:29:cc:19:a4:32:25:4e:2a:f1:65:44:
+ d0:02:ce:aa:ce:49:b4:ea:9f:7c:83:b0:40:7b:e7:
+ 43:ab:a7:6c:a3:8f:7d:89:81:fa:4c:a5:ff:d5:8e:
+ c3:ce:4b:e0:b5:d8:b3:8e:45:cf:76:c0:ed:40:2b:
+ fd:53:0f:b0:a7:d5:3b:0d:b1:8a:a2:03:de:31:ad:
+ cc:77:ea:6f:7b:3e:d6:df:91:22:12:e6:be:fa:d8:
+ 32:fc:10:63:14:51:72:de:5d:d6:16:93:bd:29:68:
+ 33:ef:3a:66:ec:07:8a:26:df:13:d7:57:65:78:27:
+ de:5e:49:14:00:a2:00:7f:9a:a8:21:b6:a9:b1:95:
+ b0:a5:b9:0d:16:11:da:c7:6c:48:3c:40:e0:7e:0d:
+ 5a:cd:56:3c:d1:97:05:b9:cb:4b:ed:39:4b:9c:c4:
+ 3f:d2:55:13:6e:24:b0:d6:71:fa:f4:c1:ba:cc:ed:
+ 1b:f5:fe:81:41:d8:00:98:3d:3a:c8:ae:7a:98:37:
+ 18:05:95
+ Exponent: 65537 (0x10001)
+ X509v3 extensions:
+ X509v3 Key Usage: critical
+ Digital Signature, Certificate Sign, CRL Sign
+ X509v3 Basic Constraints: critical
+ CA:TRUE
+ X509v3 Subject Key Identifier:
+ E4:AF:2B:26:71:1A:2B:48:27:85:2F:52:66:2C:EF:F0:89:13:71:3E
+ Signature Algorithm: sha384WithRSAEncryption
+ Signature Value:
+ 9f:aa:42:26:db:0b:9b:be:ff:1e:96:92:2e:3e:a2:65:4a:6a:
+ 98:ba:22:cb:7d:c1:3a:d8:82:0a:06:c6:f6:a5:de:c0:4e:87:
+ 66:79:a1:f9:a6:58:9c:aa:f9:b5:e6:60:e7:e0:e8:b1:1e:42:
+ 41:33:0b:37:3d:ce:89:70:15:ca:b5:24:a8:cf:6b:b5:d2:40:
+ 21:98:cf:22:34:cf:3b:c5:22:84:e0:c5:0e:8a:7c:5d:88:e4:
+ 35:24:ce:9b:3e:1a:54:1e:6e:db:b2:87:a7:fc:f3:fa:81:55:
+ 14:62:0a:59:a9:22:05:31:3e:82:d6:ee:db:57:34:bc:33:95:
+ d3:17:1b:e8:27:a2:8b:7b:4e:26:1a:7a:5a:64:b6:d1:ac:37:
+ f1:fd:a0:f3:38:ec:72:f0:11:75:9d:cb:34:52:8d:e6:76:6b:
+ 17:c6:df:86:ab:27:8e:49:2b:75:66:81:10:21:a6:ea:3e:f4:
+ ae:25:ff:7c:15:de:ce:8c:25:3f:ca:62:70:0a:f7:2f:09:66:
+ 07:c8:3f:1c:fc:f0:db:45:30:df:62:88:c1:b5:0f:9d:c3:9f:
+ 4a:de:59:59:47:c5:87:22:36:e6:82:a7:ed:0a:b9:e2:07:a0:
+ 8d:7b:7a:4a:3c:71:d2:e2:03:a1:1f:32:07:dd:1b:e4:42:ce:
+ 0c:00:45:61:80:b5:0b:20:59:29:78:bd:f9:55:cb:63:c5:3c:
+ 4c:f4:b6:ff:db:6a:5f:31:6b:99:9e:2c:c1:6b:50:a4:d7:e6:
+ 18:14:bd:85:3f:67:ab:46:9f:a0:ff:42:a7:3a:7f:5c:cb:5d:
+ b0:70:1d:2b:34:f5:d4:76:09:0c:eb:78:4c:59:05:f3:33:42:
+ c3:61:15:10:1b:77:4d:ce:22:8c:d4:85:f2:45:7d:b7:53:ea:
+ ef:40:5a:94:0a:5c:20:5f:4e:40:5d:62:22:76:df:ff:ce:61:
+ bd:8c:23:78:d2:37:02:e0:8e:de:d1:11:37:89:f6:bf:ed:49:
+ 07:62:ae:92:ec:40:1a:af:14:09:d9:d0:4e:b2:a2:f7:be:ee:
+ ee:d8:ff:dc:1a:2d:de:b8:36:71:e2:fc:79:b7:94:25:d1:48:
+ 73:5b:a1:35:e7:b3:99:67:75:c1:19:3a:2b:47:4e:d3:42:8e:
+ fd:31:c8:16:66:da:d2:0c:3c:db:b3:8e:c9:a1:0d:80:0f:7b:
+ 16:77:14:bf:ff:db:09:94:b2:93:bc:20:58:15:e9:db:71:43:
+ f3:de:10:c3:00:dc:a8:2a:95:b6:c2:d6:3f:90:6b:76:db:6c:
+ fe:8c:bc:f2:70:35:0c:dc:99:19:35:dc:d7:c8:46:63:d5:36:
+ 71:ae:57:fb:b7:82:6d:dc
+-----BEGIN CERTIFICATE-----
+MIIFVzCCAz+gAwIBAgINAgPlk28xsBNJiGuiFzANBgkqhkiG9w0BAQwFADBHMQsw
+CQYDVQQGEwJVUzEiMCAGA1UEChMZR29vZ2xlIFRydXN0IFNlcnZpY2VzIExMQzEU
+MBIGA1UEAxMLR1RTIFJvb3QgUjEwHhcNMTYwNjIyMDAwMDAwWhcNMzYwNjIyMDAw
+MDAwWjBHMQswCQYDVQQGEwJVUzEiMCAGA1UEChMZR29vZ2xlIFRydXN0IFNlcnZp
+Y2VzIExMQzEUMBIGA1UEAxMLR1RTIFJvb3QgUjEwggIiMA0GCSqGSIb3DQEBAQUA
+A4ICDwAwggIKAoICAQC2EQKLHuOhd5s73L+UPreVp0A8of2C+X0yBoJx9vaMf/vo
+27xqLpeXo4xL+Sv2sfnOhB2x+cWX3u+58qPpvBKJXqeqUqv4IyfLpLGcY9vXmX7w
+Cl7raKb0xlpHDU0QM+NOsROjyBhsS+z8CZDfnWQpJSMHobTSPS5g4M/SCYe7zUjw
+TcLCeoiKu7rPWRnWr4+wB7CeMfGCwcDfLqZtbBkOtdh+JhpFAz2weaSUKK0Pfybl
+qAj+lug8aJRT7oM6iCsVlgmy4HqMLnXWnOunVmSPlk9orj2XwoSPwLxAwAtcvfaH
+szVsrBhQf4TgTM2S0yDpM7xSma8ytSmzJSq0SPly4cpk9+aCEI3oncKKiPo4Zor8
+Y/kB+Xj9e1x3+naH+uzfsQ55lVe0vSbv1gHR6xYKu44LtcXFilWr06zqkUspzBmk
+MiVOKvFlRNACzqrOSbTqn3yDsEB750Orp2yjj32JgfpMpf/VjsPOS+C12LOORc92
+wO1AK/1TD7Cn1TsNsYqiA94xrcx36m97PtbfkSIS5r762DL8EGMUUXLeXdYWk70p
+aDPvOmbsB4om3xPXV2V4J95eSRQAogB/mqghtqmxlbCluQ0WEdrHbEg8QOB+DVrN
+VjzRlwW5y0vtOUucxD/SVRNuJLDWcfr0wbrM7Rv1/oFB2ACYPTrIrnqYNxgFlQID
+AQABo0IwQDAOBgNVHQ8BAf8EBAMCAYYwDwYDVR0TAQH/BAUwAwEB/zAdBgNVHQ4E
+FgQU5K8rJnEaK0gnhS9SZizv8IkTcT4wDQYJKoZIhvcNAQEMBQADggIBAJ+qQibb
+C5u+/x6Wki4+omVKapi6Ist9wTrYggoGxval3sBOh2Z5ofmmWJyq+bXmYOfg6LEe
+QkEzCzc9zolwFcq1JKjPa7XSQCGYzyI0zzvFIoTgxQ6KfF2I5DUkzps+GlQebtuy
+h6f88/qBVRRiClmpIgUxPoLW7ttXNLwzldMXG+gnoot7TiYaelpkttGsN/H9oPM4
+7HLwEXWdyzRSjeZ2axfG34arJ45JK3VmgRAhpuo+9K4l/3wV3s6MJT/KYnAK9y8J
+ZgfIPxz88NtFMN9iiMG1D53Dn0reWVlHxYciNuaCp+0KueIHoI17eko8cdLiA6Ef
+MgfdG+RCzgwARWGAtQsgWSl4vflVy2PFPEz0tv/bal8xa5meLMFrUKTX5hgUvYU/
+Z6tGn6D/Qqc6f1zLXbBwHSs09dR2CQzreExZBfMzQsNhFRAbd03OIozUhfJFfbdT
+6u9AWpQKXCBfTkBdYiJ23//OYb2MI3jSNwLgjt7RETeJ9r/tSQdirpLsQBqvFAnZ
+0E6yove+7u7Y/9waLd64NnHi/Hm3lCXRSHNboTXns5lndcEZOitHTtNCjv0xyBZm
+2tIMPNuzjsmhDYAPexZ3FL//2wmUspO8IFgV6dtxQ/PeEMMA3KgqlbbC1j+Qa3bb
+bP6MvPJwNQzcmRk13NfIRmPVNnGuV/u3gm3c
+-----END CERTIFICATE-----
diff --git a/certs/GlobalSign-Atlas-R3-DV-TLS-CA-2022-Q3.pem b/certs/GlobalSign-Atlas-R3-DV-TLS-CA-2022-Q3.pem
new file mode 100644
index 0000000..b514c11
--- /dev/null
+++ b/certs/GlobalSign-Atlas-R3-DV-TLS-CA-2022-Q3.pem
@@ -0,0 +1,177 @@
+Certificate:
+ Data:
+ Version: 3 (0x2)
+ Serial Number:
+ 7c:2a:0c:21:3f:c6:55:53:45:c9:1f:19:1f:b8:4e:fa
+ Signature Algorithm: sha256WithRSAEncryption
+ Issuer: OU = GlobalSign Root CA - R3, O = GlobalSign, CN = GlobalSign
+ Validity
+ Not Before: Apr 20 12:00:00 2022 GMT
+ Not After : Apr 20 00:00:00 2025 GMT
+ Subject: C = BE, O = GlobalSign nv-sa, CN = GlobalSign Atlas R3 DV TLS CA 2022 Q3
+ Subject Public Key Info:
+ Public Key Algorithm: rsaEncryption
+ Public-Key: (2048 bit)
+ Modulus:
+ 00:b8:a8:7a:66:3c:4e:66:9c:ce:37:a5:54:35:4d:
+ 36:c7:99:d3:a8:27:36:f2:2f:c6:d5:18:3e:e9:09:
+ dd:05:d6:d7:2c:34:32:7c:08:63:49:d1:10:37:e5:
+ 78:5d:11:62:ce:6d:fb:2f:3f:37:94:db:8f:7b:30:
+ e9:5e:2c:d9:55:3f:b2:db:b9:a0:b5:60:37:8b:a4:
+ 06:32:35:50:a4:09:af:0a:45:ff:a8:1f:9b:65:8e:
+ dd:4a:e0:40:a1:e3:63:37:58:90:dd:75:3b:fc:0e:
+ 1c:82:40:98:bd:70:b1:c1:48:14:14:3c:04:4b:69:
+ dd:d4:9c:01:a6:e9:21:e3:82:0a:fe:e4:aa:bf:34:
+ a0:8c:cb:c9:79:6e:3e:5c:6a:52:9e:c4:ed:2b:c5:
+ 69:fe:50:3c:93:9d:b5:ff:2d:28:a8:6c:06:6c:9d:
+ c5:af:b2:59:fb:59:77:0d:74:7a:88:84:a4:d4:1d:
+ d4:ba:20:06:cc:b5:1e:48:4e:74:21:15:86:75:c0:
+ cc:5a:d1:05:cf:57:16:7a:13:17:ec:c2:4a:ae:d5:
+ 1e:72:aa:22:5a:8c:9c:82:32:c4:10:e6:42:6e:21:
+ 86:68:7c:80:23:30:35:d3:bd:b0:5e:0a:29:2b:f0:
+ 14:b1:18:37:d9:59:25:c3:e7:38:d9:e9:d4:2d:36:
+ 35:65
+ Exponent: 65537 (0x10001)
+ X509v3 extensions:
+ X509v3 Key Usage: critical
+ Digital Signature, Certificate Sign, CRL Sign
+ X509v3 Extended Key Usage:
+ TLS Web Server Authentication, TLS Web Client Authentication
+ X509v3 Basic Constraints: critical
+ CA:TRUE, pathlen:0
+ X509v3 Subject Key Identifier:
+ FA:91:39:63:9A:FB:AD:10:24:E5:BE:B5:B9:DA:AB:D9:C4:46:69:AB
+ X509v3 Authority Key Identifier:
+ 8F:F0:4B:7F:A8:2E:45:24:AE:4D:50:FA:63:9A:8B:DE:E2:DD:1B:BC
+ Authority Information Access:
+ OCSP - URI:http://ocsp2.globalsign.com/rootr3
+ CA Issuers - URI:http://secure.globalsign.com/cacert/root-r3.crt
+ X509v3 CRL Distribution Points:
+ Full Name:
+ URI:http://crl.globalsign.com/root-r3.crl
+ X509v3 Certificate Policies:
+ Policy: 2.23.140.1.2.1
+ Policy: 1.3.6.1.4.1.4146.10.1.3
+ Signature Algorithm: sha256WithRSAEncryption
+ Signature Value:
+ 14:33:2c:79:e5:3f:82:c6:70:3f:da:59:38:a7:bb:a2:76:ac:
+ 61:18:05:68:57:d9:0d:fb:8a:46:bc:f1:a8:e8:0c:70:02:1d:
+ c6:2f:97:ed:36:3e:9e:52:86:2f:5c:62:d8:d5:47:43:9a:73:
+ d1:2b:25:87:9f:44:b4:14:eb:26:bc:21:47:74:20:bd:9f:a4:
+ bf:b3:80:1d:4d:35:7d:cd:b9:b5:da:55:f2:90:50:c8:b2:17:
+ 4e:0e:b4:61:88:29:5f:44:5d:03:7f:57:91:81:d0:eb:30:ae:
+ d5:2a:ec:82:20:ce:4e:d2:b0:8b:95:02:61:73:d8:69:34:f4:
+ ad:63:0e:5c:e4:20:1f:a9:7d:ed:8e:e5:1c:04:bb:22:9f:c7:
+ a9:22:ca:99:3d:02:a7:67:e8:06:2d:fa:04:6b:bb:49:d2:6c:
+ 99:57:63:6c:2d:c2:61:78:e1:20:b1:fb:f6:bf:e1:82:39:39:
+ 3c:7b:ef:7d:1a:95:4a:b2:72:da:55:90:ae:ed:dd:e2:70:90:
+ 7c:1a:ee:b5:32:5a:5d:cf:d6:fa:45:f2:9e:01:0c:31:2f:89:
+ 84:fe:31:60:0f:fd:ee:a6:5b:84:d5:c7:18:e6:a4:f9:40:30:
+ 29:18:1e:fe:fc:41:b5:b9:29:05:75:8b:62:1a:5b:22:2e:bf:
+ e4:59:6c:b0
+-----BEGIN CERTIFICATE-----
+MIIEjzCCA3egAwIBAgIQfCoMIT/GVVNFyR8ZH7hO+jANBgkqhkiG9w0BAQsFADBM
+MSAwHgYDVQQLExdHbG9iYWxTaWduIFJvb3QgQ0EgLSBSMzETMBEGA1UEChMKR2xv
+YmFsU2lnbjETMBEGA1UEAxMKR2xvYmFsU2lnbjAeFw0yMjA0MjAxMjAwMDBaFw0y
+NTA0MjAwMDAwMDBaMFgxCzAJBgNVBAYTAkJFMRkwFwYDVQQKExBHbG9iYWxTaWdu
+IG52LXNhMS4wLAYDVQQDEyVHbG9iYWxTaWduIEF0bGFzIFIzIERWIFRMUyBDQSAy
+MDIyIFEzMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAuKh6ZjxOZpzO
+N6VUNU02x5nTqCc28i/G1Rg+6QndBdbXLDQyfAhjSdEQN+V4XRFizm37Lz83lNuP
+ezDpXizZVT+y27mgtWA3i6QGMjVQpAmvCkX/qB+bZY7dSuBAoeNjN1iQ3XU7/A4c
+gkCYvXCxwUgUFDwES2nd1JwBpukh44IK/uSqvzSgjMvJeW4+XGpSnsTtK8Vp/lA8
+k521/y0oqGwGbJ3Fr7JZ+1l3DXR6iISk1B3UuiAGzLUeSE50IRWGdcDMWtEFz1cW
+ehMX7MJKrtUecqoiWoycgjLEEOZCbiGGaHyAIzA1072wXgopK/AUsRg32Vklw+c4
+2enULTY1ZQIDAQABo4IBXzCCAVswDgYDVR0PAQH/BAQDAgGGMB0GA1UdJQQWMBQG
+CCsGAQUFBwMBBggrBgEFBQcDAjASBgNVHRMBAf8ECDAGAQH/AgEAMB0GA1UdDgQW
+BBT6kTljmvutECTlvrW52qvZxEZpqzAfBgNVHSMEGDAWgBSP8Et/qC5FJK5NUPpj
+move4t0bvDB7BggrBgEFBQcBAQRvMG0wLgYIKwYBBQUHMAGGImh0dHA6Ly9vY3Nw
+Mi5nbG9iYWxzaWduLmNvbS9yb290cjMwOwYIKwYBBQUHMAKGL2h0dHA6Ly9zZWN1
+cmUuZ2xvYmFsc2lnbi5jb20vY2FjZXJ0L3Jvb3QtcjMuY3J0MDYGA1UdHwQvMC0w
+K6ApoCeGJWh0dHA6Ly9jcmwuZ2xvYmFsc2lnbi5jb20vcm9vdC1yMy5jcmwwIQYD
+VR0gBBowGDAIBgZngQwBAgEwDAYKKwYBBAGgMgoBAzANBgkqhkiG9w0BAQsFAAOC
+AQEAFDMseeU/gsZwP9pZOKe7onasYRgFaFfZDfuKRrzxqOgMcAIdxi+X7TY+nlKG
+L1xi2NVHQ5pz0Sslh59EtBTrJrwhR3QgvZ+kv7OAHU01fc25tdpV8pBQyLIXTg60
+YYgpX0RdA39XkYHQ6zCu1SrsgiDOTtKwi5UCYXPYaTT0rWMOXOQgH6l97Y7lHAS7
+Ip/HqSLKmT0Cp2foBi36BGu7SdJsmVdjbC3CYXjhILH79r/hgjk5PHvvfRqVSrJy
+2lWQru3d4nCQfBrutTJaXc/W+kXyngEMMS+JhP4xYA/97qZbhNXHGOak+UAwKRge
+/vxBtbkpBXWLYhpbIi6/5FlssA==
+-----END CERTIFICATE-----
+Certificate:
+ Data:
+ Version: 3 (0x2)
+ Serial Number:
+ 04:00:00:00:00:01:21:58:53:08:a2
+ Signature Algorithm: sha256WithRSAEncryption
+ Issuer: OU = GlobalSign Root CA - R3, O = GlobalSign, CN = GlobalSign
+ Validity
+ Not Before: Mar 18 10:00:00 2009 GMT
+ Not After : Mar 18 10:00:00 2029 GMT
+ Subject: OU = GlobalSign Root CA - R3, O = GlobalSign, CN = GlobalSign
+ Subject Public Key Info:
+ Public Key Algorithm: rsaEncryption
+ Public-Key: (2048 bit)
+ Modulus:
+ 00:cc:25:76:90:79:06:78:22:16:f5:c0:83:b6:84:
+ ca:28:9e:fd:05:76:11:c5:ad:88:72:fc:46:02:43:
+ c7:b2:8a:9d:04:5f:24:cb:2e:4b:e1:60:82:46:e1:
+ 52:ab:0c:81:47:70:6c:dd:64:d1:eb:f5:2c:a3:0f:
+ 82:3d:0c:2b:ae:97:d7:b6:14:86:10:79:bb:3b:13:
+ 80:77:8c:08:e1:49:d2:6a:62:2f:1f:5e:fa:96:68:
+ df:89:27:95:38:9f:06:d7:3e:c9:cb:26:59:0d:73:
+ de:b0:c8:e9:26:0e:83:15:c6:ef:5b:8b:d2:04:60:
+ ca:49:a6:28:f6:69:3b:f6:cb:c8:28:91:e5:9d:8a:
+ 61:57:37:ac:74:14:dc:74:e0:3a:ee:72:2f:2e:9c:
+ fb:d0:bb:bf:f5:3d:00:e1:06:33:e8:82:2b:ae:53:
+ a6:3a:16:73:8c:dd:41:0e:20:3a:c0:b4:a7:a1:e9:
+ b2:4f:90:2e:32:60:e9:57:cb:b9:04:92:68:68:e5:
+ 38:26:60:75:b2:9f:77:ff:91:14:ef:ae:20:49:fc:
+ ad:40:15:48:d1:02:31:61:19:5e:b8:97:ef:ad:77:
+ b7:64:9a:7a:bf:5f:c1:13:ef:9b:62:fb:0d:6c:e0:
+ 54:69:16:a9:03:da:6e:e9:83:93:71:76:c6:69:85:
+ 82:17
+ Exponent: 65537 (0x10001)
+ X509v3 extensions:
+ X509v3 Key Usage: critical
+ Certificate Sign, CRL Sign
+ X509v3 Basic Constraints: critical
+ CA:TRUE
+ X509v3 Subject Key Identifier:
+ 8F:F0:4B:7F:A8:2E:45:24:AE:4D:50:FA:63:9A:8B:DE:E2:DD:1B:BC
+ Signature Algorithm: sha256WithRSAEncryption
+ Signature Value:
+ 4b:40:db:c0:50:aa:fe:c8:0c:ef:f7:96:54:45:49:bb:96:00:
+ 09:41:ac:b3:13:86:86:28:07:33:ca:6b:e6:74:b9:ba:00:2d:
+ ae:a4:0a:d3:f5:f1:f1:0f:8a:bf:73:67:4a:83:c7:44:7b:78:
+ e0:af:6e:6c:6f:03:29:8e:33:39:45:c3:8e:e4:b9:57:6c:aa:
+ fc:12:96:ec:53:c6:2d:e4:24:6c:b9:94:63:fb:dc:53:68:67:
+ 56:3e:83:b8:cf:35:21:c3:c9:68:fe:ce:da:c2:53:aa:cc:90:
+ 8a:e9:f0:5d:46:8c:95:dd:7a:58:28:1a:2f:1d:de:cd:00:37:
+ 41:8f:ed:44:6d:d7:53:28:97:7e:f3:67:04:1e:15:d7:8a:96:
+ b4:d3:de:4c:27:a4:4c:1b:73:73:76:f4:17:99:c2:1f:7a:0e:
+ e3:2d:08:ad:0a:1c:2c:ff:3c:ab:55:0e:0f:91:7e:36:eb:c3:
+ 57:49:be:e1:2e:2d:7c:60:8b:c3:41:51:13:23:9d:ce:f7:32:
+ 6b:94:01:a8:99:e7:2c:33:1f:3a:3b:25:d2:86:40:ce:3b:2c:
+ 86:78:c9:61:2f:14:ba:ee:db:55:6f:df:84:ee:05:09:4d:bd:
+ 28:d8:72:ce:d3:62:50:65:1e:eb:92:97:83:31:d9:b3:b5:ca:
+ 47:58:3f:5f
+-----BEGIN CERTIFICATE-----
+MIIDXzCCAkegAwIBAgILBAAAAAABIVhTCKIwDQYJKoZIhvcNAQELBQAwTDEgMB4G
+A1UECxMXR2xvYmFsU2lnbiBSb290IENBIC0gUjMxEzARBgNVBAoTCkdsb2JhbFNp
+Z24xEzARBgNVBAMTCkdsb2JhbFNpZ24wHhcNMDkwMzE4MTAwMDAwWhcNMjkwMzE4
+MTAwMDAwWjBMMSAwHgYDVQQLExdHbG9iYWxTaWduIFJvb3QgQ0EgLSBSMzETMBEG
+A1UEChMKR2xvYmFsU2lnbjETMBEGA1UEAxMKR2xvYmFsU2lnbjCCASIwDQYJKoZI
+hvcNAQEBBQADggEPADCCAQoCggEBAMwldpB5BngiFvXAg7aEyiie/QV2EcWtiHL8
+RgJDx7KKnQRfJMsuS+FggkbhUqsMgUdwbN1k0ev1LKMPgj0MK66X17YUhhB5uzsT
+gHeMCOFJ0mpiLx9e+pZo34knlTifBtc+ycsmWQ1z3rDI6SYOgxXG71uL0gRgykmm
+KPZpO/bLyCiR5Z2KYVc3rHQU3HTgOu5yLy6c+9C7v/U9AOEGM+iCK65TpjoWc4zd
+QQ4gOsC0p6Hpsk+QLjJg6VfLuQSSaGjlOCZgdbKfd/+RFO+uIEn8rUAVSNECMWEZ
+XriX7613t2Saer9fwRPvm2L7DWzgVGkWqQPabumDk3F2xmmFghcCAwEAAaNCMEAw
+DgYDVR0PAQH/BAQDAgEGMA8GA1UdEwEB/wQFMAMBAf8wHQYDVR0OBBYEFI/wS3+o
+LkUkrk1Q+mOai97i3Ru8MA0GCSqGSIb3DQEBCwUAA4IBAQBLQNvAUKr+yAzv95ZU
+RUm7lgAJQayzE4aGKAczymvmdLm6AC2upArT9fHxD4q/c2dKg8dEe3jgr25sbwMp
+jjM5RcOO5LlXbKr8EpbsU8Yt5CRsuZRj+9xTaGdWPoO4zzUhw8lo/s7awlOqzJCK
+6fBdRoyV3XpYKBovHd7NADdBj+1EbddTKJd+82cEHhXXipa0095MJ6RMG3NzdvQX
+mcIfeg7jLQitChws/zyrVQ4PkX4268NXSb7hLi18YIvDQVETI53O9zJrlAGomecs
+Mx86OyXShkDOOyyGeMlhLxS67ttVb9+E7gUJTb0o2HLO02JQZR7rkpeDMdmztcpH
+WD9f
+-----END CERTIFICATE-----
diff --git a/certs/Go Daddy Secure Certificate Authority - G2.pem b/certs/Go Daddy Secure Certificate Authority - G2.pem
deleted file mode 100644
index 72e5054..0000000
--- a/certs/Go Daddy Secure Certificate Authority - G2.pem
+++ /dev/null
@@ -1,51 +0,0 @@
------BEGIN CERTIFICATE-----
-MIIDxTCCAq2gAwIBAgIBADANBgkqhkiG9w0BAQsFADCBgzELMAkGA1UEBhMCVVMx
-EDAOBgNVBAgTB0FyaXpvbmExEzARBgNVBAcTClNjb3R0c2RhbGUxGjAYBgNVBAoT
-EUdvRGFkZHkuY29tLCBJbmMuMTEwLwYDVQQDEyhHbyBEYWRkeSBSb290IENlcnRp
-ZmljYXRlIEF1dGhvcml0eSAtIEcyMB4XDTA5MDkwMTAwMDAwMFoXDTM3MTIzMTIz
-NTk1OVowgYMxCzAJBgNVBAYTAlVTMRAwDgYDVQQIEwdBcml6b25hMRMwEQYDVQQH
-EwpTY290dHNkYWxlMRowGAYDVQQKExFHb0RhZGR5LmNvbSwgSW5jLjExMC8GA1UE
-AxMoR28gRGFkZHkgUm9vdCBDZXJ0aWZpY2F0ZSBBdXRob3JpdHkgLSBHMjCCASIw
-DQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAL9xYgjx+lk09xvJGKP3gElY6SKD
-E6bFIEMBO4Tx5oVJnyfq9oQbTqC023CYxzIBsQU+B07u9PpPL1kwIuerGVZr4oAH
-/PMWdYA5UXvl+TW2dE6pjYIT5LY/qQOD+qK+ihVqf94Lw7YZFAXK6sOoBJQ7Rnwy
-DfMAZiLIjWltNowRGLfTshxgtDj6AozO091GB94KPutdfMh8+7ArU6SSYmlRJQVh
-GkSBjCypQ5Yj36w6gZoOKcUcqeldHraenjAKOc7xiID7S13MMuyFYkMlNAJWJwGR
-tDtwKj9useiciAF9n9T521NtYJ2/LOdYq7hfRvzOxBsDPAnrSTFcaUaz4EcCAwEA
-AaNCMEAwDwYDVR0TAQH/BAUwAwEB/zAOBgNVHQ8BAf8EBAMCAQYwHQYDVR0OBBYE
-FDqahQcQZyi27/a9BUFuIMGU2g/eMA0GCSqGSIb3DQEBCwUAA4IBAQCZ21151fmX
-WWcDYfF+OwYxdS2hII5PZYe096acvNjpL9DbWu7PdIxztDhC2gV7+AJ1uP2lsdeu
-9tfeE8tTEH6KRtGX+rcuKxGrkLAngPnon1rpN5+r5N9ss4UXnT3ZJE95kTXWXwTr
-gIOrmgIttRD02JDHBHNA7XIloKmf7J6raBKZV8aPEjoJpL1E/QYVN8Gb5DKj7Tjo
-2GTzLH4U/ALqn83/B2gX2yKQOC16jdFU8WnjXzPKej17CuPKf1855eJ1usV2GDPO
-LPAvTK33sefOT6jEm0pUBsV/fdUID+Ic/n4XuKxe9tQWskMJDE32p2u0mYRlynqI
-4uJEvlz36hz1
------END CERTIFICATE-----
------BEGIN CERTIFICATE-----
-MIIE0DCCA7igAwIBAgIBBzANBgkqhkiG9w0BAQsFADCBgzELMAkGA1UEBhMCVVMx
-EDAOBgNVBAgTB0FyaXpvbmExEzARBgNVBAcTClNjb3R0c2RhbGUxGjAYBgNVBAoT
-EUdvRGFkZHkuY29tLCBJbmMuMTEwLwYDVQQDEyhHbyBEYWRkeSBSb290IENlcnRp
-ZmljYXRlIEF1dGhvcml0eSAtIEcyMB4XDTExMDUwMzA3MDAwMFoXDTMxMDUwMzA3
-MDAwMFowgbQxCzAJBgNVBAYTAlVTMRAwDgYDVQQIEwdBcml6b25hMRMwEQYDVQQH
-EwpTY290dHNkYWxlMRowGAYDVQQKExFHb0RhZGR5LmNvbSwgSW5jLjEtMCsGA1UE
-CxMkaHR0cDovL2NlcnRzLmdvZGFkZHkuY29tL3JlcG9zaXRvcnkvMTMwMQYDVQQD
-EypHbyBEYWRkeSBTZWN1cmUgQ2VydGlmaWNhdGUgQXV0aG9yaXR5IC0gRzIwggEi
-MA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQC54MsQ1K92vdSTYuswZLiBCGzD
-BNliF44v/z5lz4/OYuY8UhzaFkVLVat4a2ODYpDOD2lsmcgaFItMzEUz6ojcnqOv
-K/6AYZ15V8TPLvQ/MDxdR/yaFrzDN5ZBUY4RS1T4KL7QjL7wMDge87Am+GZHY23e
-cSZHjzhHU9FGHbTj3ADqRay9vHHZqm8A29vNMDp5T19MR/gd71vCxJ1gO7GyQ5HY
-pDNO6rPWJ0+tJYqlxvTV0KaudAVkV4i1RFXULSo6Pvi4vekyCgKUZMQWOlDxSq7n
-eTOvDCAHf+jfBDnCaQJsY1L6d8EbyHSHyLmTGFBUNUtpTrw700kuH9zB0lL7AgMB
-AAGjggEaMIIBFjAPBgNVHRMBAf8EBTADAQH/MA4GA1UdDwEB/wQEAwIBBjAdBgNV
-HQ4EFgQUQMK9J47MNIMwojPX+2yz8LQsgM4wHwYDVR0jBBgwFoAUOpqFBxBnKLbv
-9r0FQW4gwZTaD94wNAYIKwYBBQUHAQEEKDAmMCQGCCsGAQUFBzABhhhodHRwOi8v
-b2NzcC5nb2RhZGR5LmNvbS8wNQYDVR0fBC4wLDAqoCigJoYkaHR0cDovL2NybC5n
-b2RhZGR5LmNvbS9nZHJvb3QtZzIuY3JsMEYGA1UdIAQ/MD0wOwYEVR0gADAzMDEG
-CCsGAQUFBwIBFiVodHRwczovL2NlcnRzLmdvZGFkZHkuY29tL3JlcG9zaXRvcnkv
-MA0GCSqGSIb3DQEBCwUAA4IBAQAIfmyTEMg4uJapkEv/oV9PBO9sPpyIBslQj6Zz
-91cxG7685C/b+LrTW+C05+Z5Yg4MotdqY3MxtfWoSKQ7CC2iXZDXtHwlTxFWMMS2
-RJ17LJ3lXubvDGGqv+QqG+6EnriDfcFDzkSnE3ANkR/0yBOtg2DZ2HKocyQetawi
-DsoXiWJYRBuriSUBAA/NxBti21G00w9RKpv0vHP8ds42pM3Z2Czqrpv1KrKQ0U11
-GIo/ikGQI31bS/6kA1ibRrLDYGCD+H1QQc7CoZDDu+8CL9IVVO5EFdkKrqeKM+2x
-LXY2JtwE65/3YR8V3Idv7kaWKK2hJn0KCacuBKONvPi8BDAB
------END CERTIFICATE-----
diff --git a/certs/Go-Daddy-Secure-Certificate-Authority-G2.pem b/certs/Go-Daddy-Secure-Certificate-Authority-G2.pem
new file mode 100644
index 0000000..4faba90
--- /dev/null
+++ b/certs/Go-Daddy-Secure-Certificate-Authority-G2.pem
@@ -0,0 +1,178 @@
+Certificate:
+ Data:
+ Version: 3 (0x2)
+ Serial Number: 7 (0x7)
+ Signature Algorithm: sha256WithRSAEncryption
+ Issuer: C = US, ST = Arizona, L = Scottsdale, O = "GoDaddy.com, Inc.", CN = Go Daddy Root Certificate Authority - G2
+ Validity
+ Not Before: May 3 07:00:00 2011 GMT
+ Not After : May 3 07:00:00 2031 GMT
+ Subject: C = US, ST = Arizona, L = Scottsdale, O = "GoDaddy.com, Inc.", OU = http://certs.godaddy.com/repository/, CN = Go Daddy Secure Certificate Authority - G2
+ Subject Public Key Info:
+ Public Key Algorithm: rsaEncryption
+ RSA Public-Key: (2048 bit)
+ Modulus:
+ 00:b9:e0:cb:10:d4:af:76:bd:d4:93:62:eb:30:64:
+ b8:81:08:6c:c3:04:d9:62:17:8e:2f:ff:3e:65:cf:
+ 8f:ce:62:e6:3c:52:1c:da:16:45:4b:55:ab:78:6b:
+ 63:83:62:90:ce:0f:69:6c:99:c8:1a:14:8b:4c:cc:
+ 45:33:ea:88:dc:9e:a3:af:2b:fe:80:61:9d:79:57:
+ c4:cf:2e:f4:3f:30:3c:5d:47:fc:9a:16:bc:c3:37:
+ 96:41:51:8e:11:4b:54:f8:28:be:d0:8c:be:f0:30:
+ 38:1e:f3:b0:26:f8:66:47:63:6d:de:71:26:47:8f:
+ 38:47:53:d1:46:1d:b4:e3:dc:00:ea:45:ac:bd:bc:
+ 71:d9:aa:6f:00:db:db:cd:30:3a:79:4f:5f:4c:47:
+ f8:1d:ef:5b:c2:c4:9d:60:3b:b1:b2:43:91:d8:a4:
+ 33:4e:ea:b3:d6:27:4f:ad:25:8a:a5:c6:f4:d5:d0:
+ a6:ae:74:05:64:57:88:b5:44:55:d4:2d:2a:3a:3e:
+ f8:b8:bd:e9:32:0a:02:94:64:c4:16:3a:50:f1:4a:
+ ae:e7:79:33:af:0c:20:07:7f:e8:df:04:39:c2:69:
+ 02:6c:63:52:fa:77:c1:1b:c8:74:87:c8:b9:93:18:
+ 50:54:35:4b:69:4e:bc:3b:d3:49:2e:1f:dc:c1:d2:
+ 52:fb
+ Exponent: 65537 (0x10001)
+ X509v3 extensions:
+ X509v3 Basic Constraints: critical
+ CA:TRUE
+ X509v3 Key Usage: critical
+ Certificate Sign, CRL Sign
+ X509v3 Subject Key Identifier:
+ 40:C2:BD:27:8E:CC:34:83:30:A2:33:D7:FB:6C:B3:F0:B4:2C:80:CE
+ X509v3 Authority Key Identifier:
+ keyid:3A:9A:85:07:10:67:28:B6:EF:F6:BD:05:41:6E:20:C1:94:DA:0F:DE
+
+ Authority Information Access:
+ OCSP - URI:http://ocsp.godaddy.com/
+
+ X509v3 CRL Distribution Points:
+
+ Full Name:
+ URI:http://crl.godaddy.com/gdroot-g2.crl
+
+ X509v3 Certificate Policies:
+ Policy: X509v3 Any Policy
+ CPS: https://certs.godaddy.com/repository/
+
+ Signature Algorithm: sha256WithRSAEncryption
+ 08:7e:6c:93:10:c8:38:b8:96:a9:90:4b:ff:a1:5f:4f:04:ef:
+ 6c:3e:9c:88:06:c9:50:8f:a6:73:f7:57:31:1b:be:bc:e4:2f:
+ db:f8:ba:d3:5b:e0:b4:e7:e6:79:62:0e:0c:a2:d7:6a:63:73:
+ 31:b5:f5:a8:48:a4:3b:08:2d:a2:5d:90:d7:b4:7c:25:4f:11:
+ 56:30:c4:b6:44:9d:7b:2c:9d:e5:5e:e6:ef:0c:61:aa:bf:e4:
+ 2a:1b:ee:84:9e:b8:83:7d:c1:43:ce:44:a7:13:70:0d:91:1f:
+ f4:c8:13:ad:83:60:d9:d8:72:a8:73:24:1e:b5:ac:22:0e:ca:
+ 17:89:62:58:44:1b:ab:89:25:01:00:0f:cd:c4:1b:62:db:51:
+ b4:d3:0f:51:2a:9b:f4:bc:73:fc:76:ce:36:a4:cd:d9:d8:2c:
+ ea:ae:9b:f5:2a:b2:90:d1:4d:75:18:8a:3f:8a:41:90:23:7d:
+ 5b:4b:fe:a4:03:58:9b:46:b2:c3:60:60:83:f8:7d:50:41:ce:
+ c2:a1:90:c3:bb:ef:02:2f:d2:15:54:ee:44:15:d9:0a:ae:a7:
+ 8a:33:ed:b1:2d:76:36:26:dc:04:eb:9f:f7:61:1f:15:dc:87:
+ 6f:ee:46:96:28:ad:a1:26:7d:0a:09:a7:2e:04:a3:8d:bc:f8:
+ bc:04:30:01
+-----BEGIN CERTIFICATE-----
+MIIE0DCCA7igAwIBAgIBBzANBgkqhkiG9w0BAQsFADCBgzELMAkGA1UEBhMCVVMx
+EDAOBgNVBAgTB0FyaXpvbmExEzARBgNVBAcTClNjb3R0c2RhbGUxGjAYBgNVBAoT
+EUdvRGFkZHkuY29tLCBJbmMuMTEwLwYDVQQDEyhHbyBEYWRkeSBSb290IENlcnRp
+ZmljYXRlIEF1dGhvcml0eSAtIEcyMB4XDTExMDUwMzA3MDAwMFoXDTMxMDUwMzA3
+MDAwMFowgbQxCzAJBgNVBAYTAlVTMRAwDgYDVQQIEwdBcml6b25hMRMwEQYDVQQH
+EwpTY290dHNkYWxlMRowGAYDVQQKExFHb0RhZGR5LmNvbSwgSW5jLjEtMCsGA1UE
+CxMkaHR0cDovL2NlcnRzLmdvZGFkZHkuY29tL3JlcG9zaXRvcnkvMTMwMQYDVQQD
+EypHbyBEYWRkeSBTZWN1cmUgQ2VydGlmaWNhdGUgQXV0aG9yaXR5IC0gRzIwggEi
+MA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQC54MsQ1K92vdSTYuswZLiBCGzD
+BNliF44v/z5lz4/OYuY8UhzaFkVLVat4a2ODYpDOD2lsmcgaFItMzEUz6ojcnqOv
+K/6AYZ15V8TPLvQ/MDxdR/yaFrzDN5ZBUY4RS1T4KL7QjL7wMDge87Am+GZHY23e
+cSZHjzhHU9FGHbTj3ADqRay9vHHZqm8A29vNMDp5T19MR/gd71vCxJ1gO7GyQ5HY
+pDNO6rPWJ0+tJYqlxvTV0KaudAVkV4i1RFXULSo6Pvi4vekyCgKUZMQWOlDxSq7n
+eTOvDCAHf+jfBDnCaQJsY1L6d8EbyHSHyLmTGFBUNUtpTrw700kuH9zB0lL7AgMB
+AAGjggEaMIIBFjAPBgNVHRMBAf8EBTADAQH/MA4GA1UdDwEB/wQEAwIBBjAdBgNV
+HQ4EFgQUQMK9J47MNIMwojPX+2yz8LQsgM4wHwYDVR0jBBgwFoAUOpqFBxBnKLbv
+9r0FQW4gwZTaD94wNAYIKwYBBQUHAQEEKDAmMCQGCCsGAQUFBzABhhhodHRwOi8v
+b2NzcC5nb2RhZGR5LmNvbS8wNQYDVR0fBC4wLDAqoCigJoYkaHR0cDovL2NybC5n
+b2RhZGR5LmNvbS9nZHJvb3QtZzIuY3JsMEYGA1UdIAQ/MD0wOwYEVR0gADAzMDEG
+CCsGAQUFBwIBFiVodHRwczovL2NlcnRzLmdvZGFkZHkuY29tL3JlcG9zaXRvcnkv
+MA0GCSqGSIb3DQEBCwUAA4IBAQAIfmyTEMg4uJapkEv/oV9PBO9sPpyIBslQj6Zz
+91cxG7685C/b+LrTW+C05+Z5Yg4MotdqY3MxtfWoSKQ7CC2iXZDXtHwlTxFWMMS2
+RJ17LJ3lXubvDGGqv+QqG+6EnriDfcFDzkSnE3ANkR/0yBOtg2DZ2HKocyQetawi
+DsoXiWJYRBuriSUBAA/NxBti21G00w9RKpv0vHP8ds42pM3Z2Czqrpv1KrKQ0U11
+GIo/ikGQI31bS/6kA1ibRrLDYGCD+H1QQc7CoZDDu+8CL9IVVO5EFdkKrqeKM+2x
+LXY2JtwE65/3YR8V3Idv7kaWKK2hJn0KCacuBKONvPi8BDAB
+-----END CERTIFICATE-----
+Certificate:
+ Data:
+ Version: 3 (0x2)
+ Serial Number: 0 (0x0)
+ Signature Algorithm: sha256WithRSAEncryption
+ Issuer: C = US, ST = Arizona, L = Scottsdale, O = "GoDaddy.com, Inc.", CN = Go Daddy Root Certificate Authority - G2
+ Validity
+ Not Before: Sep 1 00:00:00 2009 GMT
+ Not After : Dec 31 23:59:59 2037 GMT
+ Subject: C = US, ST = Arizona, L = Scottsdale, O = "GoDaddy.com, Inc.", CN = Go Daddy Root Certificate Authority - G2
+ Subject Public Key Info:
+ Public Key Algorithm: rsaEncryption
+ RSA Public-Key: (2048 bit)
+ Modulus:
+ 00:bf:71:62:08:f1:fa:59:34:f7:1b:c9:18:a3:f7:
+ 80:49:58:e9:22:83:13:a6:c5:20:43:01:3b:84:f1:
+ e6:85:49:9f:27:ea:f6:84:1b:4e:a0:b4:db:70:98:
+ c7:32:01:b1:05:3e:07:4e:ee:f4:fa:4f:2f:59:30:
+ 22:e7:ab:19:56:6b:e2:80:07:fc:f3:16:75:80:39:
+ 51:7b:e5:f9:35:b6:74:4e:a9:8d:82:13:e4:b6:3f:
+ a9:03:83:fa:a2:be:8a:15:6a:7f:de:0b:c3:b6:19:
+ 14:05:ca:ea:c3:a8:04:94:3b:46:7c:32:0d:f3:00:
+ 66:22:c8:8d:69:6d:36:8c:11:18:b7:d3:b2:1c:60:
+ b4:38:fa:02:8c:ce:d3:dd:46:07:de:0a:3e:eb:5d:
+ 7c:c8:7c:fb:b0:2b:53:a4:92:62:69:51:25:05:61:
+ 1a:44:81:8c:2c:a9:43:96:23:df:ac:3a:81:9a:0e:
+ 29:c5:1c:a9:e9:5d:1e:b6:9e:9e:30:0a:39:ce:f1:
+ 88:80:fb:4b:5d:cc:32:ec:85:62:43:25:34:02:56:
+ 27:01:91:b4:3b:70:2a:3f:6e:b1:e8:9c:88:01:7d:
+ 9f:d4:f9:db:53:6d:60:9d:bf:2c:e7:58:ab:b8:5f:
+ 46:fc:ce:c4:1b:03:3c:09:eb:49:31:5c:69:46:b3:
+ e0:47
+ Exponent: 65537 (0x10001)
+ X509v3 extensions:
+ X509v3 Basic Constraints: critical
+ CA:TRUE
+ X509v3 Key Usage: critical
+ Certificate Sign, CRL Sign
+ X509v3 Subject Key Identifier:
+ 3A:9A:85:07:10:67:28:B6:EF:F6:BD:05:41:6E:20:C1:94:DA:0F:DE
+ Signature Algorithm: sha256WithRSAEncryption
+ 99:db:5d:79:d5:f9:97:59:67:03:61:f1:7e:3b:06:31:75:2d:
+ a1:20:8e:4f:65:87:b4:f7:a6:9c:bc:d8:e9:2f:d0:db:5a:ee:
+ cf:74:8c:73:b4:38:42:da:05:7b:f8:02:75:b8:fd:a5:b1:d7:
+ ae:f6:d7:de:13:cb:53:10:7e:8a:46:d1:97:fa:b7:2e:2b:11:
+ ab:90:b0:27:80:f9:e8:9f:5a:e9:37:9f:ab:e4:df:6c:b3:85:
+ 17:9d:3d:d9:24:4f:79:91:35:d6:5f:04:eb:80:83:ab:9a:02:
+ 2d:b5:10:f4:d8:90:c7:04:73:40:ed:72:25:a0:a9:9f:ec:9e:
+ ab:68:12:99:57:c6:8f:12:3a:09:a4:bd:44:fd:06:15:37:c1:
+ 9b:e4:32:a3:ed:38:e8:d8:64:f3:2c:7e:14:fc:02:ea:9f:cd:
+ ff:07:68:17:db:22:90:38:2d:7a:8d:d1:54:f1:69:e3:5f:33:
+ ca:7a:3d:7b:0a:e3:ca:7f:5f:39:e5:e2:75:ba:c5:76:18:33:
+ ce:2c:f0:2f:4c:ad:f7:b1:e7:ce:4f:a8:c4:9b:4a:54:06:c5:
+ 7f:7d:d5:08:0f:e2:1c:fe:7e:17:b8:ac:5e:f6:d4:16:b2:43:
+ 09:0c:4d:f6:a7:6b:b4:99:84:65:ca:7a:88:e2:e2:44:be:5c:
+ f7:ea:1c:f5
+-----BEGIN CERTIFICATE-----
+MIIDxTCCAq2gAwIBAgIBADANBgkqhkiG9w0BAQsFADCBgzELMAkGA1UEBhMCVVMx
+EDAOBgNVBAgTB0FyaXpvbmExEzARBgNVBAcTClNjb3R0c2RhbGUxGjAYBgNVBAoT
+EUdvRGFkZHkuY29tLCBJbmMuMTEwLwYDVQQDEyhHbyBEYWRkeSBSb290IENlcnRp
+ZmljYXRlIEF1dGhvcml0eSAtIEcyMB4XDTA5MDkwMTAwMDAwMFoXDTM3MTIzMTIz
+NTk1OVowgYMxCzAJBgNVBAYTAlVTMRAwDgYDVQQIEwdBcml6b25hMRMwEQYDVQQH
+EwpTY290dHNkYWxlMRowGAYDVQQKExFHb0RhZGR5LmNvbSwgSW5jLjExMC8GA1UE
+AxMoR28gRGFkZHkgUm9vdCBDZXJ0aWZpY2F0ZSBBdXRob3JpdHkgLSBHMjCCASIw
+DQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAL9xYgjx+lk09xvJGKP3gElY6SKD
+E6bFIEMBO4Tx5oVJnyfq9oQbTqC023CYxzIBsQU+B07u9PpPL1kwIuerGVZr4oAH
+/PMWdYA5UXvl+TW2dE6pjYIT5LY/qQOD+qK+ihVqf94Lw7YZFAXK6sOoBJQ7Rnwy
+DfMAZiLIjWltNowRGLfTshxgtDj6AozO091GB94KPutdfMh8+7ArU6SSYmlRJQVh
+GkSBjCypQ5Yj36w6gZoOKcUcqeldHraenjAKOc7xiID7S13MMuyFYkMlNAJWJwGR
+tDtwKj9useiciAF9n9T521NtYJ2/LOdYq7hfRvzOxBsDPAnrSTFcaUaz4EcCAwEA
+AaNCMEAwDwYDVR0TAQH/BAUwAwEB/zAOBgNVHQ8BAf8EBAMCAQYwHQYDVR0OBBYE
+FDqahQcQZyi27/a9BUFuIMGU2g/eMA0GCSqGSIb3DQEBCwUAA4IBAQCZ21151fmX
+WWcDYfF+OwYxdS2hII5PZYe096acvNjpL9DbWu7PdIxztDhC2gV7+AJ1uP2lsdeu
+9tfeE8tTEH6KRtGX+rcuKxGrkLAngPnon1rpN5+r5N9ss4UXnT3ZJE95kTXWXwTr
+gIOrmgIttRD02JDHBHNA7XIloKmf7J6raBKZV8aPEjoJpL1E/QYVN8Gb5DKj7Tjo
+2GTzLH4U/ALqn83/B2gX2yKQOC16jdFU8WnjXzPKej17CuPKf1855eJ1usV2GDPO
+LPAvTK33sefOT6jEm0pUBsV/fdUID+Ic/n4XuKxe9tQWskMJDE32p2u0mYRlynqI
+4uJEvlz36hz1
+-----END CERTIFICATE-----
diff --git a/certs/Let's Encrypt Authority X3.pem b/certs/Let's Encrypt Authority X3.pem
deleted file mode 100644
index 7df773f..0000000
--- a/certs/Let's Encrypt Authority X3.pem
+++ /dev/null
@@ -1,83 +0,0 @@
------BEGIN CERTIFICATE-----
-MIIFazCCA1OgAwIBAgIRAIIQz7DSQONZRGPgu2OCiwAwDQYJKoZIhvcNAQELBQAw
-TzELMAkGA1UEBhMCVVMxKTAnBgNVBAoTIEludGVybmV0IFNlY3VyaXR5IFJlc2Vh
-cmNoIEdyb3VwMRUwEwYDVQQDEwxJU1JHIFJvb3QgWDEwHhcNMTUwNjA0MTEwNDM4
-WhcNMzUwNjA0MTEwNDM4WjBPMQswCQYDVQQGEwJVUzEpMCcGA1UEChMgSW50ZXJu
-ZXQgU2VjdXJpdHkgUmVzZWFyY2ggR3JvdXAxFTATBgNVBAMTDElTUkcgUm9vdCBY
-MTCCAiIwDQYJKoZIhvcNAQEBBQADggIPADCCAgoCggIBAK3oJHP0FDfzm54rVygc
-h77ct984kIxuPOZXoHj3dcKi/vVqbvYATyjb3miGbESTtrFj/RQSa78f0uoxmyF+
-0TM8ukj13Xnfs7j/EvEhmkvBioZxaUpmZmyPfjxwv60pIgbz5MDmgK7iS4+3mX6U
-A5/TR5d8mUgjU+g4rk8Kb4Mu0UlXjIB0ttov0DiNewNwIRt18jA8+o+u3dpjq+sW
-T8KOEUt+zwvo/7V3LvSye0rgTBIlDHCNAymg4VMk7BPZ7hm/ELNKjD+Jo2FR3qyH
-B5T0Y3HsLuJvW5iB4YlcNHlsdu87kGJ55tukmi8mxdAQ4Q7e2RCOFvu396j3x+UC
-B5iPNgiV5+I3lg02dZ77DnKxHZu8A/lJBdiB3QW0KtZB6awBdpUKD9jf1b0SHzUv
-KBds0pjBqAlkd25HN7rOrFleaJ1/ctaJxQZBKT5ZPt0m9STJEadao0xAH0ahmbWn
-OlFuhjuefXKnEgV4We0+UXgVCwOPjdAvBbI+e0ocS3MFEvzG6uBQE3xDk3SzynTn
-jh8BCNAw1FtxNrQHusEwMFxIt4I7mKZ9YIqioymCzLq9gwQbooMDQaHWBfEbwrbw
-qHyGO0aoSCqI3Haadr8faqU9GY/rOPNk3sgrDQoo//fb4hVC1CLQJ13hef4Y53CI
-rU7m2Ys6xt0nUW7/vGT1M0NPAgMBAAGjQjBAMA4GA1UdDwEB/wQEAwIBBjAPBgNV
-HRMBAf8EBTADAQH/MB0GA1UdDgQWBBR5tFnme7bl5AFzgAiIyBpY9umbbjANBgkq
-hkiG9w0BAQsFAAOCAgEAVR9YqbyyqFDQDLHYGmkgJykIrGF1XIpu+ILlaS/V9lZL
-ubhzEFnTIZd+50xx+7LSYK05qAvqFyFWhfFQDlnrzuBZ6brJFe+GnY+EgPbk6ZGQ
-3BebYhtF8GaV0nxvwuo77x/Py9auJ/GpsMiu/X1+mvoiBOv/2X/qkSsisRcOj/KK
-NFtY2PwByVS5uCbMiogziUwthDyC3+6WVwW6LLv3xLfHTjuCvjHIInNzktHCgKQ5
-ORAzI4JMPJ+GslWYHb4phowim57iaztXOoJwTdwJx4nLCgdNbOhdjsnvzqvHu7Ur
-TkXWStAmzOVyyghqpZXjFaH3pO3JLF+l+/+sKAIuvtd7u+Nxe5AW0wdeRlN8NwdC
-jNPElpzVmbUq4JUagEiuTDkHzsxHpFKVK7q4+63SM1N95R1NbdWhscdCb+ZAJzVc
-oyi3B43njTOQ5yOf+1CceWxG1bQVs5ZufpsMljq4Ui0/1lvh+wjChP4kqKOJ2qxq
-4RgqsahDYVvTH9w7jXbyLeiNdd8XM2w9U/t7y0Ff/9yi0GE44Za4rF2LN9d11TPA
-mRGunUHBcnWEvgJBQl9nJEiU0Zsnvgc/ubhPgXRR4Xq37Z0j4r7g1SgEEzwxA57d
-emyPxgcYxn/eR44/KJ4EBs+lVDR3veyJm+kXQ99b21/+jh5Xos1AnX5iItreGCc=
------END CERTIFICATE-----
------BEGIN CERTIFICATE-----
-MIIFjTCCA3WgAwIBAgIRANOxciY0IzLc9AUoUSrsnGowDQYJKoZIhvcNAQELBQAw
-TzELMAkGA1UEBhMCVVMxKTAnBgNVBAoTIEludGVybmV0IFNlY3VyaXR5IFJlc2Vh
-cmNoIEdyb3VwMRUwEwYDVQQDEwxJU1JHIFJvb3QgWDEwHhcNMTYxMDA2MTU0MzU1
-WhcNMjExMDA2MTU0MzU1WjBKMQswCQYDVQQGEwJVUzEWMBQGA1UEChMNTGV0J3Mg
-RW5jcnlwdDEjMCEGA1UEAxMaTGV0J3MgRW5jcnlwdCBBdXRob3JpdHkgWDMwggEi
-MA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQCc0wzwWuUuR7dyXTeDs2hjMOrX
-NSYZJeG9vjXxcJIvt7hLQQWrqZ41CFjssSrEaIcLo+N15Obzp2JxunmBYB/XkZqf
-89B4Z3HIaQ6Vkc/+5pnpYDxIzH7KTXcSJJ1HG1rrueweNwAcnKx7pwXqzkrrvUHl
-Npi5y/1tPJZo3yMqQpAMhnRnyH+lmrhSYRQTP2XpgofL2/oOVvaGifOFP5eGr7Dc
-Gu9rDZUWfcQroGWymQQ2dYBrrErzG5BJeC+ilk8qICUpBMZ0wNAxzY8xOJUWuqgz
-uEPxsR/DMH+ieTETPS02+OP88jNquTkxxa/EjQ0dZBYzqvqEKbbUC8DYfcOTAgMB
-AAGjggFnMIIBYzAOBgNVHQ8BAf8EBAMCAYYwEgYDVR0TAQH/BAgwBgEB/wIBADBU
-BgNVHSAETTBLMAgGBmeBDAECATA/BgsrBgEEAYLfEwEBATAwMC4GCCsGAQUFBwIB
-FiJodHRwOi8vY3BzLnJvb3QteDEubGV0c2VuY3J5cHQub3JnMB0GA1UdDgQWBBSo
-SmpjBH3duubRObemRWXv86jsoTAzBgNVHR8ELDAqMCigJqAkhiJodHRwOi8vY3Js
-LnJvb3QteDEubGV0c2VuY3J5cHQub3JnMHIGCCsGAQUFBwEBBGYwZDAwBggrBgEF
-BQcwAYYkaHR0cDovL29jc3Aucm9vdC14MS5sZXRzZW5jcnlwdC5vcmcvMDAGCCsG
-AQUFBzAChiRodHRwOi8vY2VydC5yb290LXgxLmxldHNlbmNyeXB0Lm9yZy8wHwYD
-VR0jBBgwFoAUebRZ5nu25eQBc4AIiMgaWPbpm24wDQYJKoZIhvcNAQELBQADggIB
-ABnPdSA0LTqmRf/Q1eaM2jLonG4bQdEnqOJQ8nCqxOeTRrToEKtwT++36gTSlBGx
-A/5dut82jJQ2jxN8RI8L9QFXrWi4xXnA2EqA10yjHiR6H9cj6MFiOnb5In1eWsRM
-UM2v3e9tNsCAgBukPHAg1lQh07rvFKm/Bz9BCjaxorALINUfZ9DD64j2igLIxle2
-DPxW8dI/F2loHMjXZjqG8RkqZUdoxtID5+90FgsGIfkMpqgRS05f4zPbCEHqCXl1
-eO5HyELTgcVlLXXQDgAWnRzut1hFJeczY1tjQQno6f6s+nMydLN26WuU4s3UYvOu
-OsUxRlJu7TSRHqDC3lSE5XggVkzdaPkuKGQbGpny+01/47hfXXNB7HntWNZ6N2Vw
-p7G6OfY+YQrZwIaQmhrIqJZuigsrbe3W+gdn5ykE9+Ky0VgVUsfxo52mwFYs1JKY
-2PGDuWx8M6DlS6qQkvHaRUo0FMd8TsSlbF0/v965qGFKhSDeQoMpYnwcmQilRh/0
-ayLThlHLN81gSkJjVrPI0Y8xCVPB4twb1PFUd2fPM3sA1tJ83sZ5v8vgFv2yofKR
-PB0t6JzUA81mSqM3kxl5e+IZwhYAyO0OTg3/fs8HqGTNKd9BqoUwSRBzp06JMg5b
-rUCGwbCUDI0mxadJ3Bz4WxR6fyNpBK2yAinWEsikxqEt
------END CERTIFICATE-----
------BEGIN CERTIFICATE-----
-MIIDSjCCAjKgAwIBAgIQRK+wgNajJ7qJMDmGLvhAazANBgkqhkiG9w0BAQUFADA/
-MSQwIgYDVQQKExtEaWdpdGFsIFNpZ25hdHVyZSBUcnVzdCBDby4xFzAVBgNVBAMT
-DkRTVCBSb290IENBIFgzMB4XDTAwMDkzMDIxMTIxOVoXDTIxMDkzMDE0MDExNVow
-PzEkMCIGA1UEChMbRGlnaXRhbCBTaWduYXR1cmUgVHJ1c3QgQ28uMRcwFQYDVQQD
-Ew5EU1QgUm9vdCBDQSBYMzCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEB
-AN+v6ZdQCINXtMxiZfaQguzH0yxrMMpb7NnDfcdAwRgUi+DoM3ZJKuM/IUmTrE4O
-rz5Iy2Xu/NMhD2XSKtkyj4zl93ewEnu1lcCJo6m67XMuegwGMoOifooUMM0RoOEq
-OLl5CjH9UL2AZd+3UWODyOKIYepLYYHsUmu5ouJLGiifSKOeDNoJjj4XLh7dIN9b
-xiqKqy69cK3FCxolkHRyxXtqqzTWMIn/5WgTe1QLyNau7Fqckh49ZLOMxt+/yUFw
-7BZy1SbsOFU5Q9D8/RhcQPGX69Wam40dutolucbY38EVAjqr2m7xPi71XAicPNaD
-aeQQmxkqtilX4+U9m5/wAl0CAwEAAaNCMEAwDwYDVR0TAQH/BAUwAwEB/zAOBgNV
-HQ8BAf8EBAMCAQYwHQYDVR0OBBYEFMSnsaR7LHH62+FLkHX/xBVghYkQMA0GCSqG
-SIb3DQEBBQUAA4IBAQCjGiybFwBcqR7uKGY3Or+Dxz9LwwmglSBd49lZRNI+DT69
-ikugdB/OEIKcdBodfpga3csTS7MgROSR6cz8faXbauX+5v3gTt23ADq1cEmv8uXr
-AvHRAosZy5Q6XkjEGB5YGV8eAlrwDPGxrancWYaLbumR9YbK+rlmM6pZW87ipxZz
-R8srzJmwN0jP41ZL9c8PDHIyh8bwRLtTcm1D9SZImlJnt1ir/md2cXjbDaJWFBM5
-JDGFoqgCWjBH4d1QB7wCCZAA62RjYJsWvIjJEubSfZGL+T0yjWW06XyxV3bqxbYo
-Ob8VZRzI9neWagqNdwvYkQsEjgfbKbYK7p2CNTUQ
------END CERTIFICATE-----
diff --git a/certs/R3.pem b/certs/R3.pem
new file mode 100644
index 0000000..837b709
--- /dev/null
+++ b/certs/R3.pem
@@ -0,0 +1,237 @@
+Certificate:
+ Data:
+ Version: 3 (0x2)
+ Serial Number:
+ 91:2b:08:4a:cf:0c:18:a7:53:f6:d6:2e:25:a7:5f:5a
+ Signature Algorithm: sha256WithRSAEncryption
+ Issuer: C = US, O = Internet Security Research Group, CN = ISRG Root X1
+ Validity
+ Not Before: Sep 4 00:00:00 2020 GMT
+ Not After : Sep 15 16:00:00 2025 GMT
+ Subject: C = US, O = Let's Encrypt, CN = R3
+ Subject Public Key Info:
+ Public Key Algorithm: rsaEncryption
+ RSA Public-Key: (2048 bit)
+ Modulus:
+ 00:bb:02:15:28:cc:f6:a0:94:d3:0f:12:ec:8d:55:
+ 92:c3:f8:82:f1:99:a6:7a:42:88:a7:5d:26:aa:b5:
+ 2b:b9:c5:4c:b1:af:8e:6b:f9:75:c8:a3:d7:0f:47:
+ 94:14:55:35:57:8c:9e:a8:a2:39:19:f5:82:3c:42:
+ a9:4e:6e:f5:3b:c3:2e:db:8d:c0:b0:5c:f3:59:38:
+ e7:ed:cf:69:f0:5a:0b:1b:be:c0:94:24:25:87:fa:
+ 37:71:b3:13:e7:1c:ac:e1:9b:ef:db:e4:3b:45:52:
+ 45:96:a9:c1:53:ce:34:c8:52:ee:b5:ae:ed:8f:de:
+ 60:70:e2:a5:54:ab:b6:6d:0e:97:a5:40:34:6b:2b:
+ d3:bc:66:eb:66:34:7c:fa:6b:8b:8f:57:29:99:f8:
+ 30:17:5d:ba:72:6f:fb:81:c5:ad:d2:86:58:3d:17:
+ c7:e7:09:bb:f1:2b:f7:86:dc:c1:da:71:5d:d4:46:
+ e3:cc:ad:25:c1:88:bc:60:67:75:66:b3:f1:18:f7:
+ a2:5c:e6:53:ff:3a:88:b6:47:a5:ff:13:18:ea:98:
+ 09:77:3f:9d:53:f9:cf:01:e5:f5:a6:70:17:14:af:
+ 63:a4:ff:99:b3:93:9d:dc:53:a7:06:fe:48:85:1d:
+ a1:69:ae:25:75:bb:13:cc:52:03:f5:ed:51:a1:8b:
+ db:15
+ Exponent: 65537 (0x10001)
+ X509v3 extensions:
+ X509v3 Key Usage: critical
+ Digital Signature, Certificate Sign, CRL Sign
+ X509v3 Extended Key Usage:
+ TLS Web Client Authentication, TLS Web Server Authentication
+ X509v3 Basic Constraints: critical
+ CA:TRUE, pathlen:0
+ X509v3 Subject Key Identifier:
+ 14:2E:B3:17:B7:58:56:CB:AE:50:09:40:E6:1F:AF:9D:8B:14:C2:C6
+ X509v3 Authority Key Identifier:
+ keyid:79:B4:59:E6:7B:B6:E5:E4:01:73:80:08:88:C8:1A:58:F6:E9:9B:6E
+
+ Authority Information Access:
+ CA Issuers - URI:http://x1.i.lencr.org/
+
+ X509v3 CRL Distribution Points:
+
+ Full Name:
+ URI:http://x1.c.lencr.org/
+
+ X509v3 Certificate Policies:
+ Policy: 2.23.140.1.2.1
+ Policy: 1.3.6.1.4.1.44947.1.1.1
+
+ Signature Algorithm: sha256WithRSAEncryption
+ 85:ca:4e:47:3e:a3:f7:85:44:85:bc:d5:67:78:b2:98:63:ad:
+ 75:4d:1e:96:3d:33:65:72:54:2d:81:a0:ea:c3:ed:f8:20:bf:
+ 5f:cc:b7:70:00:b7:6e:3b:f6:5e:94:de:e4:20:9f:a6:ef:8b:
+ b2:03:e7:a2:b5:16:3c:91:ce:b4:ed:39:02:e7:7c:25:8a:47:
+ e6:65:6e:3f:46:f4:d9:f0:ce:94:2b:ee:54:ce:12:bc:8c:27:
+ 4b:b8:c1:98:2f:a2:af:cd:71:91:4a:08:b7:c8:b8:23:7b:04:
+ 2d:08:f9:08:57:3e:83:d9:04:33:0a:47:21:78:09:82:27:c3:
+ 2a:c8:9b:b9:ce:5c:f2:64:c8:c0:be:79:c0:4f:8e:6d:44:0c:
+ 5e:92:bb:2e:f7:8b:10:e1:e8:1d:44:29:db:59:20:ed:63:b9:
+ 21:f8:12:26:94:93:57:a0:1d:65:04:c1:0a:22:ae:10:0d:43:
+ 97:a1:18:1f:7e:e0:e0:86:37:b5:5a:b1:bd:30:bf:87:6e:2b:
+ 2a:ff:21:4e:1b:05:c3:f5:18:97:f0:5e:ac:c3:a5:b8:6a:f0:
+ 2e:bc:3b:33:b9:ee:4b:de:cc:fc:e4:af:84:0b:86:3f:c0:55:
+ 43:36:f6:68:e1:36:17:6a:8e:99:d1:ff:a5:40:a7:34:b7:c0:
+ d0:63:39:35:39:75:6e:f2:ba:76:c8:93:02:e9:a9:4b:6c:17:
+ ce:0c:02:d9:bd:81:fb:9f:b7:68:d4:06:65:b3:82:3d:77:53:
+ f8:8e:79:03:ad:0a:31:07:75:2a:43:d8:55:97:72:c4:29:0e:
+ f7:c4:5d:4e:c8:ae:46:84:30:d7:f2:85:5f:18:a1:79:bb:e7:
+ 5e:70:8b:07:e1:86:93:c3:b9:8f:dc:61:71:25:2a:af:df:ed:
+ 25:50:52:68:8b:92:dc:e5:d6:b5:e3:da:7d:d0:87:6c:84:21:
+ 31:ae:82:f5:fb:b9:ab:c8:89:17:3d:e1:4c:e5:38:0e:f6:bd:
+ 2b:bd:96:81:14:eb:d5:db:3d:20:a7:7e:59:d3:e2:f8:58:f9:
+ 5b:b8:48:cd:fe:5c:4f:16:29:fe:1e:55:23:af:c8:11:b0:8d:
+ ea:7c:93:90:17:2f:fd:ac:a2:09:47:46:3f:f0:e9:b0:b7:ff:
+ 28:4d:68:32:d6:67:5e:1e:69:a3:93:b8:f5:9d:8b:2f:0b:d2:
+ 52:43:a6:6f:32:57:65:4d:32:81:df:38:53:85:5d:7e:5d:66:
+ 29:ea:b8:dd:e4:95:b5:cd:b5:56:12:42:cd:c4:4e:c6:25:38:
+ 44:50:6d:ec:ce:00:55:18:fe:e9:49:64:d4:4e:ca:97:9c:b4:
+ 5b:c0:73:a8:ab:b8:47:c2
+-----BEGIN CERTIFICATE-----
+MIIFFjCCAv6gAwIBAgIRAJErCErPDBinU/bWLiWnX1owDQYJKoZIhvcNAQELBQAw
+TzELMAkGA1UEBhMCVVMxKTAnBgNVBAoTIEludGVybmV0IFNlY3VyaXR5IFJlc2Vh
+cmNoIEdyb3VwMRUwEwYDVQQDEwxJU1JHIFJvb3QgWDEwHhcNMjAwOTA0MDAwMDAw
+WhcNMjUwOTE1MTYwMDAwWjAyMQswCQYDVQQGEwJVUzEWMBQGA1UEChMNTGV0J3Mg
+RW5jcnlwdDELMAkGA1UEAxMCUjMwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEK
+AoIBAQC7AhUozPaglNMPEuyNVZLD+ILxmaZ6QoinXSaqtSu5xUyxr45r+XXIo9cP
+R5QUVTVXjJ6oojkZ9YI8QqlObvU7wy7bjcCwXPNZOOftz2nwWgsbvsCUJCWH+jdx
+sxPnHKzhm+/b5DtFUkWWqcFTzjTIUu61ru2P3mBw4qVUq7ZtDpelQDRrK9O8Zutm
+NHz6a4uPVymZ+DAXXbpyb/uBxa3Shlg9F8fnCbvxK/eG3MHacV3URuPMrSXBiLxg
+Z3Vms/EY96Jc5lP/Ooi2R6X/ExjqmAl3P51T+c8B5fWmcBcUr2Ok/5mzk53cU6cG
+/kiFHaFpriV1uxPMUgP17VGhi9sVAgMBAAGjggEIMIIBBDAOBgNVHQ8BAf8EBAMC
+AYYwHQYDVR0lBBYwFAYIKwYBBQUHAwIGCCsGAQUFBwMBMBIGA1UdEwEB/wQIMAYB
+Af8CAQAwHQYDVR0OBBYEFBQusxe3WFbLrlAJQOYfr52LFMLGMB8GA1UdIwQYMBaA
+FHm0WeZ7tuXkAXOACIjIGlj26ZtuMDIGCCsGAQUFBwEBBCYwJDAiBggrBgEFBQcw
+AoYWaHR0cDovL3gxLmkubGVuY3Iub3JnLzAnBgNVHR8EIDAeMBygGqAYhhZodHRw
+Oi8veDEuYy5sZW5jci5vcmcvMCIGA1UdIAQbMBkwCAYGZ4EMAQIBMA0GCysGAQQB
+gt8TAQEBMA0GCSqGSIb3DQEBCwUAA4ICAQCFyk5HPqP3hUSFvNVneLKYY611TR6W
+PTNlclQtgaDqw+34IL9fzLdwALduO/ZelN7kIJ+m74uyA+eitRY8kc607TkC53wl
+ikfmZW4/RvTZ8M6UK+5UzhK8jCdLuMGYL6KvzXGRSgi3yLgjewQtCPkIVz6D2QQz
+CkcheAmCJ8MqyJu5zlzyZMjAvnnAT45tRAxekrsu94sQ4egdRCnbWSDtY7kh+BIm
+lJNXoB1lBMEKIq4QDUOXoRgffuDghje1WrG9ML+Hbisq/yFOGwXD9RiX8F6sw6W4
+avAuvDszue5L3sz85K+EC4Y/wFVDNvZo4TYXao6Z0f+lQKc0t8DQYzk1OXVu8rp2
+yJMC6alLbBfODALZvYH7n7do1AZls4I9d1P4jnkDrQoxB3UqQ9hVl3LEKQ73xF1O
+yK5GhDDX8oVfGKF5u+decIsH4YaTw7mP3GFxJSqv3+0lUFJoi5Lc5da149p90Ids
+hCExroL1+7mryIkXPeFM5TgO9r0rvZaBFOvV2z0gp35Z0+L4WPlbuEjN/lxPFin+
+HlUjr8gRsI3qfJOQFy/9rKIJR0Y/8Omwt/8oTWgy1mdeHmmjk7j1nYsvC9JSQ6Zv
+MldlTTKB3zhThV1+XWYp6rjd5JW1zbVWEkLNxE7GJThEUG3szgBVGP7pSWTUTsqX
+nLRbwHOoq7hHwg==
+-----END CERTIFICATE-----
+Certificate:
+ Data:
+ Version: 3 (0x2)
+ Serial Number:
+ 82:10:cf:b0:d2:40:e3:59:44:63:e0:bb:63:82:8b:00
+ Signature Algorithm: sha256WithRSAEncryption
+ Issuer: C = US, O = Internet Security Research Group, CN = ISRG Root X1
+ Validity
+ Not Before: Jun 4 11:04:38 2015 GMT
+ Not After : Jun 4 11:04:38 2035 GMT
+ Subject: C = US, O = Internet Security Research Group, CN = ISRG Root X1
+ Subject Public Key Info:
+ Public Key Algorithm: rsaEncryption
+ RSA Public-Key: (4096 bit)
+ Modulus:
+ 00:ad:e8:24:73:f4:14:37:f3:9b:9e:2b:57:28:1c:
+ 87:be:dc:b7:df:38:90:8c:6e:3c:e6:57:a0:78:f7:
+ 75:c2:a2:fe:f5:6a:6e:f6:00:4f:28:db:de:68:86:
+ 6c:44:93:b6:b1:63:fd:14:12:6b:bf:1f:d2:ea:31:
+ 9b:21:7e:d1:33:3c:ba:48:f5:dd:79:df:b3:b8:ff:
+ 12:f1:21:9a:4b:c1:8a:86:71:69:4a:66:66:6c:8f:
+ 7e:3c:70:bf:ad:29:22:06:f3:e4:c0:e6:80:ae:e2:
+ 4b:8f:b7:99:7e:94:03:9f:d3:47:97:7c:99:48:23:
+ 53:e8:38:ae:4f:0a:6f:83:2e:d1:49:57:8c:80:74:
+ b6:da:2f:d0:38:8d:7b:03:70:21:1b:75:f2:30:3c:
+ fa:8f:ae:dd:da:63:ab:eb:16:4f:c2:8e:11:4b:7e:
+ cf:0b:e8:ff:b5:77:2e:f4:b2:7b:4a:e0:4c:12:25:
+ 0c:70:8d:03:29:a0:e1:53:24:ec:13:d9:ee:19:bf:
+ 10:b3:4a:8c:3f:89:a3:61:51:de:ac:87:07:94:f4:
+ 63:71:ec:2e:e2:6f:5b:98:81:e1:89:5c:34:79:6c:
+ 76:ef:3b:90:62:79:e6:db:a4:9a:2f:26:c5:d0:10:
+ e1:0e:de:d9:10:8e:16:fb:b7:f7:a8:f7:c7:e5:02:
+ 07:98:8f:36:08:95:e7:e2:37:96:0d:36:75:9e:fb:
+ 0e:72:b1:1d:9b:bc:03:f9:49:05:d8:81:dd:05:b4:
+ 2a:d6:41:e9:ac:01:76:95:0a:0f:d8:df:d5:bd:12:
+ 1f:35:2f:28:17:6c:d2:98:c1:a8:09:64:77:6e:47:
+ 37:ba:ce:ac:59:5e:68:9d:7f:72:d6:89:c5:06:41:
+ 29:3e:59:3e:dd:26:f5:24:c9:11:a7:5a:a3:4c:40:
+ 1f:46:a1:99:b5:a7:3a:51:6e:86:3b:9e:7d:72:a7:
+ 12:05:78:59:ed:3e:51:78:15:0b:03:8f:8d:d0:2f:
+ 05:b2:3e:7b:4a:1c:4b:73:05:12:fc:c6:ea:e0:50:
+ 13:7c:43:93:74:b3:ca:74:e7:8e:1f:01:08:d0:30:
+ d4:5b:71:36:b4:07:ba:c1:30:30:5c:48:b7:82:3b:
+ 98:a6:7d:60:8a:a2:a3:29:82:cc:ba:bd:83:04:1b:
+ a2:83:03:41:a1:d6:05:f1:1b:c2:b6:f0:a8:7c:86:
+ 3b:46:a8:48:2a:88:dc:76:9a:76:bf:1f:6a:a5:3d:
+ 19:8f:eb:38:f3:64:de:c8:2b:0d:0a:28:ff:f7:db:
+ e2:15:42:d4:22:d0:27:5d:e1:79:fe:18:e7:70:88:
+ ad:4e:e6:d9:8b:3a:c6:dd:27:51:6e:ff:bc:64:f5:
+ 33:43:4f
+ Exponent: 65537 (0x10001)
+ X509v3 extensions:
+ X509v3 Key Usage: critical
+ Certificate Sign, CRL Sign
+ X509v3 Basic Constraints: critical
+ CA:TRUE
+ X509v3 Subject Key Identifier:
+ 79:B4:59:E6:7B:B6:E5:E4:01:73:80:08:88:C8:1A:58:F6:E9:9B:6E
+ Signature Algorithm: sha256WithRSAEncryption
+ 55:1f:58:a9:bc:b2:a8:50:d0:0c:b1:d8:1a:69:20:27:29:08:
+ ac:61:75:5c:8a:6e:f8:82:e5:69:2f:d5:f6:56:4b:b9:b8:73:
+ 10:59:d3:21:97:7e:e7:4c:71:fb:b2:d2:60:ad:39:a8:0b:ea:
+ 17:21:56:85:f1:50:0e:59:eb:ce:e0:59:e9:ba:c9:15:ef:86:
+ 9d:8f:84:80:f6:e4:e9:91:90:dc:17:9b:62:1b:45:f0:66:95:
+ d2:7c:6f:c2:ea:3b:ef:1f:cf:cb:d6:ae:27:f1:a9:b0:c8:ae:
+ fd:7d:7e:9a:fa:22:04:eb:ff:d9:7f:ea:91:2b:22:b1:17:0e:
+ 8f:f2:8a:34:5b:58:d8:fc:01:c9:54:b9:b8:26:cc:8a:88:33:
+ 89:4c:2d:84:3c:82:df:ee:96:57:05:ba:2c:bb:f7:c4:b7:c7:
+ 4e:3b:82:be:31:c8:22:73:73:92:d1:c2:80:a4:39:39:10:33:
+ 23:82:4c:3c:9f:86:b2:55:98:1d:be:29:86:8c:22:9b:9e:e2:
+ 6b:3b:57:3a:82:70:4d:dc:09:c7:89:cb:0a:07:4d:6c:e8:5d:
+ 8e:c9:ef:ce:ab:c7:bb:b5:2b:4e:45:d6:4a:d0:26:cc:e5:72:
+ ca:08:6a:a5:95:e3:15:a1:f7:a4:ed:c9:2c:5f:a5:fb:ff:ac:
+ 28:02:2e:be:d7:7b:bb:e3:71:7b:90:16:d3:07:5e:46:53:7c:
+ 37:07:42:8c:d3:c4:96:9c:d5:99:b5:2a:e0:95:1a:80:48:ae:
+ 4c:39:07:ce:cc:47:a4:52:95:2b:ba:b8:fb:ad:d2:33:53:7d:
+ e5:1d:4d:6d:d5:a1:b1:c7:42:6f:e6:40:27:35:5c:a3:28:b7:
+ 07:8d:e7:8d:33:90:e7:23:9f:fb:50:9c:79:6c:46:d5:b4:15:
+ b3:96:6e:7e:9b:0c:96:3a:b8:52:2d:3f:d6:5b:e1:fb:08:c2:
+ 84:fe:24:a8:a3:89:da:ac:6a:e1:18:2a:b1:a8:43:61:5b:d3:
+ 1f:dc:3b:8d:76:f2:2d:e8:8d:75:df:17:33:6c:3d:53:fb:7b:
+ cb:41:5f:ff:dc:a2:d0:61:38:e1:96:b8:ac:5d:8b:37:d7:75:
+ d5:33:c0:99:11:ae:9d:41:c1:72:75:84:be:02:41:42:5f:67:
+ 24:48:94:d1:9b:27:be:07:3f:b9:b8:4f:81:74:51:e1:7a:b7:
+ ed:9d:23:e2:be:e0:d5:28:04:13:3c:31:03:9e:dd:7a:6c:8f:
+ c6:07:18:c6:7f:de:47:8e:3f:28:9e:04:06:cf:a5:54:34:77:
+ bd:ec:89:9b:e9:17:43:df:5b:db:5f:fe:8e:1e:57:a2:cd:40:
+ 9d:7e:62:22:da:de:18:27
+-----BEGIN CERTIFICATE-----
+MIIFazCCA1OgAwIBAgIRAIIQz7DSQONZRGPgu2OCiwAwDQYJKoZIhvcNAQELBQAw
+TzELMAkGA1UEBhMCVVMxKTAnBgNVBAoTIEludGVybmV0IFNlY3VyaXR5IFJlc2Vh
+cmNoIEdyb3VwMRUwEwYDVQQDEwxJU1JHIFJvb3QgWDEwHhcNMTUwNjA0MTEwNDM4
+WhcNMzUwNjA0MTEwNDM4WjBPMQswCQYDVQQGEwJVUzEpMCcGA1UEChMgSW50ZXJu
+ZXQgU2VjdXJpdHkgUmVzZWFyY2ggR3JvdXAxFTATBgNVBAMTDElTUkcgUm9vdCBY
+MTCCAiIwDQYJKoZIhvcNAQEBBQADggIPADCCAgoCggIBAK3oJHP0FDfzm54rVygc
+h77ct984kIxuPOZXoHj3dcKi/vVqbvYATyjb3miGbESTtrFj/RQSa78f0uoxmyF+
+0TM8ukj13Xnfs7j/EvEhmkvBioZxaUpmZmyPfjxwv60pIgbz5MDmgK7iS4+3mX6U
+A5/TR5d8mUgjU+g4rk8Kb4Mu0UlXjIB0ttov0DiNewNwIRt18jA8+o+u3dpjq+sW
+T8KOEUt+zwvo/7V3LvSye0rgTBIlDHCNAymg4VMk7BPZ7hm/ELNKjD+Jo2FR3qyH
+B5T0Y3HsLuJvW5iB4YlcNHlsdu87kGJ55tukmi8mxdAQ4Q7e2RCOFvu396j3x+UC
+B5iPNgiV5+I3lg02dZ77DnKxHZu8A/lJBdiB3QW0KtZB6awBdpUKD9jf1b0SHzUv
+KBds0pjBqAlkd25HN7rOrFleaJ1/ctaJxQZBKT5ZPt0m9STJEadao0xAH0ahmbWn
+OlFuhjuefXKnEgV4We0+UXgVCwOPjdAvBbI+e0ocS3MFEvzG6uBQE3xDk3SzynTn
+jh8BCNAw1FtxNrQHusEwMFxIt4I7mKZ9YIqioymCzLq9gwQbooMDQaHWBfEbwrbw
+qHyGO0aoSCqI3Haadr8faqU9GY/rOPNk3sgrDQoo//fb4hVC1CLQJ13hef4Y53CI
+rU7m2Ys6xt0nUW7/vGT1M0NPAgMBAAGjQjBAMA4GA1UdDwEB/wQEAwIBBjAPBgNV
+HRMBAf8EBTADAQH/MB0GA1UdDgQWBBR5tFnme7bl5AFzgAiIyBpY9umbbjANBgkq
+hkiG9w0BAQsFAAOCAgEAVR9YqbyyqFDQDLHYGmkgJykIrGF1XIpu+ILlaS/V9lZL
+ubhzEFnTIZd+50xx+7LSYK05qAvqFyFWhfFQDlnrzuBZ6brJFe+GnY+EgPbk6ZGQ
+3BebYhtF8GaV0nxvwuo77x/Py9auJ/GpsMiu/X1+mvoiBOv/2X/qkSsisRcOj/KK
+NFtY2PwByVS5uCbMiogziUwthDyC3+6WVwW6LLv3xLfHTjuCvjHIInNzktHCgKQ5
+ORAzI4JMPJ+GslWYHb4phowim57iaztXOoJwTdwJx4nLCgdNbOhdjsnvzqvHu7Ur
+TkXWStAmzOVyyghqpZXjFaH3pO3JLF+l+/+sKAIuvtd7u+Nxe5AW0wdeRlN8NwdC
+jNPElpzVmbUq4JUagEiuTDkHzsxHpFKVK7q4+63SM1N95R1NbdWhscdCb+ZAJzVc
+oyi3B43njTOQ5yOf+1CceWxG1bQVs5ZufpsMljq4Ui0/1lvh+wjChP4kqKOJ2qxq
+4RgqsahDYVvTH9w7jXbyLeiNdd8XM2w9U/t7y0Ff/9yi0GE44Za4rF2LN9d11TPA
+mRGunUHBcnWEvgJBQl9nJEiU0Zsnvgc/ubhPgXRR4Xq37Z0j4r7g1SgEEzwxA57d
+emyPxgcYxn/eR44/KJ4EBs+lVDR3veyJm+kXQ99b21/+jh5Xos1AnX5iItreGCc=
+-----END CERTIFICATE-----
diff --git a/certs/Starfield Secure Certificate Authority - G2.pem b/certs/Starfield Secure Certificate Authority - G2.pem
deleted file mode 100644
index 9c17e74..0000000
--- a/certs/Starfield Secure Certificate Authority - G2.pem
+++ /dev/null
@@ -1,52 +0,0 @@
------BEGIN CERTIFICATE-----
-MIID3TCCAsWgAwIBAgIBADANBgkqhkiG9w0BAQsFADCBjzELMAkGA1UEBhMCVVMx
-EDAOBgNVBAgTB0FyaXpvbmExEzARBgNVBAcTClNjb3R0c2RhbGUxJTAjBgNVBAoT
-HFN0YXJmaWVsZCBUZWNobm9sb2dpZXMsIEluYy4xMjAwBgNVBAMTKVN0YXJmaWVs
-ZCBSb290IENlcnRpZmljYXRlIEF1dGhvcml0eSAtIEcyMB4XDTA5MDkwMTAwMDAw
-MFoXDTM3MTIzMTIzNTk1OVowgY8xCzAJBgNVBAYTAlVTMRAwDgYDVQQIEwdBcml6
-b25hMRMwEQYDVQQHEwpTY290dHNkYWxlMSUwIwYDVQQKExxTdGFyZmllbGQgVGVj
-aG5vbG9naWVzLCBJbmMuMTIwMAYDVQQDEylTdGFyZmllbGQgUm9vdCBDZXJ0aWZp
-Y2F0ZSBBdXRob3JpdHkgLSBHMjCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoC
-ggEBAL3twQP89o/8ArFvW59I2Z154qK3A2FWGMNHttfKPTUuiUP3oWmb3ooa/RMg
-nLRJdzIpVv257IzdIvpy3Cdhl+72WoTsbhm5iSzchFvVdPtrX8WJpRBSiUZV9Lh1
-HOZ/5FSuS/hVclcCGfgXcVnrHigHdMWdSL5stPSksPNkN3mSwOxGXn/hbVNMYq/N
-Hwtjuzqd+/x5AJhhdM8mgkBj87JyahkNmcrUDnXMN/uLicFZ8WJ/X7NfZTD4p7dN
-dloedl40wOiWVpmKs/B/pM293DIxfJHP4F8R+GuqSVzRmZTRouNjWwl2tVZi4Ut0
-HZbUJtQIBFnQmA4O5t78w+wfkPECAwEAAaNCMEAwDwYDVR0TAQH/BAUwAwEB/zAO
-BgNVHQ8BAf8EBAMCAQYwHQYDVR0OBBYEFHwMMh+n2TB/xH1oo2Kooc6rB1snMA0G
-CSqGSIb3DQEBCwUAA4IBAQARWfolTwNvlJk7mh+ChTnUdgWUXuEok21iXQnCoKjU
-sHU48TRqneSfioYmUeYs0cYtbpUgSpIB7LiKZ3sx4mcujJUDJi5DnUox9g61DLu3
-4jd/IroAow57UvtruzvE03lRTs2Q9GcHGcg8RnoNAX3FWOdt5oUwF5okxBDgBPfg
-8n/Uqgr/Qh037ZTlZFkSIHc40zI+OIF1lnP6aI+xy84fxez6nH7PfrHxBy22/L/K
-pL/QlwVKvOoYKAKQvVR4CSFx09F9HdkWsKlhPdAKACL8x3vLCWRFCztAgfd9fDL1
-mMpYjn0q7pBZc2T5NnReJaH1ZgUufzkVqSr7UIuOhWn0
------END CERTIFICATE-----
------BEGIN CERTIFICATE-----
-MIIFADCCA+igAwIBAgIBBzANBgkqhkiG9w0BAQsFADCBjzELMAkGA1UEBhMCVVMx
-EDAOBgNVBAgTB0FyaXpvbmExEzARBgNVBAcTClNjb3R0c2RhbGUxJTAjBgNVBAoT
-HFN0YXJmaWVsZCBUZWNobm9sb2dpZXMsIEluYy4xMjAwBgNVBAMTKVN0YXJmaWVs
-ZCBSb290IENlcnRpZmljYXRlIEF1dGhvcml0eSAtIEcyMB4XDTExMDUwMzA3MDAw
-MFoXDTMxMDUwMzA3MDAwMFowgcYxCzAJBgNVBAYTAlVTMRAwDgYDVQQIEwdBcml6
-b25hMRMwEQYDVQQHEwpTY290dHNkYWxlMSUwIwYDVQQKExxTdGFyZmllbGQgVGVj
-aG5vbG9naWVzLCBJbmMuMTMwMQYDVQQLEypodHRwOi8vY2VydHMuc3RhcmZpZWxk
-dGVjaC5jb20vcmVwb3NpdG9yeS8xNDAyBgNVBAMTK1N0YXJmaWVsZCBTZWN1cmUg
-Q2VydGlmaWNhdGUgQXV0aG9yaXR5IC0gRzIwggEiMA0GCSqGSIb3DQEBAQUAA4IB
-DwAwggEKAoIBAQDlkGZL7PlGcakgg77pbL9KyUhpgXVObST2yxcT+LBxWYR6ayuF
-pDS1FuXLzOlBcCykLtb6Mn3hqN6UEKwxwcDYav9ZJ6t21vwLdGu4p64/xFT0tDFE
-3ZNWjKRMXpuJyySDm+JXfbfYEh/JhW300YDxUJuHrtQLEAX7J7oobRfpDtZNuTlV
-Bv8KJAV+L8YdcmzUiymMV33a2etmGtNPp99/UsQwxaXJDgLFU793OGgGJMNmyDd+
-MB5FcSM1/5DYKp2N57CSTTx/KgqT3M0WRmX3YISLdkuRJ3MUkuDq7o8W6o0OPnYX
-v32JgIBEQ+ct4EMJddo26K3biTr1XRKOIwSDAgMBAAGjggEsMIIBKDAPBgNVHRMB
-Af8EBTADAQH/MA4GA1UdDwEB/wQEAwIBBjAdBgNVHQ4EFgQUJUWBaFAmOD07LSy+
-zWrZtj2zZmMwHwYDVR0jBBgwFoAUfAwyH6fZMH/EfWijYqihzqsHWycwOgYIKwYB
-BQUHAQEELjAsMCoGCCsGAQUFBzABhh5odHRwOi8vb2NzcC5zdGFyZmllbGR0ZWNo
-LmNvbS8wOwYDVR0fBDQwMjAwoC6gLIYqaHR0cDovL2NybC5zdGFyZmllbGR0ZWNo
-LmNvbS9zZnJvb3QtZzIuY3JsMEwGA1UdIARFMEMwQQYEVR0gADA5MDcGCCsGAQUF
-BwIBFitodHRwczovL2NlcnRzLnN0YXJmaWVsZHRlY2guY29tL3JlcG9zaXRvcnkv
-MA0GCSqGSIb3DQEBCwUAA4IBAQBWZcr+8z8KqJOLGMfeQ2kTNCC+Tl94qGuc22pN
-QdvBE+zcMQAiXvcAngzgNGU0+bE6TkjIEoGIXFs+CFN69xpk37hQYcxTUUApS8L0
-rjpf5MqtJsxOYUPl/VemN3DOQyuwlMOS6eFfqhBJt2nk4NAfZKQrzR9voPiEJBjO
-eT2pkb9UGBOJmVQRDVXFJgt5T1ocbvlj2xSApAer+rKluYjdkf5lO6Sjeb6JTeHQ
-sPTIFwwKlhR8Cbds4cLYVdQYoKpBaXAko7nv6VrcPuuUSvC33l8Odvr7+2kDRUBQ
-7nIMpBKGgc0T0U7EPMpODdIm8QC3tKai4W56gf0wrHofx1l7
------END CERTIFICATE-----
diff --git a/certs/Starfield-Secure-Certificate-Authority-G2.pem b/certs/Starfield-Secure-Certificate-Authority-G2.pem
new file mode 100644
index 0000000..7772e6b
--- /dev/null
+++ b/certs/Starfield-Secure-Certificate-Authority-G2.pem
@@ -0,0 +1,179 @@
+Certificate:
+ Data:
+ Version: 3 (0x2)
+ Serial Number: 7 (0x7)
+ Signature Algorithm: sha256WithRSAEncryption
+ Issuer: C = US, ST = Arizona, L = Scottsdale, O = "Starfield Technologies, Inc.", CN = Starfield Root Certificate Authority - G2
+ Validity
+ Not Before: May 3 07:00:00 2011 GMT
+ Not After : May 3 07:00:00 2031 GMT
+ Subject: C = US, ST = Arizona, L = Scottsdale, O = "Starfield Technologies, Inc.", OU = http://certs.starfieldtech.com/repository/, CN = Starfield Secure Certificate Authority - G2
+ Subject Public Key Info:
+ Public Key Algorithm: rsaEncryption
+ RSA Public-Key: (2048 bit)
+ Modulus:
+ 00:e5:90:66:4b:ec:f9:46:71:a9:20:83:be:e9:6c:
+ bf:4a:c9:48:69:81:75:4e:6d:24:f6:cb:17:13:f8:
+ b0:71:59:84:7a:6b:2b:85:a4:34:b5:16:e5:cb:cc:
+ e9:41:70:2c:a4:2e:d6:fa:32:7d:e1:a8:de:94:10:
+ ac:31:c1:c0:d8:6a:ff:59:27:ab:76:d6:fc:0b:74:
+ 6b:b8:a7:ae:3f:c4:54:f4:b4:31:44:dd:93:56:8c:
+ a4:4c:5e:9b:89:cb:24:83:9b:e2:57:7d:b7:d8:12:
+ 1f:c9:85:6d:f4:d1:80:f1:50:9b:87:ae:d4:0b:10:
+ 05:fb:27:ba:28:6d:17:e9:0e:d6:4d:b9:39:55:06:
+ ff:0a:24:05:7e:2f:c6:1d:72:6c:d4:8b:29:8c:57:
+ 7d:da:d9:eb:66:1a:d3:4f:a7:df:7f:52:c4:30:c5:
+ a5:c9:0e:02:c5:53:bf:77:38:68:06:24:c3:66:c8:
+ 37:7e:30:1e:45:71:23:35:ff:90:d8:2a:9d:8d:e7:
+ b0:92:4d:3c:7f:2a:0a:93:dc:cd:16:46:65:f7:60:
+ 84:8b:76:4b:91:27:73:14:92:e0:ea:ee:8f:16:ea:
+ 8d:0e:3e:76:17:bf:7d:89:80:80:44:43:e7:2d:e0:
+ 43:09:75:da:36:e8:ad:db:89:3a:f5:5d:12:8e:23:
+ 04:83
+ Exponent: 65537 (0x10001)
+ X509v3 extensions:
+ X509v3 Basic Constraints: critical
+ CA:TRUE
+ X509v3 Key Usage: critical
+ Certificate Sign, CRL Sign
+ X509v3 Subject Key Identifier:
+ 25:45:81:68:50:26:38:3D:3B:2D:2C:BE:CD:6A:D9:B6:3D:B3:66:63
+ X509v3 Authority Key Identifier:
+ keyid:7C:0C:32:1F:A7:D9:30:7F:C4:7D:68:A3:62:A8:A1:CE:AB:07:5B:27
+
+ Authority Information Access:
+ OCSP - URI:http://ocsp.starfieldtech.com/
+
+ X509v3 CRL Distribution Points:
+
+ Full Name:
+ URI:http://crl.starfieldtech.com/sfroot-g2.crl
+
+ X509v3 Certificate Policies:
+ Policy: X509v3 Any Policy
+ CPS: https://certs.starfieldtech.com/repository/
+
+ Signature Algorithm: sha256WithRSAEncryption
+ 56:65:ca:fe:f3:3f:0a:a8:93:8b:18:c7:de:43:69:13:34:20:
+ be:4e:5f:78:a8:6b:9c:db:6a:4d:41:db:c1:13:ec:dc:31:00:
+ 22:5e:f7:00:9e:0c:e0:34:65:34:f9:b1:3a:4e:48:c8:12:81:
+ 88:5c:5b:3e:08:53:7a:f7:1a:64:df:b8:50:61:cc:53:51:40:
+ 29:4b:c2:f4:ae:3a:5f:e4:ca:ad:26:cc:4e:61:43:e5:fd:57:
+ a6:37:70:ce:43:2b:b0:94:c3:92:e9:e1:5f:aa:10:49:b7:69:
+ e4:e0:d0:1f:64:a4:2b:cd:1f:6f:a0:f8:84:24:18:ce:79:3d:
+ a9:91:bf:54:18:13:89:99:54:11:0d:55:c5:26:0b:79:4f:5a:
+ 1c:6e:f9:63:db:14:80:a4:07:ab:fa:b2:a5:b9:88:dd:91:fe:
+ 65:3b:a4:a3:79:be:89:4d:e1:d0:b0:f4:c8:17:0c:0a:96:14:
+ 7c:09:b7:6c:e1:c2:d8:55:d4:18:a0:aa:41:69:70:24:a3:b9:
+ ef:e9:5a:dc:3e:eb:94:4a:f0:b7:de:5f:0e:76:fa:fb:fb:69:
+ 03:45:40:50:ee:72:0c:a4:12:86:81:cd:13:d1:4e:c4:3c:ca:
+ 4e:0d:d2:26:f1:00:b7:b4:a6:a2:e1:6e:7a:81:fd:30:ac:7a:
+ 1f:c7:59:7b
+-----BEGIN CERTIFICATE-----
+MIIFADCCA+igAwIBAgIBBzANBgkqhkiG9w0BAQsFADCBjzELMAkGA1UEBhMCVVMx
+EDAOBgNVBAgTB0FyaXpvbmExEzARBgNVBAcTClNjb3R0c2RhbGUxJTAjBgNVBAoT
+HFN0YXJmaWVsZCBUZWNobm9sb2dpZXMsIEluYy4xMjAwBgNVBAMTKVN0YXJmaWVs
+ZCBSb290IENlcnRpZmljYXRlIEF1dGhvcml0eSAtIEcyMB4XDTExMDUwMzA3MDAw
+MFoXDTMxMDUwMzA3MDAwMFowgcYxCzAJBgNVBAYTAlVTMRAwDgYDVQQIEwdBcml6
+b25hMRMwEQYDVQQHEwpTY290dHNkYWxlMSUwIwYDVQQKExxTdGFyZmllbGQgVGVj
+aG5vbG9naWVzLCBJbmMuMTMwMQYDVQQLEypodHRwOi8vY2VydHMuc3RhcmZpZWxk
+dGVjaC5jb20vcmVwb3NpdG9yeS8xNDAyBgNVBAMTK1N0YXJmaWVsZCBTZWN1cmUg
+Q2VydGlmaWNhdGUgQXV0aG9yaXR5IC0gRzIwggEiMA0GCSqGSIb3DQEBAQUAA4IB
+DwAwggEKAoIBAQDlkGZL7PlGcakgg77pbL9KyUhpgXVObST2yxcT+LBxWYR6ayuF
+pDS1FuXLzOlBcCykLtb6Mn3hqN6UEKwxwcDYav9ZJ6t21vwLdGu4p64/xFT0tDFE
+3ZNWjKRMXpuJyySDm+JXfbfYEh/JhW300YDxUJuHrtQLEAX7J7oobRfpDtZNuTlV
+Bv8KJAV+L8YdcmzUiymMV33a2etmGtNPp99/UsQwxaXJDgLFU793OGgGJMNmyDd+
+MB5FcSM1/5DYKp2N57CSTTx/KgqT3M0WRmX3YISLdkuRJ3MUkuDq7o8W6o0OPnYX
+v32JgIBEQ+ct4EMJddo26K3biTr1XRKOIwSDAgMBAAGjggEsMIIBKDAPBgNVHRMB
+Af8EBTADAQH/MA4GA1UdDwEB/wQEAwIBBjAdBgNVHQ4EFgQUJUWBaFAmOD07LSy+
+zWrZtj2zZmMwHwYDVR0jBBgwFoAUfAwyH6fZMH/EfWijYqihzqsHWycwOgYIKwYB
+BQUHAQEELjAsMCoGCCsGAQUFBzABhh5odHRwOi8vb2NzcC5zdGFyZmllbGR0ZWNo
+LmNvbS8wOwYDVR0fBDQwMjAwoC6gLIYqaHR0cDovL2NybC5zdGFyZmllbGR0ZWNo
+LmNvbS9zZnJvb3QtZzIuY3JsMEwGA1UdIARFMEMwQQYEVR0gADA5MDcGCCsGAQUF
+BwIBFitodHRwczovL2NlcnRzLnN0YXJmaWVsZHRlY2guY29tL3JlcG9zaXRvcnkv
+MA0GCSqGSIb3DQEBCwUAA4IBAQBWZcr+8z8KqJOLGMfeQ2kTNCC+Tl94qGuc22pN
+QdvBE+zcMQAiXvcAngzgNGU0+bE6TkjIEoGIXFs+CFN69xpk37hQYcxTUUApS8L0
+rjpf5MqtJsxOYUPl/VemN3DOQyuwlMOS6eFfqhBJt2nk4NAfZKQrzR9voPiEJBjO
+eT2pkb9UGBOJmVQRDVXFJgt5T1ocbvlj2xSApAer+rKluYjdkf5lO6Sjeb6JTeHQ
+sPTIFwwKlhR8Cbds4cLYVdQYoKpBaXAko7nv6VrcPuuUSvC33l8Odvr7+2kDRUBQ
+7nIMpBKGgc0T0U7EPMpODdIm8QC3tKai4W56gf0wrHofx1l7
+-----END CERTIFICATE-----
+Certificate:
+ Data:
+ Version: 3 (0x2)
+ Serial Number: 0 (0x0)
+ Signature Algorithm: sha256WithRSAEncryption
+ Issuer: C = US, ST = Arizona, L = Scottsdale, O = "Starfield Technologies, Inc.", CN = Starfield Root Certificate Authority - G2
+ Validity
+ Not Before: Sep 1 00:00:00 2009 GMT
+ Not After : Dec 31 23:59:59 2037 GMT
+ Subject: C = US, ST = Arizona, L = Scottsdale, O = "Starfield Technologies, Inc.", CN = Starfield Root Certificate Authority - G2
+ Subject Public Key Info:
+ Public Key Algorithm: rsaEncryption
+ RSA Public-Key: (2048 bit)
+ Modulus:
+ 00:bd:ed:c1:03:fc:f6:8f:fc:02:b1:6f:5b:9f:48:
+ d9:9d:79:e2:a2:b7:03:61:56:18:c3:47:b6:d7:ca:
+ 3d:35:2e:89:43:f7:a1:69:9b:de:8a:1a:fd:13:20:
+ 9c:b4:49:77:32:29:56:fd:b9:ec:8c:dd:22:fa:72:
+ dc:27:61:97:ee:f6:5a:84:ec:6e:19:b9:89:2c:dc:
+ 84:5b:d5:74:fb:6b:5f:c5:89:a5:10:52:89:46:55:
+ f4:b8:75:1c:e6:7f:e4:54:ae:4b:f8:55:72:57:02:
+ 19:f8:17:71:59:eb:1e:28:07:74:c5:9d:48:be:6c:
+ b4:f4:a4:b0:f3:64:37:79:92:c0:ec:46:5e:7f:e1:
+ 6d:53:4c:62:af:cd:1f:0b:63:bb:3a:9d:fb:fc:79:
+ 00:98:61:74:cf:26:82:40:63:f3:b2:72:6a:19:0d:
+ 99:ca:d4:0e:75:cc:37:fb:8b:89:c1:59:f1:62:7f:
+ 5f:b3:5f:65:30:f8:a7:b7:4d:76:5a:1e:76:5e:34:
+ c0:e8:96:56:99:8a:b3:f0:7f:a4:cd:bd:dc:32:31:
+ 7c:91:cf:e0:5f:11:f8:6b:aa:49:5c:d1:99:94:d1:
+ a2:e3:63:5b:09:76:b5:56:62:e1:4b:74:1d:96:d4:
+ 26:d4:08:04:59:d0:98:0e:0e:e6:de:fc:c3:ec:1f:
+ 90:f1
+ Exponent: 65537 (0x10001)
+ X509v3 extensions:
+ X509v3 Basic Constraints: critical
+ CA:TRUE
+ X509v3 Key Usage: critical
+ Certificate Sign, CRL Sign
+ X509v3 Subject Key Identifier:
+ 7C:0C:32:1F:A7:D9:30:7F:C4:7D:68:A3:62:A8:A1:CE:AB:07:5B:27
+ Signature Algorithm: sha256WithRSAEncryption
+ 11:59:fa:25:4f:03:6f:94:99:3b:9a:1f:82:85:39:d4:76:05:
+ 94:5e:e1:28:93:6d:62:5d:09:c2:a0:a8:d4:b0:75:38:f1:34:
+ 6a:9d:e4:9f:8a:86:26:51:e6:2c:d1:c6:2d:6e:95:20:4a:92:
+ 01:ec:b8:8a:67:7b:31:e2:67:2e:8c:95:03:26:2e:43:9d:4a:
+ 31:f6:0e:b5:0c:bb:b7:e2:37:7f:22:ba:00:a3:0e:7b:52:fb:
+ 6b:bb:3b:c4:d3:79:51:4e:cd:90:f4:67:07:19:c8:3c:46:7a:
+ 0d:01:7d:c5:58:e7:6d:e6:85:30:17:9a:24:c4:10:e0:04:f7:
+ e0:f2:7f:d4:aa:0a:ff:42:1d:37:ed:94:e5:64:59:12:20:77:
+ 38:d3:32:3e:38:81:75:96:73:fa:68:8f:b1:cb:ce:1f:c5:ec:
+ fa:9c:7e:cf:7e:b1:f1:07:2d:b6:fc:bf:ca:a4:bf:d0:97:05:
+ 4a:bc:ea:18:28:02:90:bd:54:78:09:21:71:d3:d1:7d:1d:d9:
+ 16:b0:a9:61:3d:d0:0a:00:22:fc:c7:7b:cb:09:64:45:0b:3b:
+ 40:81:f7:7d:7c:32:f5:98:ca:58:8e:7d:2a:ee:90:59:73:64:
+ f9:36:74:5e:25:a1:f5:66:05:2e:7f:39:15:a9:2a:fb:50:8b:
+ 8e:85:69:f4
+-----BEGIN CERTIFICATE-----
+MIID3TCCAsWgAwIBAgIBADANBgkqhkiG9w0BAQsFADCBjzELMAkGA1UEBhMCVVMx
+EDAOBgNVBAgTB0FyaXpvbmExEzARBgNVBAcTClNjb3R0c2RhbGUxJTAjBgNVBAoT
+HFN0YXJmaWVsZCBUZWNobm9sb2dpZXMsIEluYy4xMjAwBgNVBAMTKVN0YXJmaWVs
+ZCBSb290IENlcnRpZmljYXRlIEF1dGhvcml0eSAtIEcyMB4XDTA5MDkwMTAwMDAw
+MFoXDTM3MTIzMTIzNTk1OVowgY8xCzAJBgNVBAYTAlVTMRAwDgYDVQQIEwdBcml6
+b25hMRMwEQYDVQQHEwpTY290dHNkYWxlMSUwIwYDVQQKExxTdGFyZmllbGQgVGVj
+aG5vbG9naWVzLCBJbmMuMTIwMAYDVQQDEylTdGFyZmllbGQgUm9vdCBDZXJ0aWZp
+Y2F0ZSBBdXRob3JpdHkgLSBHMjCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoC
+ggEBAL3twQP89o/8ArFvW59I2Z154qK3A2FWGMNHttfKPTUuiUP3oWmb3ooa/RMg
+nLRJdzIpVv257IzdIvpy3Cdhl+72WoTsbhm5iSzchFvVdPtrX8WJpRBSiUZV9Lh1
+HOZ/5FSuS/hVclcCGfgXcVnrHigHdMWdSL5stPSksPNkN3mSwOxGXn/hbVNMYq/N
+Hwtjuzqd+/x5AJhhdM8mgkBj87JyahkNmcrUDnXMN/uLicFZ8WJ/X7NfZTD4p7dN
+dloedl40wOiWVpmKs/B/pM293DIxfJHP4F8R+GuqSVzRmZTRouNjWwl2tVZi4Ut0
+HZbUJtQIBFnQmA4O5t78w+wfkPECAwEAAaNCMEAwDwYDVR0TAQH/BAUwAwEB/zAO
+BgNVHQ8BAf8EBAMCAQYwHQYDVR0OBBYEFHwMMh+n2TB/xH1oo2Kooc6rB1snMA0G
+CSqGSIb3DQEBCwUAA4IBAQARWfolTwNvlJk7mh+ChTnUdgWUXuEok21iXQnCoKjU
+sHU48TRqneSfioYmUeYs0cYtbpUgSpIB7LiKZ3sx4mcujJUDJi5DnUox9g61DLu3
+4jd/IroAow57UvtruzvE03lRTs2Q9GcHGcg8RnoNAX3FWOdt5oUwF5okxBDgBPfg
+8n/Uqgr/Qh037ZTlZFkSIHc40zI+OIF1lnP6aI+xy84fxez6nH7PfrHxBy22/L/K
+pL/QlwVKvOoYKAKQvVR4CSFx09F9HdkWsKlhPdAKACL8x3vLCWRFCztAgfd9fDL1
+mMpYjn0q7pBZc2T5NnReJaH1ZgUufzkVqSr7UIuOhWn0
+-----END CERTIFICATE-----
diff --git a/check-certificates b/check-certificates
deleted file mode 100644
index f378f01..0000000
--- a/check-certificates
+++ /dev/null
@@ -1,104 +0,0 @@
-#!rsc
-# RouterOS script: check-certificates
-# Copyright (c) 2013-2020 Christian Hesse <mail@eworm.de>
-#
-# check for certificate validity
-
-:global Identity;
-:global CertRenewUrl;
-:global CertRenewPass;
-
-:global ParseKeyValueStore;
-:global SendNotification;
-:global UrlEncode;
-:global WaitForFile;
-
-:local FormatExpire do={
- :global CharacterReplace;
- :return [ $CharacterReplace [ $CharacterReplace [ :tostr $1 ] "w" "w " ] "d" "d " ];
-}
-
-:foreach Cert in=[ / certificate find where !revoked !ca expires-after<3w ] do={
- :local CertVal [ / certificate get $Cert ];
-
- :do {
- :if ([ :len $CertRenewUrl ] = 0) do={
- :log info "No CertRenewUrl given.";
- :error "No CertRenewUrl given.";
- }
-
- :foreach Type in={ ".pem"; ".p12" } do={
- :local CertFileName ([ $UrlEncode ($CertVal->"common-name") ] . $Type);
- :do {
- / tool fetch check-certificate=yes-without-crl \
- ($CertRenewUrl . $CertFileName) dst-path=$CertFileName;
- $WaitForFile $CertFileName;
- :foreach PassPhrase in=$CertRenewPass do={
- / certificate import file-name=$CertFileName passphrase=$PassPhrase;
- }
- / file remove [ find where name=$CertFileName ];
- } on-error={
- :log debug ("Could not download certificate file " . $CertFileName);
- }
- }
-
- :local CertNew [ / certificate find where common-name=($CertVal->"common-name") fingerprint!=[ :tostr ($CertVal->"fingerprint") ] expires-after>3w ];
- :local CertNewVal [ / certificate get $CertNew ];
-
- :if ($Cert != $CertNew) do={
- :log debug ("Certificate '" . $CertVal->"name" . "' was not updated, but replaced.");
-
- / ip service set certificate=($CertNewVal->"name") [ find where certificate=($CertVal->"name") ];
-
- :do {
- / ip ipsec identity set certificate=($CertNewVal->"name") [ / ip ipsec identity find where certificate=($CertVal->"name") ];
- / ip ipsec identity set remote-certificate=($CertNewVal->"name") [ / ip ipsec identity find where remote-certificate=($CertVal->"name") ];
- } on-error={
- :log debug ("Setting IPSEC certificates failed. Package 'security' not installed?");
- }
-
- :do {
- / ip hotspot profile set ssl-certificate=($CertNewVal->"name") [ / ip hotspot profile find where ssl-certificate=($CertVal->"name") ];
- } on-error={
- :log debug ("Setting hotspot certificates failed. Package 'hotspot' not installed?");
- }
-
- / certificate remove $Cert;
- / certificate set $CertNew name=($CertVal->"name");
- }
-
- $SendNotification ("Certificate renewed") \
- ("A certificate on " . $Identity . " has been renewed.\n\n" . \
- "Name: " . ($CertVal->"name") . "\n" . \
- "CommonName: " . ($CertNewVal->"common-name") . "\n" . \
- "Fingerprint: " . ($CertNewVal->"fingerprint") . "\n" . \
- "Issuer: " . ([ $ParseKeyValueStore ($CertNewVal->"issuer") ]->"CN") . "\n" . \
- "Validity: " . ($CertNewVal->"invalid-before") . " to " . ($CertNewVal->"invalid-after") . "\n" . \
- "Expires in: " . [ $FormatExpire ($CertNewVal->"expires-after") ]) "" "true";
- :log info ("The certificate " . ($CertVal->"name") . " has been renewed.");
- } on-error={
- :log debug ("Could not renew certificate " . ($CertVal->"name") . ".");
- }
-}
-
-:foreach Cert in=[ / certificate find where !revoked expires-after<2w fingerprint~"."] do={
- :local CertVal [ / certificate get $Cert ];
-
- :local ExpiresAfter [ $FormatExpire ($CertVal->"expires-after") ];
- :local State "is about to expire";
- :if (($CertVal->"expired") = true) do={
- :set ExpiresAfter "expired";
- :set State "expired";
- }
-
- $SendNotification ("Certificate warning!") \
- ("A certificate on " . $Identity . " " . $State . ".\n\n" . \
- "Name: " . ($CertVal->"name") . "\n" . \
- "CommonName: " . ($CertVal->"common-name") . "\n" . \
- "Fingerprint: " . ($CertVal->"fingerprint") . "\n" . \
- "Issuer: " . ($CertVal->"ca") . ([ $ParseKeyValueStore ($CertVal->"issuer") ]->"CN") . "\n" . \
- "Validity: " . ($CertVal->"invalid-before") . " to " . ($CertVal->"invalid-after") . "\n" . \
- "Expires in: " . $ExpiresAfter);
- :log warning ("The certificate " . ($CertVal->"name") . " " . $State . \
- ", it is invalid after " . ($CertVal->"invalid-after") . ".");
-}
diff --git a/check-certificates.rsc b/check-certificates.rsc
new file mode 100644
index 0000000..e9235f1
--- /dev/null
+++ b/check-certificates.rsc
@@ -0,0 +1,222 @@
+#!rsc by RouterOS
+# RouterOS script: check-certificates
+# Copyright (c) 2013-2024 Christian Hesse <mail@eworm.de>
+# https://git.eworm.de/cgit/routeros-scripts/about/COPYING.md
+#
+# requires RouterOS, version=7.13
+#
+# check for certificate validity
+# https://git.eworm.de/cgit/routeros-scripts/about/doc/check-certificates.md
+
+:global GlobalFunctionsReady;
+:while ($GlobalFunctionsReady != true) do={ :delay 500ms; }
+
+:do {
+ :local ScriptName [ :jobname ];
+
+ :global CertRenewTime;
+ :global CertRenewUrl;
+ :global CertWarnTime;
+ :global Identity;
+
+ :global CertificateAvailable
+ :global EscapeForRegEx;
+ :global IfThenElse;
+ :global LogPrint;
+ :global ParseKeyValueStore;
+ :global ScriptLock;
+ :global SendNotification2;
+ :global SymbolForNotification;
+ :global UrlEncode;
+ :global WaitFullyConnected;
+
+ :local CheckCertificatesDownloadImport do={
+ :local ScriptName [ :tostr $1 ];
+ :local Name [ :tostr $2 ];
+
+ :global CertRenewUrl;
+ :global CertRenewPass;
+
+ :global CertificateNameByCN;
+ :global EscapeForRegEx;
+ :global FetchUserAgentStr;
+ :global LogPrint;
+ :global UrlEncode;
+ :global WaitForFile;
+
+ :local Return false;
+
+ :foreach Type in={ ".pem"; ".p12" } do={
+ :local CertFileName ([ $UrlEncode $Name ] . $Type);
+ :do {
+ /tool/fetch check-certificate=yes-without-crl http-header-field=({ [ $FetchUserAgentStr $ScriptName ] }) \
+ ($CertRenewUrl . $CertFileName) dst-path=$CertFileName as-value;
+ $WaitForFile $CertFileName;
+
+ :local DecryptionFailed true;
+ :foreach PassPhrase in=$CertRenewPass do={
+ :local Result [ /certificate/import file-name=$CertFileName passphrase=$PassPhrase as-value ];
+ :if ($Result->"decryption-failures" = 0) do={
+ :set DecryptionFailed false;
+ }
+ }
+ /file/remove [ find where name=$CertFileName ];
+
+ :if ($DecryptionFailed = true) do={
+ $LogPrint warning $ScriptName ("Decryption failed for certificate file '" . $CertFileName . "'.");
+ }
+
+ :foreach CertInChain in=[ /certificate/find where name~("^" . [ $EscapeForRegEx $CertFileName ] . "_[0-9]+\$") \
+ common-name!=$Name !(subject-alt-name~("(^|\\W)(DNS|IP):" . [ $EscapeForRegEx $Name ] . "(\\W|\$)")) !(common-name=[]) ] do={
+ $CertificateNameByCN [ /certificate/get $CertInChain common-name ];
+ }
+
+ :set Return true;
+ } on-error={
+ $LogPrint debug $ScriptName ("Could not download certificate file '" . $CertFileName . "'.");
+ }
+ }
+
+ :return $Return;
+ }
+
+ :local FormatInfo do={
+ :local Cert $1;
+
+ :global FormatLine;
+ :global FormatMultiLines;
+ :global IfThenElse;
+
+ :local FormatExpire do={
+ :global CharacterReplace;
+ :return [ $CharacterReplace [ $CharacterReplace [ :tostr $1 ] "w" "w " ] "d" "d " ];
+ }
+
+ :local FormatCertChain do={
+ :local Cert $1;
+
+ :global EitherOr;
+ :global ParseKeyValueStore;
+
+ :local CertVal [ /certificate/get $Cert ];
+
+ :if ([ :typeof ($CertVal->"issuer") ] = "nothing") do={
+ :return "self-signed";
+ }
+
+ :local Return "";
+ :for I from=0 to=5 do={
+ :set Return ($Return . [ $EitherOr ([ $ParseKeyValueStore ($CertVal->"issuer") ]->"CN") \
+ ([ $ParseKeyValueStore (($CertVal->"issuer")->0) ]->"CN") ]);
+ :set CertVal [ /certificate/get [ find where skid=($CertVal->"akid") ] ];
+ :if (($CertVal->"akid") = "" || ($CertVal->"akid") = ($CertVal->"skid")) do={
+ :return $Return;
+ }
+ :set Return ($Return . " -> ");
+ }
+ :return ($Return . "...");
+ }
+
+ :local CertVal [ /certificate/get $Cert ];
+
+ :return ( \
+ [ $FormatLine "Name" ($CertVal->"name") ] . "\n" . \
+ [ $IfThenElse ([ :len ($CertVal->"common-name") ] > 0) ([ $FormatLine "CommonName" ($CertVal->"common-name") ] . "\n") ] . \
+ [ $IfThenElse ([ :len ($CertVal->"subject-alt-name") ] > 0) ([ $FormatMultiLines "SubjectAltNames" ($CertVal->"subject-alt-name") ] . "\n") ] . \
+ [ $FormatLine "Private key" [ $IfThenElse (($CertVal->"private-key") = true) "available" "missing" ] ] . "\n" . \
+ [ $FormatLine "Fingerprint" ($CertVal->"fingerprint") ] . "\n" . \
+ [ $IfThenElse ([ :len ($CertVal->"ca") ] > 0) [ $FormatLine "Issuer" ($CertVal->"ca") ] [ $FormatLine "Issuer chain" [ $FormatCertChain $Cert ] ] ] . "\n" . \
+ "Validity:\n" . \
+ [ $FormatLine " from" ($CertVal->"invalid-before") ] . "\n" . \
+ [ $FormatLine " to" ($CertVal->"invalid-after") ] . "\n" . \
+ [ $FormatLine "Expires in" [ $IfThenElse (($CertVal->"expired") = true) "expired" [ $FormatExpire ($CertVal->"expires-after") ] ] ]);
+ }
+
+ :if ([ $ScriptLock $ScriptName ] = false) do={
+ :error false;
+ }
+ $WaitFullyConnected;
+
+ :foreach Cert in=[ /certificate/find where !revoked !ca !scep-url expires-after<$CertRenewTime ] do={
+ :local CertVal [ /certificate/get $Cert ];
+ :local CertNew;
+ :local LastName;
+
+ :do {
+ :if ([ :len $CertRenewUrl ] = 0) do={
+ $LogPrint info $ScriptName ("No CertRenewUrl given.");
+ :error false;
+ }
+ $LogPrint info $ScriptName ("Attempting to renew certificate '" . ($CertVal->"name") . "'.");
+
+ :local ImportSuccess false;
+ :set LastName ($CertVal->"common-name");
+ :set ImportSuccess [ $CheckCertificatesDownloadImport $ScriptName $LastName ];
+ :foreach SAN in=($CertVal->"subject-alt-name") do={
+ :if ($ImportSuccess = false) do={
+ :set LastName [ :pick $SAN ([ :find $SAN ":" ] + 1) [ :len $SAN ] ];
+ :set ImportSuccess [ $CheckCertificatesDownloadImport $ScriptName $LastName ];
+ }
+ }
+ :if ($ImportSuccess = false) do={ :error false; }
+
+ :if ([ :len ($CertVal->"fingerprint") ] > 0 && $CertVal->"fingerprint" != [ /certificate/get $Cert fingerprint ]) do={
+ $LogPrint debug $ScriptName ("Certificate '" . $CertVal->"name" . "' was updated in place.");
+ :set CertVal [ /certificate/get $Cert ];
+ } else={
+ $LogPrint debug $ScriptName ("Certificate '" . $CertVal->"name" . "' was not updated, but replaced.");
+
+ :set CertNew [ /certificate/find where name~("^" . [ $EscapeForRegEx [ $UrlEncode $LastName ] ] . "\\.(p12|pem)_[0-9]+\$") \
+ (common-name=($CertVal->"common-name") or subject-alt-name~("(^|\\W)(DNS|IP):" . [ $EscapeForRegEx $LastName ] . "(\\W|\$)")) \
+ fingerprint!=[ :tostr ($CertVal->"fingerprint") ] expires-after>$CertRenewTime ];
+ :local CertNewVal [ /certificate/get $CertNew ];
+
+ :if ([ $CertificateAvailable ([ $ParseKeyValueStore ($CertNewVal->"issuer") ]->"CN") ] = false) do={
+ $LogPrint warning $ScriptName ("The certificate chain is not available!");
+ }
+
+ :if (($CertVal->"private-key") = true && ($CertVal->"private-key") != ($CertNewVal->"private-key")) do={
+ /certificate/remove $CertNew;
+ $LogPrint warning $ScriptName ("Old certificate '" . ($CertVal->"name") . "' has a private key, new certificate does not. Aborting renew.");
+ :error false;
+ }
+
+ /ip/service/set certificate=($CertNewVal->"name") [ find where certificate=($CertVal->"name") ];
+
+ /ip/ipsec/identity/set certificate=($CertNewVal->"name") [ find where certificate=($CertVal->"name") ];
+ /ip/ipsec/identity/set remote-certificate=($CertNewVal->"name") [ find where remote-certificate=($CertVal->"name") ];
+
+ /ip/hotspot/profile/set ssl-certificate=($CertNewVal->"name") [ find where ssl-certificate=($CertVal->"name") ];
+
+ /certificate/remove $Cert;
+ /certificate/set $CertNew name=($CertVal->"name");
+ :set CertNewVal;
+ :set CertVal [ /certificate/get $CertNew ];
+ }
+
+ $SendNotification2 ({ origin=$ScriptName; silent=true; \
+ subject=([ $SymbolForNotification "lock-with-ink-pen" ] . "Certificate renewed: " . ($CertVal->"name")); \
+ message=("A certificate on " . $Identity . " has been renewed.\n\n" . [ $FormatInfo $CertNew ]) });
+ $LogPrint info $ScriptName ("The certificate '" . ($CertVal->"name") . "' has been renewed.");
+ } on-error={
+ $LogPrint debug $ScriptName ("Could not renew certificate '" . ($CertVal->"name") . "'.");
+ }
+ }
+
+ :foreach Cert in=[ /certificate/find where !revoked !scep-url !(expires-after=[]) \
+ expires-after<$CertWarnTime !(fingerprint=[]) ] do={
+ :local CertVal [ /certificate/get $Cert ];
+
+ :if ([ :len [ /certificate/scep-server/find where ca-cert=($CertVal->"ca") ] ] > 0) do={
+ $LogPrint debug $ScriptName ("Certificate '" . ($CertVal->"name") . "' is handled by SCEP, skipping.");
+ } else={
+ :local State [ $IfThenElse (($CertVal->"expired") = true) "expired" "is about to expire" ];
+
+ $SendNotification2 ({ origin=$ScriptName; \
+ subject=([ $SymbolForNotification "warning-sign" ] . "Certificate warning: " . ($CertVal->"name")); \
+ message=("A certificate on " . $Identity . " " . $State . ".\n\n" . [ $FormatInfo $Cert ]) });
+ $LogPrint info $ScriptName ("The certificate '" . ($CertVal->"name") . "' " . $State . \
+ ", it is invalid after " . ($CertVal->"invalid-after") . ".");
+ }
+ }
+} on-error={ }
diff --git a/check-health b/check-health
deleted file mode 100644
index 95a92d7..0000000
--- a/check-health
+++ /dev/null
@@ -1,72 +0,0 @@
-#!rsc
-# RouterOS script: check-health
-# Copyright (c) 2019-2020 Christian Hesse <mail@eworm.de>
-#
-# check for RouterOS health state
-
-:global CheckHealthLast;
-:global CheckHealthTemperature;
-:global CheckHealthVoltagePercent;
-:global Identity;
-
-:global SendNotification;
-
-:local FormatVoltage do={
- :local Voltage [ :tonum $1 ];
- :return (($Voltage / 10) . "." . ($Voltage % ($Voltage / 10 * 10)) . "V");
-}
-
-:local CheckHealthCurrent [ / system health get ];
-
-:foreach Voltage in={ "battery"; "psu1-voltage"; "psu2-voltage"; "voltage" } do={
- :if ([ :typeof ($CheckHealthLast->$Voltage) ] = "num" && \
- [ :typeof ($CheckHealthCurrent->$Voltage) ] = "num") do={
- :if ($CheckHealthLast->$Voltage * (100 + $CheckHealthVoltagePercent) / 100 < $CheckHealthCurrent->$Voltage || \
- $CheckHealthLast->$Voltage > $CheckHealthCurrent->$Voltage * (100 + $CheckHealthVoltagePercent) / 100) do={
- $SendNotification ("Health warning: " . $Voltage) \
- ("The " . $Voltage . " on " . $Identity . " jumped more than " . $CheckHealthVoltagePercent . "%.\n\n" . \
- "old value: " . [ $FormatVoltage ($CheckHealthLast->$Voltage) ] . "\n" . \
- "new value: " . [ $FormatVoltage ($CheckHealthCurrent->$Voltage) ]);
- }
- }
-}
-
-:foreach PSU in={ "psu1"; "psu2" } do={
- :if ([ :typeof ($CheckHealthLast->($PSU . "-state")) ] = "str" && \
- [ :typeof ($CheckHealthCurrent->($PSU . "-state")) ] = "str") do={
- :if ($CheckHealthLast->($PSU . "-state") = "ok" && \
- $CheckHealthCurrent->($PSU . "-state") != "ok") do={
- $SendNotification ("Health warning: " . $PSU . " state") \
- ("The power supply unit '" . $PSU . "' on " . $Identity . " failed!");
- }
- :if ($CheckHealthLast->($PSU . "-state") != "ok" && \
- $CheckHealthCurrent->($PSU . "-state") = "ok") do={
- $SendNotification ("Health recovery: " . $PSU . " state") \
- ("The power supply unit '" . $PSU . "' on " . $Identity . " recovered!");
- }
- }
-}
-
-:foreach Temperature in={ "temperature"; "cpu-temperature"; "board-temperature1"; "board-temperature2" } do={
- :if ([ :typeof ($CheckHealthLast->$Temperature) ] = "num" && \
- [ :typeof ($CheckHealthCurrent->$Temperature) ] = "num") do={
- :if ([ :typeof ($CheckHealthTemperature->$Temperature) ] != "num" ) do={
- :log warning ("No threshold given for " . $Temperature . ", assuming 50C.");
- :set ($CheckHealthTemperature->$Temperature) 50;
- }
- :if ($CheckHealthLast->$Temperature <= $CheckHealthTemperature->$Temperature && \
- $CheckHealthCurrent->$Temperature > $CheckHealthTemperature->$Temperature) do={
- $SendNotification ("Health warning: " . $Temperature) \
- ("The " . $Temperature . " on " . $Identity . " is above threshold: " . \
- $CheckHealthCurrent->$Temperature . "C");
- }
- :if ($CheckHealthLast->$Temperature > $CheckHealthTemperature->$Temperature && \
- $CheckHealthCurrent->$Temperature <= $CheckHealthTemperature->$Temperature) do={
- $SendNotification ("Health recovery: " . $Temperature) \
- ("The " . $Temperature . " on " . $Identity . " dropped below threshold: " . \
- $CheckHealthCurrent->$Temperature . "C");
- }
- }
-}
-
-:set CheckHealthLast $CheckHealthCurrent;
diff --git a/check-health.rsc b/check-health.rsc
new file mode 100644
index 0000000..a769fa8
--- /dev/null
+++ b/check-health.rsc
@@ -0,0 +1,178 @@
+#!rsc by RouterOS
+# RouterOS script: check-health
+# Copyright (c) 2019-2024 Christian Hesse <mail@eworm.de>
+# https://git.eworm.de/cgit/routeros-scripts/about/COPYING.md
+#
+# requires RouterOS, version=7.13
+#
+# check for RouterOS health state
+# https://git.eworm.de/cgit/routeros-scripts/about/doc/check-health.md
+
+:global GlobalFunctionsReady;
+:while ($GlobalFunctionsReady != true) do={ :delay 500ms; }
+
+:do {
+ :local ScriptName [ :jobname ];
+
+ :global CheckHealthCPUUtilization;
+ :global CheckHealthCPUUtilizationNotified;
+ :global CheckHealthLast;
+ :global CheckHealthRAMUtilizationNotified;
+ :global CheckHealthTemperature;
+ :global CheckHealthTemperatureDeviation;
+ :global CheckHealthTemperatureNotified;
+ :global CheckHealthVoltageLow;
+ :global CheckHealthVoltagePercent;
+ :global Identity;
+
+ :global FormatLine;
+ :global HumanReadableNum;
+ :global IfThenElse;
+ :global LogPrint;
+ :global ScriptLock;
+ :global SendNotification2;
+ :global SymbolForNotification;
+
+ :local TempToNum do={
+ :global CharacterReplace;
+ :local T [ :toarray [ $CharacterReplace $1 "." "," ] ];
+ :return ($T->0 * 10 + $T->1);
+ }
+
+ :if ([ $ScriptLock $ScriptName ] = false) do={
+ :error false;
+ }
+
+ :local Resource [ /system/resource/get ];
+
+ :set CheckHealthCPUUtilization (($CheckHealthCPUUtilization * 4 + ($Resource->"cpu-load") * 10) / 5);
+ :if ($CheckHealthCPUUtilization > 750 && $CheckHealthCPUUtilizationNotified != true) do={
+ $SendNotification2 ({ origin=$ScriptName; \
+ subject=([ $SymbolForNotification "abacus,chart-increasing" ] . "Health warning: CPU utilization"); \
+ message=("The average CPU utilization on " . $Identity . " is at " . ($CheckHealthCPUUtilization / 10) . "%!") });
+ :set CheckHealthCPUUtilizationNotified true;
+ }
+ :if ($CheckHealthCPUUtilization < 650 && $CheckHealthCPUUtilizationNotified = true) do={
+ $SendNotification2 ({ origin=$ScriptName; \
+ subject=([ $SymbolForNotification "abacus,chart-decreasing" ] . "Health recovery: CPU utilization"); \
+ message=("The average CPU utilization on " . $Identity . " decreased to " . ($CheckHealthCPUUtilization / 10) . "%.") });
+ :set CheckHealthCPUUtilizationNotified false;
+ }
+
+ :local CheckHealthRAMUtilization (($Resource->"total-memory" - $Resource->"free-memory") * 100 / $Resource->"total-memory");
+ :if ($CheckHealthRAMUtilization >=80 && $CheckHealthRAMUtilizationNotified != true) do={
+ $SendNotification2 ({ origin=$ScriptName; \
+ subject=([ $SymbolForNotification "card-file-box,chart-increasing" ] . "Health warning: RAM utilization"); \
+ message=("The RAM utilization on " . $Identity . " is at " . $CheckHealthRAMUtilization . "%!\n\n" . \
+ [ $FormatLine "total" ([ $HumanReadableNum ($Resource->"total-memory") 1024 ] . "B") 8 ] . "\n" . \
+ [ $FormatLine "used" ([ $HumanReadableNum ($Resource->"total-memory" - $Resource->"free-memory") 1024 ] . "B") 8 ] . "\n" . \
+ [ $FormatLine "free" ([ $HumanReadableNum ($Resource->"free-memory") 1024 ] . "B") 8 ]) });
+ :set CheckHealthRAMUtilizationNotified true;
+ }
+ :if ($CheckHealthRAMUtilization < 70 && $CheckHealthRAMUtilizationNotified = true) do={
+ $SendNotification2 ({ origin=$ScriptName; \
+ subject=([ $SymbolForNotification "card-file-box,chart-decreasing" ] . "Health recovery: RAM utilization"); \
+ message=("The RAM utilization on " . $Identity . " decreased to " . $CheckHealthRAMUtilization . "%.") });
+ :set CheckHealthRAMUtilizationNotified false;
+ }
+
+ :if ([ :len [ /system/health/find ] ] = 0) do={
+ $LogPrint debug $ScriptName ("Your device does not provide any health values.");
+ :error true;
+ }
+
+ :if ([ :typeof $CheckHealthLast ] != "array") do={
+ :set CheckHealthLast ({});
+ }
+ :if ([ :typeof $CheckHealthTemperatureNotified ] != "array") do={
+ :set CheckHealthTemperatureNotified ({});
+ }
+
+
+ :foreach Voltage in=[ /system/health/find where type="V" ] do={
+ :local Name [ /system/health/get $Voltage name ];
+ :local Value [ /system/health/get $Voltage value ];
+
+ :if ([ :typeof ($CheckHealthLast->$Name) ] != "nothing") do={
+ :local NumCurr [ $TempToNum $Value ];
+ :local NumLast [ $TempToNum ($CheckHealthLast->$Name) ];
+
+ :if ($NumLast * (100 + $CheckHealthVoltagePercent) < $NumCurr * 100 || \
+ $NumLast * 100 > $NumCurr * (100 + $CheckHealthVoltagePercent)) do={
+ $SendNotification2 ({ origin=$ScriptName; \
+ subject=([ $SymbolForNotification ("high-voltage-sign,chart-" . [ $IfThenElse ($NumLast < \
+ $NumCurr) "in" "de" ] . "creasing") ] . "Health warning: " . $Name); \
+ message=("The " . $Name . " on " . $Identity . " jumped more than " . $CheckHealthVoltagePercent . "%.\n\n" . \
+ [ $FormatLine "old value" ($CheckHealthLast->$Name . " V") 12 ] . "\n" . \
+ [ $FormatLine "new value" ($Value . " V") 12 ]) });
+ } else={
+ :if ($NumCurr <= $CheckHealthVoltageLow && $NumLast > $CheckHealthVoltageLow) do={
+ $SendNotification2 ({ origin=$ScriptName; \
+ subject=([ $SymbolForNotification "high-voltage-sign,chart-decreasing" ] . "Health warning: Low " . $Name); \
+ message=("The " . $Name . " on " . $Identity . " dropped to " . $Value . " V below hard limit.") });
+ }
+ :if ($NumCurr > $CheckHealthVoltageLow && $NumLast <= $CheckHealthVoltageLow) do={
+ $SendNotification2 ({ origin=$ScriptName; \
+ subject=([ $SymbolForNotification "high-voltage-sign,chart-increasing" ] . "Health recovery: Low " . $Name); \
+ message=("The " . $Name . " on " . $Identity . " recovered to " . $Value . " V above hard limit.") });
+ }
+ }
+ }
+ :set ($CheckHealthLast->$Name) $Value;
+ }
+
+ :foreach PSU in=[ /system/health/find where name~"^psu.*-state\$" ] do={
+ :local Name [ /system/health/get $PSU name ];
+ :local Value [ /system/health/get $PSU value ];
+
+ :if ([ :typeof ($CheckHealthLast->$Name) ] != "nothing") do={
+ :if ($CheckHealthLast->$Name = "ok" && \
+ $Value != "ok") do={
+ $SendNotification2 ({ origin=$ScriptName; \
+ subject=([ $SymbolForNotification "cross-mark" ] . "Health warning: " . $Name); \
+ message=("The power supply unit '" . $Name . "' on " . $Identity . " failed!") });
+ }
+ :if ($CheckHealthLast->$Name != "ok" && \
+ $Value = "ok") do={
+ $SendNotification2 ({ origin=$ScriptName; \
+ subject=([ $SymbolForNotification "white-heavy-check-mark" ] . "Health recovery: " . $Name); \
+ message=("The power supply unit '" . $Name . "' on " . $Identity . " recovered!") });
+ }
+ }
+ :set ($CheckHealthLast->$Name) $Value;
+ }
+
+ :foreach Temperature in=[ /system/health/find where type="C" ] do={
+ :local Name [ /system/health/get $Temperature name ];
+ :local Value [ /system/health/get $Temperature value ];
+
+ :if ([ :typeof ($CheckHealthLast->$Name) ] != "nothing") do={
+ :if ([ :typeof ($CheckHealthTemperature->$Name) ] != "num" ) do={
+ $LogPrint info $ScriptName ("No threshold given for " . $Name . ", assuming 50C.");
+ :set ($CheckHealthTemperature->$Name) 50;
+ }
+ :local Validate [ /system/health/get [ find where name=$Name ] value ];
+ :while ($Value != $Validate) do={
+ :set Value $Validate;
+ :set Validate [ /system/health/get [ find where name=$Name ] value ];
+ }
+ :if ($Value > $CheckHealthTemperature->$Name && \
+ $CheckHealthTemperatureNotified->$Name != true) do={
+ $SendNotification2 ({ origin=$ScriptName; \
+ subject=([ $SymbolForNotification "fire" ] . "Health warning: " . $Name); \
+ message=("The " . $Name . " on " . $Identity . " is above threshold: " . \
+ $Value . "\C2\B0" . "C") });
+ :set ($CheckHealthTemperatureNotified->$Name) true;
+ }
+ :if ($Value <= ($CheckHealthTemperature->$Name - $CheckHealthTemperatureDeviation) && \
+ $CheckHealthTemperatureNotified->$Name = true) do={
+ $SendNotification2 ({ origin=$ScriptName; \
+ subject=([ $SymbolForNotification "white-heavy-check-mark" ] . "Health recovery: " . $Name); \
+ message=("The " . $Name . " on " . $Identity . " dropped below threshold: " . \
+ $Value . "\C2\B0" . "C") });
+ :set ($CheckHealthTemperatureNotified->$Name) false;
+ }
+ }
+ :set ($CheckHealthLast->$Name) $Value;
+ }
+} on-error={ }
diff --git a/check-lte-firmware-upgrade b/check-lte-firmware-upgrade
deleted file mode 100644
index 101f5e1..0000000
--- a/check-lte-firmware-upgrade
+++ /dev/null
@@ -1,32 +0,0 @@
-#!rsc
-# RouterOS script: check-lte-firmware-upgrade
-# Copyright (c) 2018-2020 Christian Hesse <mail@eworm.de>
-#
-# check for LTE firmware upgrade, send notification
-
-:global Identity;
-:global SentLteFirmwareUpgradeNotification;
-
-:global SendNotification;
-
-:foreach Interface in=[ / interface lte find ] do={
- :local IntName [ / interface lte get $Interface name ];
- :do {
- :local Firmware [ / interface lte firmware-upgrade $Interface once as-value ];
-
- :if ($SentLteFirmwareUpgradeNotification = ($Firmware->"latest")) do={
- :log debug ("Already sent the LTE firmware upgrade notification for version " . \
- ($Firmware->"latest") . ".");
- } else={
- :if (($Firmware->"installed") != ($Firmware->"latest")) do={
- $SendNotification ("LTE firmware upgrade") \
- ("A new firmware version " . ($Firmware->"latest") . " is available for " . \
- "LTE interface " . $IntName . " on " . $Identity . ".") "" "true";
- :set SentLteFirmwareUpgradeNotification ($Firmware->"latest");
- }
- }
- } on-error={
- :log debug ("Could not get latest LTE firmware version for interface " . \
- $IntName . ".");
- }
-}
diff --git a/check-lte-firmware-upgrade.rsc b/check-lte-firmware-upgrade.rsc
new file mode 100644
index 0000000..3a25f83
--- /dev/null
+++ b/check-lte-firmware-upgrade.rsc
@@ -0,0 +1,103 @@
+#!rsc by RouterOS
+# RouterOS script: check-lte-firmware-upgrade
+# Copyright (c) 2018-2024 Christian Hesse <mail@eworm.de>
+# https://git.eworm.de/cgit/routeros-scripts/about/COPYING.md
+#
+# requires RouterOS, version=7.13
+#
+# check for LTE firmware upgrade, send notification
+# https://git.eworm.de/cgit/routeros-scripts/about/doc/check-lte-firmware-upgrade.md
+
+:global GlobalFunctionsReady;
+:while ($GlobalFunctionsReady != true) do={ :delay 500ms; }
+
+:do {
+ :local ScriptName [ :jobname ];
+
+ :global SentLteFirmwareUpgradeNotification;
+
+ :global ScriptLock;
+
+ :if ([ $ScriptLock $ScriptName ] = false) do={
+ :error false;
+ }
+
+ :if ([ :typeof $SentLteFirmwareUpgradeNotification ] != "array") do={
+ :global SentLteFirmwareUpgradeNotification ({});
+ }
+
+ :local CheckInterface do={
+ :local ScriptName $1;
+ :local Interface $2;
+
+ :global Identity;
+ :global SentLteFirmwareUpgradeNotification;
+
+ :global FormatLine;
+ :global IfThenElse;
+ :global LogPrint;
+ :global ScriptFromTerminal;
+ :global SendNotification2;
+ :global SymbolForNotification;
+
+ :local IntName [ /interface/lte/get $Interface name ];
+ :local Firmware;
+ :local Info;
+ :do {
+ :set Firmware [ /interface/lte/firmware-upgrade $Interface once as-value ];
+ :set Info [ /interface/lte/monitor $Interface once as-value ];
+ } on-error={
+ $LogPrint debug $ScriptName ("Could not get latest LTE firmware version for interface " . \
+ $IntName . ".");
+ :return false;
+ }
+
+ :if ([ :len ($Firmware->"latest") ] = 0) do={
+ $LogPrint info $ScriptName ("An empty string is not a valid version.");
+ :return false;
+ }
+
+ :if (($Firmware->"installed") = ($Firmware->"latest")) do={
+ :if ([ $ScriptFromTerminal $ScriptName ] = true) do={
+ $LogPrint info $ScriptName ("No firmware upgrade available for LTE interface " . $IntName . ".");
+ }
+ :return true;
+ }
+
+ :if ([ $ScriptFromTerminal $ScriptName ] = true && \
+ [ :len [ /system/script/find where name="unattended-lte-firmware-upgrade" ] ] > 0) do={
+ :put ("Do you want to start unattended lte firmware upgrade for interface " . $IntName . "? [y/N]");
+ :if (([ /terminal/inkey timeout=60 ] % 32) = 25) do={
+ /system/script/run unattended-lte-firmware-upgrade;
+ $LogPrint info $ScriptName ("Scheduled lte firmware upgrade for interface " . $IntName . "...");
+ :return true;
+ } else={
+ :put "Canceled...";
+ }
+ }
+
+ :if (($SentLteFirmwareUpgradeNotification->$IntName) = ($Firmware->"latest")) do={
+ $LogPrint debug $ScriptName ("Already sent the LTE firmware upgrade notification for version " . \
+ ($Firmware->"latest") . ".");
+ :return false;
+ }
+
+ $LogPrint info $ScriptName ("A new firmware version " . ($Firmware->"latest") . " is available for " . \
+ "LTE interface " . $IntName . ".");
+ $SendNotification2 ({ origin=$ScriptName; \
+ subject=([ $SymbolForNotification "sparkles" ] . "LTE firmware upgrade"); \
+ message=("A new firmware version " . ($Firmware->"latest") . " is available for " . \
+ "LTE interface " . $IntName . " on " . $Identity . ".\n\n" . \
+ [ $IfThenElse ([ :len ($Info->"manufacturer") ] > 0) ([ $FormatLine "Manufacturer" ($Info->"manufacturer") ] . "\n") ] . \
+ [ $IfThenElse ([ :len ($Info->"model") ] > 0) ([ $FormatLine "Model" ($Info->"model") ] . "\n") ] . \
+ [ $IfThenElse ([ :len ($Info->"revision") ] > 0) ([ $FormatLine "Revision" ($Info->"revision") ] . "\n") ] . \
+ "Firmware version:\n" . \
+ [ $FormatLine " Installed" ($Firmware->"installed") ] . "\n" . \
+ [ $FormatLine " Available" ($Firmware->"latest") ]); silent=true });
+ :set ($SentLteFirmwareUpgradeNotification->$IntName) ($Firmware->"latest");
+ }
+
+ :foreach Interface in=[ /interface/lte/find ] do={
+ $CheckInterface $ScriptName $Interface;
+ }
+} on-error={ }
diff --git a/check-routeros-update b/check-routeros-update
deleted file mode 100644
index 1791b23..0000000
--- a/check-routeros-update
+++ /dev/null
@@ -1,84 +0,0 @@
-#!rsc
-# RouterOS script: check-routeros-update
-# Copyright (c) 2013-2020 Christian Hesse <mail@eworm.de>
-#
-# check for RouterOS update, send notification and/or install
-
-:global Identity;
-:global SafeUpdateUrl;
-:global SentRouterosUpdateNotification;
-
-:global DeviceInfo;
-:global ScriptFromTerminal;
-:global SendNotification;
-
-:local DoUpdate do={
- :if ([ / system script print count-only where name="packages-update" ] > 0) do={
- / system script run packages-update;
- } else={
- / system package update install without-paging;
- }
- :error "Waiting for system to reboot.";
-}
-
-:if ([ / system package print count-only where name="wireless" disabled=no ] > 0) do={
- :if ([ / interface wireless cap get enabled ] = true && \
- [ / caps-man manager get enabled ] = false) do={
- :log warning "System is managed by CAPsMAN, not checking.";
- :error "Warning: See log for details.";
- }
-}
-
-:if ([ / system scheduler print count-only where name="reboot-for-update" ] > 0) do={
- :error "A reboot for update is already scheduled.";
-}
-
-/ system package update check-for-updates without-paging;
-:local Update [ / system package update get ];
-
-:if ([ :len ($Update->"latest-version") ] = 0) do={
- :log warning "An empty string is not a valid version.";
- :error "Warning: See log for details.";
-}
-
-:if ($Update->"installed-version" != $Update->"latest-version") do={
- :if ([ :len $SafeUpdateUrl ] > 0) do={
- :local Result;
- :do {
- :set Result [ / tool fetch check-certificate=yes-without-crl \
- ($SafeUpdateUrl . $Update->"channel" . "?installed=" . $Update->"installed-version" . \
- "&latest=" . $Update->"latest-version") output=user as-value ];
- } on-error={
- :log warning ("Failed receiving safe version for " . $Update->"channel" . ".");
- }
- :if ($Result->"status" = "finished" && $Result->"data" = $Update->"latest-version") do={
- :log info ("Version " . $Update->"latest-version" . " is considered safe, updating...");
- $SendNotification ("RouterOS update") \
- ("Version " . $Update->"latest-version" . " is considered safe for " . $Update->"channel" . \
- ", updating on " . $Identity . "...") "" "true";
- $DoUpdate;
- }
- }
-
- :if ([ $ScriptFromTerminal "check-routeros-update" ] = true) do={
- :put ("Do you want to install RouterOS version " . $Update->"latest-version" . "? [y/N]");
- :if ([ :terminal inkey timeout=60 ] = 121) do={
- $DoUpdate;
- } else={
- :put "Canceled...";
- }
- }
-
- :if ($SentRouterosUpdateNotification = $Update->"latest-version") do={
- :log info ("Already sent the RouterOS update notification for version " . \
- $Update->"latest-version" . ".");
- :error "Already sent notification.";
- }
-
- $SendNotification ("RouterOS update") \
- ("There is a RouterOS update available.\n\n" . \
- [ $DeviceInfo ] . "\n\n" . \
- "https://mikrotik.com/download/changelogs/" . $Update->"channel" . "-release-tree") \
- "" "true";
- :set SentRouterosUpdateNotification ($Update->"latest-version");
-}
diff --git a/check-routeros-update.rsc b/check-routeros-update.rsc
new file mode 100644
index 0000000..6dca99a
--- /dev/null
+++ b/check-routeros-update.rsc
@@ -0,0 +1,166 @@
+#!rsc by RouterOS
+# RouterOS script: check-routeros-update
+# Copyright (c) 2013-2024 Christian Hesse <mail@eworm.de>
+# https://git.eworm.de/cgit/routeros-scripts/about/COPYING.md
+#
+# requires RouterOS, version=7.13
+#
+# check for RouterOS update, send notification and/or install
+# https://git.eworm.de/cgit/routeros-scripts/about/doc/check-routeros-update.md
+
+:global GlobalFunctionsReady;
+:while ($GlobalFunctionsReady != true) do={ :delay 500ms; }
+
+:do {
+ :local ScriptName [ :jobname ];
+
+ :global Identity;
+ :global SafeUpdateAll;
+ :global SafeUpdateNeighbor;
+ :global SafeUpdateNeighborIdentity;
+ :global SafeUpdatePatch;
+ :global SafeUpdateUrl;
+ :global SentRouterosUpdateNotification;
+
+ :global DeviceInfo;
+ :global EscapeForRegEx;
+ :global FetchUserAgentStr;
+ :global LogPrint;
+ :global ScriptFromTerminal;
+ :global ScriptLock;
+ :global SendNotification2;
+ :global SymbolForNotification;
+ :global VersionToNum;
+ :global WaitFullyConnected;
+
+ :local DoUpdate do={
+ :if ([ :len [ /system/script/find where name="packages-update" ] ] > 0) do={
+ /system/script/run packages-update;
+ } else={
+ /system/package/update/install without-paging;
+ }
+ :error "Waiting for system to reboot.";
+ }
+
+ :if ([ $ScriptLock $ScriptName ] = false) do={
+ :error false;
+ }
+ $WaitFullyConnected;
+
+ :if ([ :len [ /system/scheduler/find where name="_RebootForUpdate" ] ] > 0) do={
+ :error "A reboot for update is already scheduled.";
+ }
+
+ $LogPrint debug $ScriptName ("Checking for updates...");
+ /system/package/update/check-for-updates without-paging as-value;
+ :local Update [ /system/package/update/get ];
+
+ :if ([ $ScriptFromTerminal $ScriptName ] = true && ($Update->"installed-version") = ($Update->"latest-version")) do={
+ $LogPrint info $ScriptName ("System is already up to date.");
+ :error true;
+ }
+
+ :local NumInstalled [ $VersionToNum ($Update->"installed-version") ];
+ :local NumLatest [ $VersionToNum ($Update->"latest-version") ];
+ :local Link ("https://mikrotik.com/download/changelogs/" . $Update->"channel" . "-release-tree");
+
+ :if ($NumLatest < 117505792) do={
+ $LogPrint info $ScriptName ("The version '" . ($Update->"latest-version") . "' is not a valid version.");
+ :error false;
+ }
+
+ :if ($NumInstalled < $NumLatest) do={
+ :if ($SafeUpdateAll ~ "^YES,? ?PLEASE!?\$") do={
+ $LogPrint info $ScriptName ("Installing ALL versions automatically, including " . \
+ $Update->"latest-version" . "...");
+ $SendNotification2 ({ origin=$ScriptName; \
+ subject=([ $SymbolForNotification "sparkles" ] . "RouterOS update: " . $Update->"latest-version"); \
+ message=("Installing ALL versions automatically, including " . $Update->"latest-version" . \
+ "... Updating on " . $Identity . "..."); link=$Link; silent=true });
+ $DoUpdate;
+ }
+
+ :if ($SafeUpdatePatch = true && ($NumInstalled & 0xffff0000) = ($NumLatest & 0xffff0000)) do={
+ $LogPrint info $ScriptName ("Version " . $Update->"latest-version" . " is a patch release, updating...");
+ $SendNotification2 ({ origin=$ScriptName; \
+ subject=([ $SymbolForNotification "sparkles" ] . "RouterOS update: " . $Update->"latest-version"); \
+ message=("Version " . $Update->"latest-version" . " is a patch update for " . $Update->"channel" . \
+ ", updating on " . $Identity . "..."); link=$Link; silent=true });
+ $DoUpdate;
+ }
+
+ :if ($SafeUpdateNeighbor = true) do={
+ :local Neighbors [ /ip/neighbor/find where platform="MikroTik" identity~$SafeUpdateNeighborIdentity \
+ version~("^" . [ $EscapeForRegEx ($Update->"latest-version") ] . "\\b") ];
+ :if ([ :len $Neighbors ] > 0) do={
+ :local Neighbor [ /ip/neighbor/get ($Neighbors->0) identity ];
+ $LogPrint info $ScriptName ("Seen a neighbor (" . $Neighbor . ") running version " . \
+ $Update->"latest-version" . " from " . $Update->"channel" . ", updating...");
+ $SendNotification2 ({ origin=$ScriptName; \
+ subject=([ $SymbolForNotification "sparkles" ] . "RouterOS update: " . $Update->"latest-version"); \
+ message=("Seen a neighbor (" . $Neighbor . ") running version " . $Update->"latest-version" . \
+ " from " . $Update->"channel" . ", updating on " . $Identity . "..."); link=$Link; silent=true });
+ $DoUpdate;
+ }
+ }
+
+ :if ([ :len $SafeUpdateUrl ] > 0) do={
+ :local Result;
+ :do {
+ :set Result [ /tool/fetch check-certificate=yes-without-crl \
+ ($SafeUpdateUrl . $Update->"channel" . "?installed=" . $Update->"installed-version" . \
+ "&latest=" . $Update->"latest-version") http-header-field=({ [ $FetchUserAgentStr $ScriptName ] }) \
+ output=user as-value ];
+ } on-error={
+ $LogPrint warning $ScriptName ("Failed receiving safe version for " . $Update->"channel" . ".");
+ }
+ :if ($Result->"status" = "finished" && $Result->"data" = $Update->"latest-version") do={
+ $LogPrint info $ScriptName ("Version " . $Update->"latest-version" . " is considered safe, updating...");
+ $SendNotification2 ({ origin=$ScriptName; \
+ subject=([ $SymbolForNotification "sparkles" ] . "RouterOS update: " . $Update->"latest-version"); \
+ message=("Version " . $Update->"latest-version" . " is considered safe for " . $Update->"channel" . \
+ ", updating on " . $Identity . "..."); link=$Link; silent=true });
+ $DoUpdate;
+ }
+ }
+
+ :if ([ $ScriptFromTerminal $ScriptName ] = true) do={
+ :put ("Do you want to install RouterOS version " . $Update->"latest-version" . "? [y/N]");
+ :if (([ /terminal/inkey timeout=60 ] % 32) = 25) do={
+ $DoUpdate;
+ } else={
+ :put "Canceled...";
+ }
+ }
+
+ :if ($SentRouterosUpdateNotification = $Update->"latest-version") do={
+ $LogPrint info $ScriptName ("Already sent the RouterOS update notification for version " . \
+ $Update->"latest-version" . ".");
+ :error true;
+ }
+
+ $SendNotification2 ({ origin=$ScriptName; \
+ subject=([ $SymbolForNotification "sparkles" ] . "RouterOS update: " . $Update->"latest-version"); \
+ message=("A new RouterOS version " . ($Update->"latest-version") . \
+ " is available for " . $Identity . ".\n\n" . \
+ [ $DeviceInfo ]); link=$Link; silent=true });
+ :set SentRouterosUpdateNotification ($Update->"latest-version");
+ }
+
+ :if ($NumInstalled > $NumLatest) do={
+ :if ($SentRouterosUpdateNotification = $Update->"latest-version") do={
+ $LogPrint info $ScriptName ("Already sent the RouterOS downgrade notification for version " . \
+ $Update->"latest-version" . ".");
+ :error true;
+ }
+
+ $SendNotification2 ({ origin=$ScriptName; \
+ subject=([ $SymbolForNotification "warning-sign" ] . "RouterOS version: " . $Update->"latest-version"); \
+ message=("A different RouterOS version " . ($Update->"latest-version") . \
+ " is available for " . $Identity . ", but it is a downgrade.\n\n" . \
+ [ $DeviceInfo ]); link=$Link; silent=true });
+ $LogPrint info $ScriptName ("A different RouterOS version " . ($Update->"latest-version") . \
+ " is available for downgrade.");
+ :set SentRouterosUpdateNotification ($Update->"latest-version");
+ }
+} on-error={ }
diff --git a/cloud-backup b/cloud-backup
deleted file mode 100644
index 5875cd1..0000000
--- a/cloud-backup
+++ /dev/null
@@ -1,34 +0,0 @@
-#!rsc
-# RouterOS script: cloud-backup
-# Copyright (c) 2013-2020 Christian Hesse <mail@eworm.de>
-#
-# upload backup to MikroTik cloud
-
-:global Identity;
-:global BackupPassword;
-
-:global DeviceInfo;
-:global SendNotification;
-
-:do {
- # we are not interested in output, but print without count-only is
- # required to fetch information from cloud
- / system backup cloud print as-value;
- :if ([ / system backup cloud print count-only ] > 0) do={
- / system backup cloud upload-file action=create-and-upload \
- password=$BackupPassword replace=[ get ([ find ]->0) name ];
- } else={
- / system backup cloud upload-file action=create-and-upload \
- password=$BackupPassword;
- }
- :local Cloud [ / system backup cloud get ([ find ]->0) ];
-
- $SendNotification "Cloud backup" \
- ("Uploaded backup for " . $Identity . " to cloud.\n\n" . \
- [ $DeviceInfo ] . "\n\n" . \
- "Name: " . $Cloud->"name" . "\n" . \
- "Size: " . $Cloud->"size" . "\n" . \
- "Download key: " . $Cloud->"secret-download-key") "" "true";
-} on-error={
- :log error ("Failed uploading backup for " . $Identity . " to cloud.");
-}
diff --git a/collect-wireless-mac.capsman b/collect-wireless-mac.capsman
deleted file mode 100644
index 2b9b23f..0000000
--- a/collect-wireless-mac.capsman
+++ /dev/null
@@ -1,66 +0,0 @@
-#!rsc
-# RouterOS script: collect-wireless-mac.capsman
-# Copyright (c) 2013-2020 Christian Hesse <mail@eworm.de>
-#
-# collect wireless mac adresses in access list
-#
-# !! Do not edit this file, it is generated from template!
-
-:global Identity;
-
-:global GetMacVendor;
-:global SendNotification;
-:global ScriptLock;
-
-$ScriptLock "collect-wireless-mac.capsman";
-
-:if ([ / caps-man access-list print count-only where comment="--- collected above ---" disabled ] = 0) do={
- / caps-man access-list add comment="--- collected above ---" disabled=yes;
- :log warn "Added disabled access-list entry with comment '--- collected above ---'.";
-}
-:local PlaceBefore [ / caps-man access-list find where comment="--- collected above ---" disabled ];
-
-:foreach RegTbl in=[ / caps-man registration-table find ] do={
- :local Mac [ / caps-man registration-table get $RegTbl mac-address ];
- :local AccessList ([ / caps-man access-list find where mac-address=$Mac ]->0);
- :if ([ :len $AccessList ] = 0) do={
- :local Address "no dhcp lease";
- :local DnsName "no dhcp lease";
- :local HostName "no dhcp lease";
- :local Lease [ / ip dhcp-server lease find where mac-address=$Mac dynamic=yes status=bound ];
- :if ([ :len $Lease ] > 0) do={
- :set Address [ / ip dhcp-server lease get $Lease address ];
- :set HostName [ / ip dhcp-server lease get $Lease host-name ];
- :if ([ :len $HostName ] = 0) do={
- :set HostName "no hostname";
- }
- :set DnsName [ / ip dns static get ([ find where address=$Address ]->0) name ];
- :if ([ :len $DnsName ] = 0) do={
- :set DnsName "no dns name";
- }
- }
- :local RegEntry [ / caps-man registration-table find where mac-address=$Mac ];
- :local Interface [ / caps-man registration-table get $RegEntry interface ];
- :local Ssid [ / caps-man registration-table get $RegEntry ssid ];
- :local DateTime ([ / system clock get date ] . " " . [ / system clock get time ]);
- :local Vendor [ $GetMacVendor $Mac ];
- :local Message ("unknown MAC address " . $Mac . " (" . $Vendor . ", " . $HostName . ") " . \
- "first seen on " . $DateTime . " connected to SSID " . $Ssid . ", interface " . $Interface);
- / log info $Message;
- / caps-man access-list add place-before=$PlaceBefore comment=$Message mac-address=$Mac disabled=yes;
- $SendNotification ($Mac . " connected to " . $Ssid) \
- ("A device with unknown MAC address connected to " . $Ssid . " on " . $Identity . ".\n\n" . \
- "Controller: " . $Identity . "\n" . \
- "Interface: " . $Interface . "\n" . \
- "SSID: " . $Ssid . "\n" . \
- "MAC: " . $Mac . "\n" . \
- "Vendor: " . $Vendor . "\n" . \
- "Hostname: " . $HostName . "\n" . \
- "Address: " . $Address . "\n" . \
- "DNS name: " . $DnsName . "\n" . \
- "Date: " . $DateTime);
- } else={
- :local Comment [ / caps-man access-list get $AccessList comment ];
- :log debug ("MAC address " . $Mac . " already known: " . $Comment);
- }
-}
diff --git a/collect-wireless-mac.capsman.rsc b/collect-wireless-mac.capsman.rsc
new file mode 100644
index 0000000..74c0754
--- /dev/null
+++ b/collect-wireless-mac.capsman.rsc
@@ -0,0 +1,96 @@
+#!rsc by RouterOS
+# RouterOS script: collect-wireless-mac.capsman
+# Copyright (c) 2013-2024 Christian Hesse <mail@eworm.de>
+# https://git.eworm.de/cgit/routeros-scripts/about/COPYING.md
+#
+# provides: lease-script, order=40
+# requires RouterOS, version=7.13
+#
+# collect wireless mac adresses in access list
+# https://git.eworm.de/cgit/routeros-scripts/about/doc/collect-wireless-mac.md
+#
+# !! Do not edit this file, it is generated from template!
+
+:global GlobalFunctionsReady;
+:while ($GlobalFunctionsReady != true) do={ :delay 500ms; }
+
+:do {
+ :local ScriptName [ :jobname ];
+
+ :global Identity;
+
+ :global EitherOr;
+ :global FormatLine;
+ :global FormatMultiLines;
+ :global GetMacVendor;
+ :global LogPrint;
+ :global ScriptLock;
+ :global SendNotification2;
+ :global SymbolForNotification;
+
+ :if ([ $ScriptLock $ScriptName 10 ] = false) do={
+ :error false;
+ }
+
+ :if ([ :len [ /caps-man/access-list/find where comment="--- collected above ---" disabled ] ] = 0) do={
+ /caps-man/access-list/add comment="--- collected above ---" disabled=yes;
+ $LogPrint warning $ScriptName ("Added disabled access-list entry with comment '--- collected above ---'.");
+ }
+ :local PlaceBefore ([ /caps-man/access-list/find where comment="--- collected above ---" disabled ]->0);
+
+ :foreach Reg in=[ /caps-man/registration-table/find ] do={
+ :local RegVal;
+ :do {
+ :set RegVal [ /caps-man/registration-table/get $Reg ];
+ } on-error={
+ $LogPrint debug $ScriptName ("Device already gone... Ignoring.");
+ }
+
+ :if ([ :len ($RegVal->"mac-address") ] > 0) do={
+ :local AccessList ([ /caps-man/access-list/find where mac-address=($RegVal->"mac-address") ]->0);
+ :if ([ :len $AccessList ] > 0) do={
+ $LogPrint debug $ScriptName ("MAC address " . $RegVal->"mac-address" . " already known: " . \
+ [ /caps-man/access-list/get $AccessList comment ]);
+ }
+
+ :if ([ :len $AccessList ] = 0) do={
+ :local Address "no dhcp lease";
+ :local DnsName "no dhcp lease";
+ :local HostName "no dhcp lease";
+ :local Lease ([ /ip/dhcp-server/lease/find where active-mac-address=($RegVal->"mac-address") dynamic=yes status=bound ]->0);
+ :if ([ :len $Lease ] > 0) do={
+ :set Address [ /ip/dhcp-server/lease/get $Lease active-address ];
+ :set HostName [ $EitherOr [ /ip/dhcp-server/lease/get $Lease host-name ] "no hostname" ];
+ :set DnsName "no dns name";
+ :local DnsRec ([ /ip/dns/static/find where address=$Address ]->0);
+ :if ([ :len $DnsRec ] > 0) do={
+ :set DnsName ({ [ /ip/dns/static/get $DnsRec name ] });
+ :foreach CName in=[ /ip/dns/static/find where type=CNAME cname=($DnsName->0) ] do={
+ :set DnsName ($DnsName, [ /ip/dns/static/get $CName name ]);
+ }
+ }
+ }
+ :local DateTime ([ /system/clock/get date ] . " " . [ /system/clock/get time ]);
+ :local Vendor [ $GetMacVendor ($RegVal->"mac-address") ];
+ :local Message ("MAC address " . $RegVal->"mac-address" . " (" . $Vendor . ", " . $HostName . ") " . \
+ "first seen on " . $DateTime . " connected to SSID " . $RegVal->"ssid" . ", interface " . $RegVal->"interface");
+ $LogPrint info $ScriptName $Message;
+ /caps-man/access-list/add place-before=$PlaceBefore comment=$Message mac-address=($RegVal->"mac-address") disabled=yes;
+ $SendNotification2 ({ origin=$ScriptName; \
+ subject=([ $SymbolForNotification "mobile-phone" ] . $RegVal->"mac-address" . " connected to " . $RegVal->"ssid"); \
+ message=("A device with unknown MAC address connected to " . $RegVal->"ssid" . " on " . $Identity . ".\n\n" . \
+ [ $FormatLine "Controller" $Identity ] . "\n" . \
+ [ $FormatLine "Interface" ($RegVal->"interface") ] . "\n" . \
+ [ $FormatLine "SSID" ($RegVal->"ssid") ] . "\n" . \
+ [ $FormatLine "MAC" ($RegVal->"mac-address") ] . "\n" . \
+ [ $FormatLine "Vendor" $Vendor ] . "\n" . \
+ [ $FormatLine "Hostname" $HostName ] . "\n" . \
+ [ $FormatLine "Address" $Address ] . "\n" . \
+ [ $FormatMultiLines "DNS name" $DnsName ] . "\n" . \
+ [ $FormatLine "Date" $DateTime ]) });
+ }
+ } else={
+ $LogPrint debug $ScriptName ("No mac address available... Ignoring.");
+ }
+ }
+} on-error={ }
diff --git a/collect-wireless-mac.local b/collect-wireless-mac.local
deleted file mode 100644
index 3241c27..0000000
--- a/collect-wireless-mac.local
+++ /dev/null
@@ -1,66 +0,0 @@
-#!rsc
-# RouterOS script: collect-wireless-mac.local
-# Copyright (c) 2013-2020 Christian Hesse <mail@eworm.de>
-#
-# collect wireless mac adresses in access list
-#
-# !! Do not edit this file, it is generated from template!
-
-:global Identity;
-
-:global GetMacVendor;
-:global SendNotification;
-:global ScriptLock;
-
-$ScriptLock "collect-wireless-mac.local";
-
-:if ([ / interface wireless access-list print count-only where comment="--- collected above ---" disabled ] = 0) do={
- / interface wireless access-list add comment="--- collected above ---" disabled=yes;
- :log warn "Added disabled access-list entry with comment '--- collected above ---'.";
-}
-:local PlaceBefore [ / interface wireless access-list find where comment="--- collected above ---" disabled ];
-
-:foreach RegTbl in=[ / interface wireless registration-table find ] do={
- :local Mac [ / interface wireless registration-table get $RegTbl mac-address ];
- :local AccessList ([ / interface wireless access-list find where mac-address=$Mac ]->0);
- :if ([ :len $AccessList ] = 0) do={
- :local Address "no dhcp lease";
- :local DnsName "no dhcp lease";
- :local HostName "no dhcp lease";
- :local Lease [ / ip dhcp-server lease find where mac-address=$Mac dynamic=yes status=bound ];
- :if ([ :len $Lease ] > 0) do={
- :set Address [ / ip dhcp-server lease get $Lease address ];
- :set HostName [ / ip dhcp-server lease get $Lease host-name ];
- :if ([ :len $HostName ] = 0) do={
- :set HostName "no hostname";
- }
- :set DnsName [ / ip dns static get ([ find where address=$Address ]->0) name ];
- :if ([ :len $DnsName ] = 0) do={
- :set DnsName "no dns name";
- }
- }
- :local RegEntry [ / interface wireless registration-table find where mac-address=$Mac ];
- :local Interface [ / interface wireless registration-table get $RegEntry interface ];
- :local Ssid [ / interface wireless get [ find where name=$Interface ] ssid ];
- :local DateTime ([ / system clock get date ] . " " . [ / system clock get time ]);
- :local Vendor [ $GetMacVendor $Mac ];
- :local Message ("unknown MAC address " . $Mac . " (" . $Vendor . ", " . $HostName . ") " . \
- "first seen on " . $DateTime . " connected to SSID " . $Ssid . ", interface " . $Interface);
- / log info $Message;
- / interface wireless access-list add place-before=$PlaceBefore comment=$Message mac-address=$Mac disabled=yes;
- $SendNotification ($Mac . " connected to " . $Ssid) \
- ("A device with unknown MAC address connected to " . $Ssid . " on " . $Identity . ".\n\n" . \
- "Controller: " . $Identity . "\n" . \
- "Interface: " . $Interface . "\n" . \
- "SSID: " . $Ssid . "\n" . \
- "MAC: " . $Mac . "\n" . \
- "Vendor: " . $Vendor . "\n" . \
- "Hostname: " . $HostName . "\n" . \
- "Address: " . $Address . "\n" . \
- "DNS name: " . $DnsName . "\n" . \
- "Date: " . $DateTime);
- } else={
- :local Comment [ / interface wireless access-list get $AccessList comment ];
- :log debug ("MAC address " . $Mac . " already known: " . $Comment);
- }
-}
diff --git a/collect-wireless-mac.local.rsc b/collect-wireless-mac.local.rsc
new file mode 100644
index 0000000..8a60fea
--- /dev/null
+++ b/collect-wireless-mac.local.rsc
@@ -0,0 +1,97 @@
+#!rsc by RouterOS
+# RouterOS script: collect-wireless-mac.local
+# Copyright (c) 2013-2024 Christian Hesse <mail@eworm.de>
+# https://git.eworm.de/cgit/routeros-scripts/about/COPYING.md
+#
+# provides: lease-script, order=40
+# requires RouterOS, version=7.13
+#
+# collect wireless mac adresses in access list
+# https://git.eworm.de/cgit/routeros-scripts/about/doc/collect-wireless-mac.md
+#
+# !! Do not edit this file, it is generated from template!
+
+:global GlobalFunctionsReady;
+:while ($GlobalFunctionsReady != true) do={ :delay 500ms; }
+
+:do {
+ :local ScriptName [ :jobname ];
+
+ :global Identity;
+
+ :global EitherOr;
+ :global FormatLine;
+ :global FormatMultiLines;
+ :global GetMacVendor;
+ :global LogPrint;
+ :global ScriptLock;
+ :global SendNotification2;
+ :global SymbolForNotification;
+
+ :if ([ $ScriptLock $ScriptName 10 ] = false) do={
+ :error false;
+ }
+
+ :if ([ :len [ /interface/wireless/access-list/find where comment="--- collected above ---" disabled ] ] = 0) do={
+ /interface/wireless/access-list/add comment="--- collected above ---" disabled=yes;
+ $LogPrint warning $ScriptName ("Added disabled access-list entry with comment '--- collected above ---'.");
+ }
+ :local PlaceBefore ([ /interface/wireless/access-list/find where comment="--- collected above ---" disabled ]->0);
+
+ :foreach Reg in=[ /interface/wireless/registration-table/find where ap=no ] do={
+ :local RegVal;
+ :do {
+ :set RegVal [ /interface/wireless/registration-table/get $Reg ];
+ } on-error={
+ $LogPrint debug $ScriptName ("Device already gone... Ignoring.");
+ }
+
+ :if ([ :len ($RegVal->"mac-address") ] > 0) do={
+ :local AccessList ([ /interface/wireless/access-list/find where mac-address=($RegVal->"mac-address") ]->0);
+ :if ([ :len $AccessList ] > 0) do={
+ $LogPrint debug $ScriptName ("MAC address " . $RegVal->"mac-address" . " already known: " . \
+ [ /interface/wireless/access-list/get $AccessList comment ]);
+ }
+
+ :if ([ :len $AccessList ] = 0) do={
+ :local Address "no dhcp lease";
+ :local DnsName "no dhcp lease";
+ :local HostName "no dhcp lease";
+ :local Lease ([ /ip/dhcp-server/lease/find where active-mac-address=($RegVal->"mac-address") dynamic=yes status=bound ]->0);
+ :if ([ :len $Lease ] > 0) do={
+ :set Address [ /ip/dhcp-server/lease/get $Lease active-address ];
+ :set HostName [ $EitherOr [ /ip/dhcp-server/lease/get $Lease host-name ] "no hostname" ];
+ :set DnsName "no dns name";
+ :local DnsRec ([ /ip/dns/static/find where address=$Address ]->0);
+ :if ([ :len $DnsRec ] > 0) do={
+ :set DnsName ({ [ /ip/dns/static/get $DnsRec name ] });
+ :foreach CName in=[ /ip/dns/static/find where type=CNAME cname=($DnsName->0) ] do={
+ :set DnsName ($DnsName, [ /ip/dns/static/get $CName name ]);
+ }
+ }
+ }
+ :set ($RegVal->"ssid") [ /interface/wireless/get [ find where name=($RegVal->"interface") ] ssid ];
+ :local DateTime ([ /system/clock/get date ] . " " . [ /system/clock/get time ]);
+ :local Vendor [ $GetMacVendor ($RegVal->"mac-address") ];
+ :local Message ("MAC address " . $RegVal->"mac-address" . " (" . $Vendor . ", " . $HostName . ") " . \
+ "first seen on " . $DateTime . " connected to SSID " . $RegVal->"ssid" . ", interface " . $RegVal->"interface");
+ $LogPrint info $ScriptName $Message;
+ /interface/wireless/access-list/add place-before=$PlaceBefore comment=$Message mac-address=($RegVal->"mac-address") disabled=yes;
+ $SendNotification2 ({ origin=$ScriptName; \
+ subject=([ $SymbolForNotification "mobile-phone" ] . $RegVal->"mac-address" . " connected to " . $RegVal->"ssid"); \
+ message=("A device with unknown MAC address connected to " . $RegVal->"ssid" . " on " . $Identity . ".\n\n" . \
+ [ $FormatLine "Controller" $Identity ] . "\n" . \
+ [ $FormatLine "Interface" ($RegVal->"interface") ] . "\n" . \
+ [ $FormatLine "SSID" ($RegVal->"ssid") ] . "\n" . \
+ [ $FormatLine "MAC" ($RegVal->"mac-address") ] . "\n" . \
+ [ $FormatLine "Vendor" $Vendor ] . "\n" . \
+ [ $FormatLine "Hostname" $HostName ] . "\n" . \
+ [ $FormatLine "Address" $Address ] . "\n" . \
+ [ $FormatMultiLines "DNS name" $DnsName ] . "\n" . \
+ [ $FormatLine "Date" $DateTime ]) });
+ }
+ } else={
+ $LogPrint debug $ScriptName ("No mac address available... Ignoring.");
+ }
+ }
+} on-error={ }
diff --git a/collect-wireless-mac.template b/collect-wireless-mac.template
deleted file mode 100644
index 6ad55a2..0000000
--- a/collect-wireless-mac.template
+++ /dev/null
@@ -1,68 +0,0 @@
-#!rsc
-# RouterOS script: collect-wireless-mac%TEMPL%
-# Copyright (c) 2013-2020 Christian Hesse <mail@eworm.de>
-#
-# collect wireless mac adresses in access list
-#
-# !! This is just a template! Replace '%PATH%' with 'caps-man'
-# !! or 'interface wireless'!
-
-:global Identity;
-
-:global GetMacVendor;
-:global SendNotification;
-:global ScriptLock;
-
-$ScriptLock "collect-wireless-mac%TEMPL%";
-
-:if ([ / %PATH% access-list print count-only where comment="--- collected above ---" disabled ] = 0) do={
- / %PATH% access-list add comment="--- collected above ---" disabled=yes;
- :log warn "Added disabled access-list entry with comment '--- collected above ---'.";
-}
-:local PlaceBefore [ / %PATH% access-list find where comment="--- collected above ---" disabled ];
-
-:foreach RegTbl in=[ / %PATH% registration-table find ] do={
- :local Mac [ / %PATH% registration-table get $RegTbl mac-address ];
- :local AccessList ([ / %PATH% access-list find where mac-address=$Mac ]->0);
- :if ([ :len $AccessList ] = 0) do={
- :local Address "no dhcp lease";
- :local DnsName "no dhcp lease";
- :local HostName "no dhcp lease";
- :local Lease [ / ip dhcp-server lease find where mac-address=$Mac dynamic=yes status=bound ];
- :if ([ :len $Lease ] > 0) do={
- :set Address [ / ip dhcp-server lease get $Lease address ];
- :set HostName [ / ip dhcp-server lease get $Lease host-name ];
- :if ([ :len $HostName ] = 0) do={
- :set HostName "no hostname";
- }
- :set DnsName [ / ip dns static get ([ find where address=$Address ]->0) name ];
- :if ([ :len $DnsName ] = 0) do={
- :set DnsName "no dns name";
- }
- }
- :local RegEntry [ / %PATH% registration-table find where mac-address=$Mac ];
- :local Interface [ / %PATH% registration-table get $RegEntry interface ];
- :local Ssid [ / caps-man registration-table get $RegEntry ssid ];
- :local Ssid [ / interface wireless get [ find where name=$Interface ] ssid ];
- :local DateTime ([ / system clock get date ] . " " . [ / system clock get time ]);
- :local Vendor [ $GetMacVendor $Mac ];
- :local Message ("unknown MAC address " . $Mac . " (" . $Vendor . ", " . $HostName . ") " . \
- "first seen on " . $DateTime . " connected to SSID " . $Ssid . ", interface " . $Interface);
- / log info $Message;
- / %PATH% access-list add place-before=$PlaceBefore comment=$Message mac-address=$Mac disabled=yes;
- $SendNotification ($Mac . " connected to " . $Ssid) \
- ("A device with unknown MAC address connected to " . $Ssid . " on " . $Identity . ".\n\n" . \
- "Controller: " . $Identity . "\n" . \
- "Interface: " . $Interface . "\n" . \
- "SSID: " . $Ssid . "\n" . \
- "MAC: " . $Mac . "\n" . \
- "Vendor: " . $Vendor . "\n" . \
- "Hostname: " . $HostName . "\n" . \
- "Address: " . $Address . "\n" . \
- "DNS name: " . $DnsName . "\n" . \
- "Date: " . $DateTime);
- } else={
- :local Comment [ / %PATH% access-list get $AccessList comment ];
- :log debug ("MAC address " . $Mac . " already known: " . $Comment);
- }
-}
diff --git a/collect-wireless-mac.template.rsc b/collect-wireless-mac.template.rsc
new file mode 100644
index 0000000..c5cf74a
--- /dev/null
+++ b/collect-wireless-mac.template.rsc
@@ -0,0 +1,114 @@
+#!rsc by RouterOS
+# RouterOS script: collect-wireless-mac%TEMPL%
+# Copyright (c) 2013-2024 Christian Hesse <mail@eworm.de>
+# https://git.eworm.de/cgit/routeros-scripts/about/COPYING.md
+#
+# provides: lease-script, order=40
+# requires RouterOS, version=7.13
+#
+# collect wireless mac adresses in access list
+# https://git.eworm.de/cgit/routeros-scripts/about/doc/collect-wireless-mac.md
+#
+# !! This is just a template to generate the real script!
+# !! Pattern '%TEMPL%' is replaced, paths are filtered.
+
+:global GlobalFunctionsReady;
+:while ($GlobalFunctionsReady != true) do={ :delay 500ms; }
+
+:do {
+ :local ScriptName [ :jobname ];
+
+ :global Identity;
+
+ :global EitherOr;
+ :global FormatLine;
+ :global FormatMultiLines;
+ :global GetMacVendor;
+ :global LogPrint;
+ :global ScriptLock;
+ :global SendNotification2;
+ :global SymbolForNotification;
+
+ :if ([ $ScriptLock $ScriptName 10 ] = false) do={
+ :error false;
+ }
+
+ :if ([ :len [ /caps-man/access-list/find where comment="--- collected above ---" disabled ] ] = 0) do={
+ :if ([ :len [ /interface/wifi/access-list/find where comment="--- collected above ---" disabled ] ] = 0) do={
+ :if ([ :len [ /interface/wireless/access-list/find where comment="--- collected above ---" disabled ] ] = 0) do={
+ /caps-man/access-list/add comment="--- collected above ---" disabled=yes;
+ /interface/wifi/access-list/add comment="--- collected above ---" disabled=yes;
+ /interface/wireless/access-list/add comment="--- collected above ---" disabled=yes;
+ $LogPrint warning $ScriptName ("Added disabled access-list entry with comment '--- collected above ---'.");
+ }
+ :local PlaceBefore ([ /caps-man/access-list/find where comment="--- collected above ---" disabled ]->0);
+ :local PlaceBefore ([ /interface/wifi/access-list/find where comment="--- collected above ---" disabled ]->0);
+ :local PlaceBefore ([ /interface/wireless/access-list/find where comment="--- collected above ---" disabled ]->0);
+
+ :foreach Reg in=[ /caps-man/registration-table/find ] do={
+ :foreach Reg in=[ /interface/wifi/registration-table/find ] do={
+ :foreach Reg in=[ /interface/wireless/registration-table/find where ap=no ] do={
+ :local RegVal;
+ :do {
+ :set RegVal [ /caps-man/registration-table/get $Reg ];
+ :set RegVal [ /interface/wifi/registration-table/get $Reg ];
+ :set RegVal [ /interface/wireless/registration-table/get $Reg ];
+ } on-error={
+ $LogPrint debug $ScriptName ("Device already gone... Ignoring.");
+ }
+
+ :if ([ :len ($RegVal->"mac-address") ] > 0) do={
+ :local AccessList ([ /caps-man/access-list/find where mac-address=($RegVal->"mac-address") ]->0);
+ :local AccessList ([ /interface/wifi/access-list/find where mac-address=($RegVal->"mac-address") ]->0);
+ :local AccessList ([ /interface/wireless/access-list/find where mac-address=($RegVal->"mac-address") ]->0);
+ :if ([ :len $AccessList ] > 0) do={
+ $LogPrint debug $ScriptName ("MAC address " . $RegVal->"mac-address" . " already known: " . \
+ [ /caps-man/access-list/get $AccessList comment ]);
+ [ /interface/wifi/access-list/get $AccessList comment ]);
+ [ /interface/wireless/access-list/get $AccessList comment ]);
+ }
+
+ :if ([ :len $AccessList ] = 0) do={
+ :local Address "no dhcp lease";
+ :local DnsName "no dhcp lease";
+ :local HostName "no dhcp lease";
+ :local Lease ([ /ip/dhcp-server/lease/find where active-mac-address=($RegVal->"mac-address") dynamic=yes status=bound ]->0);
+ :if ([ :len $Lease ] > 0) do={
+ :set Address [ /ip/dhcp-server/lease/get $Lease active-address ];
+ :set HostName [ $EitherOr [ /ip/dhcp-server/lease/get $Lease host-name ] "no hostname" ];
+ :set DnsName "no dns name";
+ :local DnsRec ([ /ip/dns/static/find where address=$Address ]->0);
+ :if ([ :len $DnsRec ] > 0) do={
+ :set DnsName ({ [ /ip/dns/static/get $DnsRec name ] });
+ :foreach CName in=[ /ip/dns/static/find where type=CNAME cname=($DnsName->0) ] do={
+ :set DnsName ($DnsName, [ /ip/dns/static/get $CName name ]);
+ }
+ }
+ }
+ :set ($RegVal->"ssid") [ /interface/wireless/get [ find where name=($RegVal->"interface") ] ssid ];
+ :local DateTime ([ /system/clock/get date ] . " " . [ /system/clock/get time ]);
+ :local Vendor [ $GetMacVendor ($RegVal->"mac-address") ];
+ :local Message ("MAC address " . $RegVal->"mac-address" . " (" . $Vendor . ", " . $HostName . ") " . \
+ "first seen on " . $DateTime . " connected to SSID " . $RegVal->"ssid" . ", interface " . $RegVal->"interface");
+ $LogPrint info $ScriptName $Message;
+ /caps-man/access-list/add place-before=$PlaceBefore comment=$Message mac-address=($RegVal->"mac-address") disabled=yes;
+ /interface/wifi/access-list/add place-before=$PlaceBefore comment=$Message mac-address=($RegVal->"mac-address") disabled=yes;
+ /interface/wireless/access-list/add place-before=$PlaceBefore comment=$Message mac-address=($RegVal->"mac-address") disabled=yes;
+ $SendNotification2 ({ origin=$ScriptName; \
+ subject=([ $SymbolForNotification "mobile-phone" ] . $RegVal->"mac-address" . " connected to " . $RegVal->"ssid"); \
+ message=("A device with unknown MAC address connected to " . $RegVal->"ssid" . " on " . $Identity . ".\n\n" . \
+ [ $FormatLine "Controller" $Identity ] . "\n" . \
+ [ $FormatLine "Interface" ($RegVal->"interface") ] . "\n" . \
+ [ $FormatLine "SSID" ($RegVal->"ssid") ] . "\n" . \
+ [ $FormatLine "MAC" ($RegVal->"mac-address") ] . "\n" . \
+ [ $FormatLine "Vendor" $Vendor ] . "\n" . \
+ [ $FormatLine "Hostname" $HostName ] . "\n" . \
+ [ $FormatLine "Address" $Address ] . "\n" . \
+ [ $FormatMultiLines "DNS name" $DnsName ] . "\n" . \
+ [ $FormatLine "Date" $DateTime ]) });
+ }
+ } else={
+ $LogPrint debug $ScriptName ("No mac address available... Ignoring.");
+ }
+ }
+} on-error={ }
diff --git a/collect-wireless-mac.wifi.rsc b/collect-wireless-mac.wifi.rsc
new file mode 100644
index 0000000..12c3361
--- /dev/null
+++ b/collect-wireless-mac.wifi.rsc
@@ -0,0 +1,96 @@
+#!rsc by RouterOS
+# RouterOS script: collect-wireless-mac.wifi
+# Copyright (c) 2013-2024 Christian Hesse <mail@eworm.de>
+# https://git.eworm.de/cgit/routeros-scripts/about/COPYING.md
+#
+# provides: lease-script, order=40
+# requires RouterOS, version=7.13
+#
+# collect wireless mac adresses in access list
+# https://git.eworm.de/cgit/routeros-scripts/about/doc/collect-wireless-mac.md
+#
+# !! Do not edit this file, it is generated from template!
+
+:global GlobalFunctionsReady;
+:while ($GlobalFunctionsReady != true) do={ :delay 500ms; }
+
+:do {
+ :local ScriptName [ :jobname ];
+
+ :global Identity;
+
+ :global EitherOr;
+ :global FormatLine;
+ :global FormatMultiLines;
+ :global GetMacVendor;
+ :global LogPrint;
+ :global ScriptLock;
+ :global SendNotification2;
+ :global SymbolForNotification;
+
+ :if ([ $ScriptLock $ScriptName 10 ] = false) do={
+ :error false;
+ }
+
+ :if ([ :len [ /interface/wifi/access-list/find where comment="--- collected above ---" disabled ] ] = 0) do={
+ /interface/wifi/access-list/add comment="--- collected above ---" disabled=yes;
+ $LogPrint warning $ScriptName ("Added disabled access-list entry with comment '--- collected above ---'.");
+ }
+ :local PlaceBefore ([ /interface/wifi/access-list/find where comment="--- collected above ---" disabled ]->0);
+
+ :foreach Reg in=[ /interface/wifi/registration-table/find ] do={
+ :local RegVal;
+ :do {
+ :set RegVal [ /interface/wifi/registration-table/get $Reg ];
+ } on-error={
+ $LogPrint debug $ScriptName ("Device already gone... Ignoring.");
+ }
+
+ :if ([ :len ($RegVal->"mac-address") ] > 0) do={
+ :local AccessList ([ /interface/wifi/access-list/find where mac-address=($RegVal->"mac-address") ]->0);
+ :if ([ :len $AccessList ] > 0) do={
+ $LogPrint debug $ScriptName ("MAC address " . $RegVal->"mac-address" . " already known: " . \
+ [ /interface/wifi/access-list/get $AccessList comment ]);
+ }
+
+ :if ([ :len $AccessList ] = 0) do={
+ :local Address "no dhcp lease";
+ :local DnsName "no dhcp lease";
+ :local HostName "no dhcp lease";
+ :local Lease ([ /ip/dhcp-server/lease/find where active-mac-address=($RegVal->"mac-address") dynamic=yes status=bound ]->0);
+ :if ([ :len $Lease ] > 0) do={
+ :set Address [ /ip/dhcp-server/lease/get $Lease active-address ];
+ :set HostName [ $EitherOr [ /ip/dhcp-server/lease/get $Lease host-name ] "no hostname" ];
+ :set DnsName "no dns name";
+ :local DnsRec ([ /ip/dns/static/find where address=$Address ]->0);
+ :if ([ :len $DnsRec ] > 0) do={
+ :set DnsName ({ [ /ip/dns/static/get $DnsRec name ] });
+ :foreach CName in=[ /ip/dns/static/find where type=CNAME cname=($DnsName->0) ] do={
+ :set DnsName ($DnsName, [ /ip/dns/static/get $CName name ]);
+ }
+ }
+ }
+ :local DateTime ([ /system/clock/get date ] . " " . [ /system/clock/get time ]);
+ :local Vendor [ $GetMacVendor ($RegVal->"mac-address") ];
+ :local Message ("MAC address " . $RegVal->"mac-address" . " (" . $Vendor . ", " . $HostName . ") " . \
+ "first seen on " . $DateTime . " connected to SSID " . $RegVal->"ssid" . ", interface " . $RegVal->"interface");
+ $LogPrint info $ScriptName $Message;
+ /interface/wifi/access-list/add place-before=$PlaceBefore comment=$Message mac-address=($RegVal->"mac-address") disabled=yes;
+ $SendNotification2 ({ origin=$ScriptName; \
+ subject=([ $SymbolForNotification "mobile-phone" ] . $RegVal->"mac-address" . " connected to " . $RegVal->"ssid"); \
+ message=("A device with unknown MAC address connected to " . $RegVal->"ssid" . " on " . $Identity . ".\n\n" . \
+ [ $FormatLine "Controller" $Identity ] . "\n" . \
+ [ $FormatLine "Interface" ($RegVal->"interface") ] . "\n" . \
+ [ $FormatLine "SSID" ($RegVal->"ssid") ] . "\n" . \
+ [ $FormatLine "MAC" ($RegVal->"mac-address") ] . "\n" . \
+ [ $FormatLine "Vendor" $Vendor ] . "\n" . \
+ [ $FormatLine "Hostname" $HostName ] . "\n" . \
+ [ $FormatLine "Address" $Address ] . "\n" . \
+ [ $FormatMultiLines "DNS name" $DnsName ] . "\n" . \
+ [ $FormatLine "Date" $DateTime ]) });
+ }
+ } else={
+ $LogPrint debug $ScriptName ("No mac address available... Ignoring.");
+ }
+ }
+} on-error={ }
diff --git a/contrib/logo-color.d/browser-01.avif b/contrib/logo-color.d/browser-01.avif
new file mode 100644
index 0000000..3dc0a1f
--- /dev/null
+++ b/contrib/logo-color.d/browser-01.avif
Binary files differ
diff --git a/contrib/logo-color.d/browser-02.avif b/contrib/logo-color.d/browser-02.avif
new file mode 100644
index 0000000..1867fbe
--- /dev/null
+++ b/contrib/logo-color.d/browser-02.avif
Binary files differ
diff --git a/contrib/logo-color.d/browser-03.avif b/contrib/logo-color.d/browser-03.avif
new file mode 100644
index 0000000..dc24bbb
--- /dev/null
+++ b/contrib/logo-color.d/browser-03.avif
Binary files differ
diff --git a/contrib/logo-color.d/script.js b/contrib/logo-color.d/script.js
new file mode 100644
index 0000000..82cc204
--- /dev/null
+++ b/contrib/logo-color.d/script.js
@@ -0,0 +1,12 @@
+function invertHex(hex) {
+ return (Number("0x1" + hex) ^ 0xffffff).toString(16).substr(1);
+}
+
+function color() {
+ var svg = document.querySelector(".logo").getSVGDocument();
+ svg.getElementById("dark-1").setAttribute("stop-color", document.getElementById("color1").value);
+ svg.getElementById("dark-2").setAttribute("stop-color", document.getElementById("color2").value);
+ var background = document.getElementById("color3").value;
+ svg.getElementById("background").setAttribute("fill", background);
+ svg.getElementById("hexagon").setAttribute("stroke", "#" + invertHex(background.substring(1)));
+}
diff --git a/contrib/logo-color.d/style.css b/contrib/logo-color.d/style.css
new file mode 100644
index 0000000..eb2ec6a
--- /dev/null
+++ b/contrib/logo-color.d/style.css
@@ -0,0 +1,5 @@
+body {
+ font-family: fira-sans, sans-serif;
+ font-size: 10pt;
+ background-color: transparent;
+}
diff --git a/contrib/logo-color.html b/contrib/logo-color.html
new file mode 100644
index 0000000..17942ce
--- /dev/null
+++ b/contrib/logo-color.html
@@ -0,0 +1,40 @@
+<!DOCTYPE html>
+<html lang="en">
+<head>
+<meta charset="UTF-8">
+<title>RouterOS-Scripts Logo Color Changer</title>
+<link rel="stylesheet" type="text/css" href="logo-color.d/style.css">
+<script src="logo-color.d/script.js"></script>
+</head>
+<body>
+
+<h1>RouterOS-Scripts Logo Color Changer</h1>
+
+<p>You want the logo for your own notifications? But you joined the
+<a href="https://t.me/routeros_scripts">Telegram Group</a> and want
+something that differentiates? Color it!</p>
+
+<embed class="logo" src="../logo.svg" width="192" height="192" type="image/svg+xml">
+
+<p>Select the colors here:
+<input id="color1" type="color" value="#222222" onchange="color();">
+<input id="color2" type="color" value="#444444" onchange="color();">
+<input id="color3" type="color" value="#ffffff" onchange="color();"></p>
+
+<p>Then right-click, click "<i>Take Screenshot</i>" and finally select the
+logo and download it.</p>
+
+<p><img src="logo-color.d/browser-01.avif" width=533 height=482 alt="Screenshot Browser 01">
+<img src="logo-color.d/browser-02.avif" width=533 height=482 alt="Screenshot Browser 02">
+<img src="logo-color.d/browser-03.avif" width=533 height=482 alt="Screenshot Browser 03"></p>
+
+<p>(This example is with
+<a href="https://www.mozilla.org/de/firefox/new/">Firefox</a>. The workflow
+for other browsers may differ.)</p>
+
+<p>See how to
+<a href="../../about/doc/mod/notification-telegram.md#set-a-profile-photo">Set
+a profile photo</a> for your Telegram bot.</p>
+
+</body>
+</html>
diff --git a/contrib/notification.d/script.js b/contrib/notification.d/script.js
new file mode 100644
index 0000000..91741fd
--- /dev/null
+++ b/contrib/notification.d/script.js
@@ -0,0 +1,6 @@
+function visible(cb, element) {
+ document.getElementById(element).style.display = cb.checked ? "block" : "none";
+}
+function update(cb, element) {
+ document.getElementById(element).innerHTML = cb.value;
+}
diff --git a/contrib/notification.d/style.css b/contrib/notification.d/style.css
new file mode 100644
index 0000000..648ea23
--- /dev/null
+++ b/contrib/notification.d/style.css
@@ -0,0 +1,36 @@
+body {
+ font-family: fira-sans, sans-serif;
+ font-size: 10pt;
+ background-color: transparent;
+}
+div.notification {
+ position: relative;
+ float: right;
+ width: 600px;
+ border: 3px outset #6c5d53;
+ /* border-radius: 5px; */
+ padding: 10px;
+ background-color: #e6e6e6;
+}
+div.content {
+ padding-left: 60px;
+}
+img.logo {
+ float: left;
+ border-radius: 50%;
+}
+p.heading {
+ margin: 0px;
+ font-weight: bold;
+ text-decoration: underline;
+}
+p.hint {
+ display: none;
+}
+pre {
+ font-family: fira-mono, monospace;
+ white-space: pre-wrap;
+}
+span.link {
+ color: #863600;
+}
diff --git a/contrib/notification.html b/contrib/notification.html
new file mode 100644
index 0000000..7875036
--- /dev/null
+++ b/contrib/notification.html
@@ -0,0 +1,35 @@
+<!DOCTYPE html>
+<html lang="en">
+<head>
+<meta charset="UTF-8">
+<title>RouterOS-Scripts Notification Generator</title>
+<link rel="stylesheet" type="text/css" href="notification.d/style.css">
+<script src="notification.d/script.js"></script>
+</head>
+<body>
+
+<h1>RouterOS-Scripts Notification Generator</h1>
+
+<div class="notification">
+ <img src="../logo.svg" alt="logo" class="logo" width=48 height=48>
+ <div class="content">
+ <p id="heading" class="heading">[<span id="hostname">MikroTik</span>] <span id="subject">ℹ️ Subject</span></p>
+ <pre id="message">Message</pre>
+ <p id="link" class="hint">🔗 <span id="link-text" class="link">https://eworm.de/</span></p>
+ <p id="queued" class="hint">⏰ This message was queued since <span id="queued-since">oct/18/2022 18:30:48</span> and may be obsolete.</p>
+ <p id="cut" class="hint">✂️ The message was too long and has been truncated, cut off <span id="cut-percent">13</span>%!</p>
+ </div>
+</div>
+
+<p>Hostname: <input type="text" value="MikroTik" onchange="update(this, 'hostname')"></p>
+<p>Subject: <input type="text" size=50 value="ℹ️ Subject" onchange="update(this, 'subject')"></p>
+<p>Message: <textarea id="w3review" name="w3review" rows="4" cols="50" onchange="update(this, 'message')">Message</textarea></p>
+<p><input type="checkbox" onclick="visible(this, 'link')"> Show link: <input type="text" value="https://eworm.de/" onchange="update(this, 'link-text')"></p>
+<p><input type="checkbox" onclick="visible(this, 'queued')"> Queued since <input type="text" value="oct/18/2022 18:30:48" onchange="update(this, 'queued-since')"></p>
+<p><input type="checkbox" onclick="visible(this, 'cut')"> Cut-off with <input type="number" min=1 max=99 value=13 onchange="update(this, 'cut-percent')"> percent</p>
+
+<p>Then right-click, click "<i>Take Screenshot</i>" and finally select the
+notification and download it.</p>
+
+</body>
+</html>
diff --git a/daily-psk-schedule b/daily-psk-schedule
deleted file mode 100644
index c9be0bf..0000000
--- a/daily-psk-schedule
+++ /dev/null
@@ -1,28 +0,0 @@
-#!rsc
-# RouterOS script: daily-psk-schedule
-# Copyright (c) 2013-2020 Christian Hesse <mail@eworm.de>
-#
-# schedule daily-psk on startup
-
-:local Scheduler [ / system scheduler find where name="daily-psk-schedule" ];
-
-:if ([ / system scheduler get $Scheduler interval ] = 0s) do={
- / system scheduler set interval=15s $Scheduler;
-} else={
- :if ([ / tool netwatch get [ find where comment=[ / tool e-mail get address ] ] status ] != "up") do={
- :log warning "Mail server is not up.";
- :error "Warning: See log for details.";
- }
-
- :if ([ / system ntp client get status ] != "synchronized") do={
- :if ([ / system resource get uptime ] > 5m) do={
- / system script run [ find where name="rotate-ntp" ];
- }
- :log warning "Time is not yet synchronized from ntp.";
- :error "Warning: See log for details.";
- }
-
- / system script run [ find where name~"^daily-psk\\.(capsman|local)\$" ];
-
- / system scheduler set interval=0s $Scheduler;
-}
diff --git a/daily-psk.capsman b/daily-psk.capsman
deleted file mode 100644
index 36dfc1a..0000000
--- a/daily-psk.capsman
+++ /dev/null
@@ -1,95 +0,0 @@
-#!rsc
-# RouterOS script: daily-psk.capsman
-# Copyright (c) 2013-2020 Christian Hesse <mail@eworm.de>
-# Michael Gisbers <michael@gisbers.de>
-#
-# update daily PSK (pre shared key)
-#
-# !! Do not edit this file, it is generated from template!
-
-:global Identity;
-:global DailyPskMatchComment;
-
-:global SendNotification;
-:global UrlEncode;
-:global WaitForFile;
-
-:local Seen [ :toarray "" ];
-
-# return pseudo-random string for PSK
-:local GeneratePSK do={
- :local Date [ :tostr $1 ];
-
- :global DailyPskSecrets;
-
- :local Months { "jan"; "feb"; "mar"; "apr"; "may"; "jun";
- "jul"; "aug"; "sep"; "oct"; "nov"; "dec" };
-
- :local Month [ :pick $Date 0 3 ];
- :local Day [ :tonum [ :pick $Date 4 6 ] ];
- :local Year [ :pick $Date 7 11 ];
-
- :for MIndex from=0 to=[ :len $Months ] do={
- :if ($Months->$MIndex = $Month) do={
- :set Month ($MIndex + 1);
- }
- }
-
- :local A ((14 - $Month) / 12);
- :local B ($Year - $A);
- :local C ($Month + 12 * $A - 2);
- :local WeekDay (7000 + $Day + $B + ($B / 4) - ($B / 100) + ($B / 400) + ((31 * $C) / 12));
- :set WeekDay ($WeekDay - (($WeekDay / 7) * 7));
-
- :return (($DailyPskSecrets->0->($Day - 1)) . \
- ($DailyPskSecrets->1->($Month - 1)) . \
- ($DailyPskSecrets->2->$WeekDay));
-}
-
-:local Date [ / system clock get date ];
-:local NewPsk [ $GeneratePSK $Date ];
-
-:foreach AccList in=[ / caps-man access-list find where comment~$DailyPskMatchComment ] do={
- :local Ssid [ / caps-man access-list get $AccList ssid-regexp ];
- :local Configuration [ / caps-man configuration get ([ find where ssid=$Ssid ]->0) name ];
- :local OldPsk [ / caps-man access-list get $AccList private-passphrase ];
- :local Skip 0;
-
- :if ($NewPsk != $OldPsk) do={
- :log info ("Updating daily PSK for " . $Ssid . " to " . $NewPsk . " (was " . $OldPsk . ")");
- / caps-man access-list set $AccList private-passphrase=$NewPsk;
-
- :if ([ / caps-man interface print count-only where configuration=$Configuration ] > 0) do={
- :foreach SeenSsid in=$Seen do={
- :if ($SeenSsid = $Ssid) do={
- :log debug ("Already sent a mail for SSID " . $Ssid . ", skipping.");
- :set Skip 1;
- }
- }
-
- :if ($Skip = 0) do={
- :set Seen ($Seen, $Ssid);
-
- :local Url ("https://www.eworm.de/cgi-bin/cqrlogo-wifi.cgi" . \
- "?scale=8&level=1&ssid=" . [ $UrlEncode $Ssid ] . "&pass=" . [ $UrlEncode $NewPsk ]);
- :local Attach "qrcode-daily.png";
-
- :do {
- / tool fetch check-certificate=yes-without-crl \
- $Url dst-path=$Attach;
- $WaitForFile $Attach;
- } on-error={
- :set Attach "";
- }
-
- $SendNotification ("daily PSK " . $Ssid) \
- ("This is the daily PSK on " . $Identity . ":\n\n" . \
- "SSID: " . $Ssid . "\n" . \
- "PSK: " . $NewPsk . "\n" . \
- "Date: " . $Date . "\n\n" . \
- "A client device specific rule must not exist!\n\n" . \
- $Url) $Attach;
- }
- }
- }
-}
diff --git a/daily-psk.capsman.rsc b/daily-psk.capsman.rsc
new file mode 100644
index 0000000..64e8ce7
--- /dev/null
+++ b/daily-psk.capsman.rsc
@@ -0,0 +1,92 @@
+#!rsc by RouterOS
+# RouterOS script: daily-psk.capsman
+# Copyright (c) 2013-2024 Christian Hesse <mail@eworm.de>
+# Michael Gisbers <michael@gisbers.de>
+# https://git.eworm.de/cgit/routeros-scripts/about/COPYING.md
+#
+# requires RouterOS, version=7.13
+#
+# update daily PSK (pre shared key)
+# https://git.eworm.de/cgit/routeros-scripts/about/doc/daily-psk.md
+#
+# !! Do not edit this file, it is generated from template!
+
+:global GlobalFunctionsReady;
+:while ($GlobalFunctionsReady != true) do={ :delay 500ms; }
+
+:do {
+ :local ScriptName [ :jobname ];
+
+ :global DailyPskMatchComment;
+ :global DailyPskQrCodeUrl;
+ :global Identity;
+
+ :global FormatLine;
+ :global LogPrint;
+ :global ScriptLock;
+ :global SendNotification2;
+ :global SymbolForNotification;
+ :global UrlEncode;
+ :global WaitForFile;
+ :global WaitFullyConnected;
+
+ :if ([ $ScriptLock $ScriptName ] = false) do={
+ :error false;
+ }
+ $WaitFullyConnected;
+
+ # return pseudo-random string for PSK
+ :local GeneratePSK do={
+ :local Date [ :tostr $1 ];
+
+ :global DailyPskSecrets;
+
+ :global ParseDate;
+
+ :set Date [ $ParseDate $Date ];
+
+ :local A ((14 - ($Date->"month")) / 12);
+ :local B (($Date->"year") - $A);
+ :local C (($Date->"month") + 12 * $A - 2);
+ :local WeekDay (7000 + ($Date->"day") + $B + ($B / 4) - ($B / 100) + ($B / 400) + ((31 * $C) / 12));
+ :set WeekDay ($WeekDay - (($WeekDay / 7) * 7));
+
+ :return (($DailyPskSecrets->0->(($Date->"day") - 1)) . \
+ ($DailyPskSecrets->1->(($Date->"month") - 1)) . \
+ ($DailyPskSecrets->2->$WeekDay));
+ }
+
+ :local Seen ({});
+ :local Date [ /system/clock/get date ];
+ :local NewPsk [ $GeneratePSK $Date ];
+
+ :foreach AccList in=[ /caps-man/access-list/find where comment~$DailyPskMatchComment ] do={
+ :local SsidRegExp [ /caps-man/access-list/get $AccList ssid-regexp ];
+ :local Configuration ([ /caps-man/configuration/find where ssid~$SsidRegExp ]->0);
+ :local Ssid [ /caps-man/configuration/get $Configuration ssid ];
+ :local OldPsk [ /caps-man/access-list/get $AccList private-passphrase ];
+ :local Skip 0;
+
+ :if ($NewPsk != $OldPsk) do={
+ $LogPrint info $ScriptName ("Updating daily PSK for " . $Ssid . " to " . $NewPsk . " (was " . $OldPsk . ")");
+ /caps-man/access-list/set $AccList private-passphrase=$NewPsk;
+
+ :if ([ :len [ /caps-man/actual-interface-configuration/find where configuration.ssid=$Ssid !disabled ] ] > 0) do={
+ :if ($Seen->$Ssid = 1) do={
+ $LogPrint debug $ScriptName ("Already sent a mail for SSID " . $Ssid . ", skipping.");
+ } else={
+ :local Link ($DailyPskQrCodeUrl . \
+ "?scale=8&level=1&ssid=" . [ $UrlEncode $Ssid ] . "&pass=" . [ $UrlEncode $NewPsk ]);
+ $SendNotification2 ({ origin=$ScriptName; \
+ subject=([ $SymbolForNotification "calendar" ] . "daily PSK " . $Ssid); \
+ message=("This is the daily PSK on " . $Identity . ":\n\n" . \
+ [ $FormatLine "SSID" $Ssid ] . "\n" . \
+ [ $FormatLine "PSK" $NewPsk ] . "\n" . \
+ [ $FormatLine "Date" $Date ] . "\n\n" . \
+ "A client device specific rule must not exist!"); link=$Link });
+ :set ($Seen->$Ssid) 1;
+ }
+ }
+ }
+ }
+} on-error={ }
diff --git a/daily-psk.local b/daily-psk.local
deleted file mode 100644
index 9950db8..0000000
--- a/daily-psk.local
+++ /dev/null
@@ -1,95 +0,0 @@
-#!rsc
-# RouterOS script: daily-psk.local
-# Copyright (c) 2013-2020 Christian Hesse <mail@eworm.de>
-# Michael Gisbers <michael@gisbers.de>
-#
-# update daily PSK (pre shared key)
-#
-# !! Do not edit this file, it is generated from template!
-
-:global Identity;
-:global DailyPskMatchComment;
-
-:global SendNotification;
-:global UrlEncode;
-:global WaitForFile;
-
-:local Seen [ :toarray "" ];
-
-# return pseudo-random string for PSK
-:local GeneratePSK do={
- :local Date [ :tostr $1 ];
-
- :global DailyPskSecrets;
-
- :local Months { "jan"; "feb"; "mar"; "apr"; "may"; "jun";
- "jul"; "aug"; "sep"; "oct"; "nov"; "dec" };
-
- :local Month [ :pick $Date 0 3 ];
- :local Day [ :tonum [ :pick $Date 4 6 ] ];
- :local Year [ :pick $Date 7 11 ];
-
- :for MIndex from=0 to=[ :len $Months ] do={
- :if ($Months->$MIndex = $Month) do={
- :set Month ($MIndex + 1);
- }
- }
-
- :local A ((14 - $Month) / 12);
- :local B ($Year - $A);
- :local C ($Month + 12 * $A - 2);
- :local WeekDay (7000 + $Day + $B + ($B / 4) - ($B / 100) + ($B / 400) + ((31 * $C) / 12));
- :set WeekDay ($WeekDay - (($WeekDay / 7) * 7));
-
- :return (($DailyPskSecrets->0->($Day - 1)) . \
- ($DailyPskSecrets->1->($Month - 1)) . \
- ($DailyPskSecrets->2->$WeekDay));
-}
-
-:local Date [ / system clock get date ];
-:local NewPsk [ $GeneratePSK $Date ];
-
-:foreach AccList in=[ / interface wireless access-list find where comment~$DailyPskMatchComment ] do={
- :local IntName [ / interface wireless access-list get $AccList interface ];
- :local Ssid [ / interface wireless get $IntName ssid ];
- :local OldPsk [ / interface wireless access-list get $AccList private-pre-shared-key ];
- :local Skip 0;
-
- :if ($NewPsk != $OldPsk) do={
- :log info ("Updating daily PSK for " . $Ssid . " to " . $NewPsk . " (was " . $OldPsk . ")");
- / interface wireless access-list set $AccList private-pre-shared-key=$NewPsk;
-
- :if ([ / interface wireless print count-only where name=$IntName disabled=no ] = 1) do={
- :foreach SeenSsid in=$Seen do={
- :if ($SeenSsid = $Ssid) do={
- :log debug ("Already sent a mail for SSID " . $Ssid . ", skipping.");
- :set Skip 1;
- }
- }
-
- :if ($Skip = 0) do={
- :set Seen ($Seen, $Ssid);
-
- :local Url ("https://www.eworm.de/cgi-bin/cqrlogo-wifi.cgi" . \
- "?scale=8&level=1&ssid=" . [ $UrlEncode $Ssid ] . "&pass=" . [ $UrlEncode $NewPsk ]);
- :local Attach "qrcode-daily.png";
-
- :do {
- / tool fetch check-certificate=yes-without-crl \
- $Url dst-path=$Attach;
- $WaitForFile $Attach;
- } on-error={
- :set Attach "";
- }
-
- $SendNotification ("daily PSK " . $Ssid) \
- ("This is the daily PSK on " . $Identity . ":\n\n" . \
- "SSID: " . $Ssid . "\n" . \
- "PSK: " . $NewPsk . "\n" . \
- "Date: " . $Date . "\n\n" . \
- "A client device specific rule must not exist!\n\n" . \
- $Url) $Attach;
- }
- }
- }
-}
diff --git a/daily-psk.local.rsc b/daily-psk.local.rsc
new file mode 100644
index 0000000..48e2b8d
--- /dev/null
+++ b/daily-psk.local.rsc
@@ -0,0 +1,91 @@
+#!rsc by RouterOS
+# RouterOS script: daily-psk.local
+# Copyright (c) 2013-2024 Christian Hesse <mail@eworm.de>
+# Michael Gisbers <michael@gisbers.de>
+# https://git.eworm.de/cgit/routeros-scripts/about/COPYING.md
+#
+# requires RouterOS, version=7.13
+#
+# update daily PSK (pre shared key)
+# https://git.eworm.de/cgit/routeros-scripts/about/doc/daily-psk.md
+#
+# !! Do not edit this file, it is generated from template!
+
+:global GlobalFunctionsReady;
+:while ($GlobalFunctionsReady != true) do={ :delay 500ms; }
+
+:do {
+ :local ScriptName [ :jobname ];
+
+ :global DailyPskMatchComment;
+ :global DailyPskQrCodeUrl;
+ :global Identity;
+
+ :global FormatLine;
+ :global LogPrint;
+ :global ScriptLock;
+ :global SendNotification2;
+ :global SymbolForNotification;
+ :global UrlEncode;
+ :global WaitForFile;
+ :global WaitFullyConnected;
+
+ :if ([ $ScriptLock $ScriptName ] = false) do={
+ :error false;
+ }
+ $WaitFullyConnected;
+
+ # return pseudo-random string for PSK
+ :local GeneratePSK do={
+ :local Date [ :tostr $1 ];
+
+ :global DailyPskSecrets;
+
+ :global ParseDate;
+
+ :set Date [ $ParseDate $Date ];
+
+ :local A ((14 - ($Date->"month")) / 12);
+ :local B (($Date->"year") - $A);
+ :local C (($Date->"month") + 12 * $A - 2);
+ :local WeekDay (7000 + ($Date->"day") + $B + ($B / 4) - ($B / 100) + ($B / 400) + ((31 * $C) / 12));
+ :set WeekDay ($WeekDay - (($WeekDay / 7) * 7));
+
+ :return (($DailyPskSecrets->0->(($Date->"day") - 1)) . \
+ ($DailyPskSecrets->1->(($Date->"month") - 1)) . \
+ ($DailyPskSecrets->2->$WeekDay));
+ }
+
+ :local Seen ({});
+ :local Date [ /system/clock/get date ];
+ :local NewPsk [ $GeneratePSK $Date ];
+
+ :foreach AccList in=[ /interface/wireless/access-list/find where comment~$DailyPskMatchComment ] do={
+ :local IntName [ /interface/wireless/access-list/get $AccList interface ];
+ :local Ssid [ /interface/wireless/get $IntName ssid ];
+ :local OldPsk [ /interface/wireless/access-list/get $AccList private-pre-shared-key ];
+ :local Skip 0;
+
+ :if ($NewPsk != $OldPsk) do={
+ $LogPrint info $ScriptName ("Updating daily PSK for " . $Ssid . " to " . $NewPsk . " (was " . $OldPsk . ")");
+ /interface/wireless/access-list/set $AccList private-pre-shared-key=$NewPsk;
+
+ :if ([ :len [ /interface/wireless/find where name=$IntName !disabled ] ] = 1) do={
+ :if ($Seen->$Ssid = 1) do={
+ $LogPrint debug $ScriptName ("Already sent a mail for SSID " . $Ssid . ", skipping.");
+ } else={
+ :local Link ($DailyPskQrCodeUrl . \
+ "?scale=8&level=1&ssid=" . [ $UrlEncode $Ssid ] . "&pass=" . [ $UrlEncode $NewPsk ]);
+ $SendNotification2 ({ origin=$ScriptName; \
+ subject=([ $SymbolForNotification "calendar" ] . "daily PSK " . $Ssid); \
+ message=("This is the daily PSK on " . $Identity . ":\n\n" . \
+ [ $FormatLine "SSID" $Ssid ] . "\n" . \
+ [ $FormatLine "PSK" $NewPsk ] . "\n" . \
+ [ $FormatLine "Date" $Date ] . "\n\n" . \
+ "A client device specific rule must not exist!"); link=$Link });
+ :set ($Seen->$Ssid) 1;
+ }
+ }
+ }
+ }
+} on-error={ }
diff --git a/daily-psk.template b/daily-psk.template
deleted file mode 100644
index e0ae26a..0000000
--- a/daily-psk.template
+++ /dev/null
@@ -1,101 +0,0 @@
-#!rsc
-# RouterOS script: daily-psk%TEMPL%
-# Copyright (c) 2013-2020 Christian Hesse <mail@eworm.de>
-# Michael Gisbers <michael@gisbers.de>
-#
-# update daily PSK (pre shared key)
-#
-# !! This is just a template! Replace '%PATH%' with 'caps-man'
-# !! or 'interface wireless'!
-
-:global Identity;
-:global DailyPskMatchComment;
-
-:global SendNotification;
-:global UrlEncode;
-:global WaitForFile;
-
-:local Seen [ :toarray "" ];
-
-# return pseudo-random string for PSK
-:local GeneratePSK do={
- :local Date [ :tostr $1 ];
-
- :global DailyPskSecrets;
-
- :local Months { "jan"; "feb"; "mar"; "apr"; "may"; "jun";
- "jul"; "aug"; "sep"; "oct"; "nov"; "dec" };
-
- :local Month [ :pick $Date 0 3 ];
- :local Day [ :tonum [ :pick $Date 4 6 ] ];
- :local Year [ :pick $Date 7 11 ];
-
- :for MIndex from=0 to=[ :len $Months ] do={
- :if ($Months->$MIndex = $Month) do={
- :set Month ($MIndex + 1);
- }
- }
-
- :local A ((14 - $Month) / 12);
- :local B ($Year - $A);
- :local C ($Month + 12 * $A - 2);
- :local WeekDay (7000 + $Day + $B + ($B / 4) - ($B / 100) + ($B / 400) + ((31 * $C) / 12));
- :set WeekDay ($WeekDay - (($WeekDay / 7) * 7));
-
- :return (($DailyPskSecrets->0->($Day - 1)) . \
- ($DailyPskSecrets->1->($Month - 1)) . \
- ($DailyPskSecrets->2->$WeekDay));
-}
-
-:local Date [ / system clock get date ];
-:local NewPsk [ $GeneratePSK $Date ];
-
-:foreach AccList in=[ / %PATH% access-list find where comment~$DailyPskMatchComment ] do={
- :local IntName [ / interface wireless access-list get $AccList interface ];
- :local Ssid [ / interface wireless get $IntName ssid ];
- :local Ssid [ / caps-man access-list get $AccList ssid-regexp ];
- :local Configuration [ / caps-man configuration get ([ find where ssid=$Ssid ]->0) name ];
- :local OldPsk [ / interface wireless access-list get $AccList private-pre-shared-key ];
- :local OldPsk [ / caps-man access-list get $AccList private-passphrase ];
- :local Skip 0;
-
- :if ($NewPsk != $OldPsk) do={
- :log info ("Updating daily PSK for " . $Ssid . " to " . $NewPsk . " (was " . $OldPsk . ")");
- / interface wireless access-list set $AccList private-pre-shared-key=$NewPsk;
- / caps-man access-list set $AccList private-passphrase=$NewPsk;
-
- :if ([ / interface wireless print count-only where name=$IntName disabled=no ] = 1) do={
- :if ([ / caps-man interface print count-only where configuration=$Configuration ] > 0) do={
- :foreach SeenSsid in=$Seen do={
- :if ($SeenSsid = $Ssid) do={
- :log debug ("Already sent a mail for SSID " . $Ssid . ", skipping.");
- :set Skip 1;
- }
- }
-
- :if ($Skip = 0) do={
- :set Seen ($Seen, $Ssid);
-
- :local Url ("https://www.eworm.de/cgi-bin/cqrlogo-wifi.cgi" . \
- "?scale=8&level=1&ssid=" . [ $UrlEncode $Ssid ] . "&pass=" . [ $UrlEncode $NewPsk ]);
- :local Attach "qrcode-daily.png";
-
- :do {
- / tool fetch check-certificate=yes-without-crl \
- $Url dst-path=$Attach;
- $WaitForFile $Attach;
- } on-error={
- :set Attach "";
- }
-
- $SendNotification ("daily PSK " . $Ssid) \
- ("This is the daily PSK on " . $Identity . ":\n\n" . \
- "SSID: " . $Ssid . "\n" . \
- "PSK: " . $NewPsk . "\n" . \
- "Date: " . $Date . "\n\n" . \
- "A client device specific rule must not exist!\n\n" . \
- $Url) $Attach;
- }
- }
- }
-}
diff --git a/daily-psk.template.rsc b/daily-psk.template.rsc
new file mode 100644
index 0000000..5097b00
--- /dev/null
+++ b/daily-psk.template.rsc
@@ -0,0 +1,107 @@
+#!rsc by RouterOS
+# RouterOS script: daily-psk%TEMPL%
+# Copyright (c) 2013-2024 Christian Hesse <mail@eworm.de>
+# Michael Gisbers <michael@gisbers.de>
+# https://git.eworm.de/cgit/routeros-scripts/about/COPYING.md
+#
+# requires RouterOS, version=7.13
+#
+# update daily PSK (pre shared key)
+# https://git.eworm.de/cgit/routeros-scripts/about/doc/daily-psk.md
+#
+# !! This is just a template to generate the real script!
+# !! Pattern '%TEMPL%' is replaced, paths are filtered.
+
+:global GlobalFunctionsReady;
+:while ($GlobalFunctionsReady != true) do={ :delay 500ms; }
+
+:do {
+ :local ScriptName [ :jobname ];
+
+ :global DailyPskMatchComment;
+ :global DailyPskQrCodeUrl;
+ :global Identity;
+
+ :global FormatLine;
+ :global LogPrint;
+ :global ScriptLock;
+ :global SendNotification2;
+ :global SymbolForNotification;
+ :global UrlEncode;
+ :global WaitForFile;
+ :global WaitFullyConnected;
+
+ :if ([ $ScriptLock $ScriptName ] = false) do={
+ :error false;
+ }
+ $WaitFullyConnected;
+
+ # return pseudo-random string for PSK
+ :local GeneratePSK do={
+ :local Date [ :tostr $1 ];
+
+ :global DailyPskSecrets;
+
+ :global ParseDate;
+
+ :set Date [ $ParseDate $Date ];
+
+ :local A ((14 - ($Date->"month")) / 12);
+ :local B (($Date->"year") - $A);
+ :local C (($Date->"month") + 12 * $A - 2);
+ :local WeekDay (7000 + ($Date->"day") + $B + ($B / 4) - ($B / 100) + ($B / 400) + ((31 * $C) / 12));
+ :set WeekDay ($WeekDay - (($WeekDay / 7) * 7));
+
+ :return (($DailyPskSecrets->0->(($Date->"day") - 1)) . \
+ ($DailyPskSecrets->1->(($Date->"month") - 1)) . \
+ ($DailyPskSecrets->2->$WeekDay));
+ }
+
+ :local Seen ({});
+ :local Date [ /system/clock/get date ];
+ :local NewPsk [ $GeneratePSK $Date ];
+
+ :foreach AccList in=[ /caps-man/access-list/find where comment~$DailyPskMatchComment ] do={
+ :foreach AccList in=[ /interface/wifi/access-list/find where comment~$DailyPskMatchComment ] do={
+ :foreach AccList in=[ /interface/wireless/access-list/find where comment~$DailyPskMatchComment ] do={
+ :local SsidRegExp [ /caps-man/access-list/get $AccList ssid-regexp ];
+ :local SsidRegExp [ /interface/wifi/access-list/get $AccList ssid-regexp ];
+ :local Configuration ([ /caps-man/configuration/find where ssid~$SsidRegExp ]->0);
+ :local Configuration ([ /interface/wifi/configuration/find where ssid~$SsidRegExp ]->0);
+ :local Ssid [ /caps-man/configuration/get $Configuration ssid ];
+ :local Ssid [ /interface/wifi/configuration/get $Configuration ssid ];
+ :local OldPsk [ /caps-man/access-list/get $AccList private-passphrase ];
+ :local OldPsk [ /interface/wifi/access-list/get $AccList passphrase ];
+ # /caps-man/ /interface/wifi/ above - /interface/wireless/ below
+ :local IntName [ /interface/wireless/access-list/get $AccList interface ];
+ :local Ssid [ /interface/wireless/get $IntName ssid ];
+ :local OldPsk [ /interface/wireless/access-list/get $AccList private-pre-shared-key ];
+ :local Skip 0;
+
+ :if ($NewPsk != $OldPsk) do={
+ $LogPrint info $ScriptName ("Updating daily PSK for " . $Ssid . " to " . $NewPsk . " (was " . $OldPsk . ")");
+ /caps-man/access-list/set $AccList private-passphrase=$NewPsk;
+ /interface/wifi/access-list/set $AccList passphrase=$NewPsk;
+ /interface/wireless/access-list/set $AccList private-pre-shared-key=$NewPsk;
+
+ :if ([ :len [ /caps-man/actual-interface-configuration/find where configuration.ssid=$Ssid !disabled ] ] > 0) do={
+ :if ([ :len [ /interface/wifi/actual-configuration/find where configuration.ssid=$Ssid ] ] > 0) do={
+ :if ([ :len [ /interface/wireless/find where name=$IntName !disabled ] ] = 1) do={
+ :if ($Seen->$Ssid = 1) do={
+ $LogPrint debug $ScriptName ("Already sent a mail for SSID " . $Ssid . ", skipping.");
+ } else={
+ :local Link ($DailyPskQrCodeUrl . \
+ "?scale=8&level=1&ssid=" . [ $UrlEncode $Ssid ] . "&pass=" . [ $UrlEncode $NewPsk ]);
+ $SendNotification2 ({ origin=$ScriptName; \
+ subject=([ $SymbolForNotification "calendar" ] . "daily PSK " . $Ssid); \
+ message=("This is the daily PSK on " . $Identity . ":\n\n" . \
+ [ $FormatLine "SSID" $Ssid ] . "\n" . \
+ [ $FormatLine "PSK" $NewPsk ] . "\n" . \
+ [ $FormatLine "Date" $Date ] . "\n\n" . \
+ "A client device specific rule must not exist!"); link=$Link });
+ :set ($Seen->$Ssid) 1;
+ }
+ }
+ }
+ }
+} on-error={ }
diff --git a/daily-psk.wifi.rsc b/daily-psk.wifi.rsc
new file mode 100644
index 0000000..9d7f285
--- /dev/null
+++ b/daily-psk.wifi.rsc
@@ -0,0 +1,92 @@
+#!rsc by RouterOS
+# RouterOS script: daily-psk.wifi
+# Copyright (c) 2013-2024 Christian Hesse <mail@eworm.de>
+# Michael Gisbers <michael@gisbers.de>
+# https://git.eworm.de/cgit/routeros-scripts/about/COPYING.md
+#
+# requires RouterOS, version=7.13
+#
+# update daily PSK (pre shared key)
+# https://git.eworm.de/cgit/routeros-scripts/about/doc/daily-psk.md
+#
+# !! Do not edit this file, it is generated from template!
+
+:global GlobalFunctionsReady;
+:while ($GlobalFunctionsReady != true) do={ :delay 500ms; }
+
+:do {
+ :local ScriptName [ :jobname ];
+
+ :global DailyPskMatchComment;
+ :global DailyPskQrCodeUrl;
+ :global Identity;
+
+ :global FormatLine;
+ :global LogPrint;
+ :global ScriptLock;
+ :global SendNotification2;
+ :global SymbolForNotification;
+ :global UrlEncode;
+ :global WaitForFile;
+ :global WaitFullyConnected;
+
+ :if ([ $ScriptLock $ScriptName ] = false) do={
+ :error false;
+ }
+ $WaitFullyConnected;
+
+ # return pseudo-random string for PSK
+ :local GeneratePSK do={
+ :local Date [ :tostr $1 ];
+
+ :global DailyPskSecrets;
+
+ :global ParseDate;
+
+ :set Date [ $ParseDate $Date ];
+
+ :local A ((14 - ($Date->"month")) / 12);
+ :local B (($Date->"year") - $A);
+ :local C (($Date->"month") + 12 * $A - 2);
+ :local WeekDay (7000 + ($Date->"day") + $B + ($B / 4) - ($B / 100) + ($B / 400) + ((31 * $C) / 12));
+ :set WeekDay ($WeekDay - (($WeekDay / 7) * 7));
+
+ :return (($DailyPskSecrets->0->(($Date->"day") - 1)) . \
+ ($DailyPskSecrets->1->(($Date->"month") - 1)) . \
+ ($DailyPskSecrets->2->$WeekDay));
+ }
+
+ :local Seen ({});
+ :local Date [ /system/clock/get date ];
+ :local NewPsk [ $GeneratePSK $Date ];
+
+ :foreach AccList in=[ /interface/wifi/access-list/find where comment~$DailyPskMatchComment ] do={
+ :local SsidRegExp [ /interface/wifi/access-list/get $AccList ssid-regexp ];
+ :local Configuration ([ /interface/wifi/configuration/find where ssid~$SsidRegExp ]->0);
+ :local Ssid [ /interface/wifi/configuration/get $Configuration ssid ];
+ :local OldPsk [ /interface/wifi/access-list/get $AccList passphrase ];
+ :local Skip 0;
+
+ :if ($NewPsk != $OldPsk) do={
+ $LogPrint info $ScriptName ("Updating daily PSK for " . $Ssid . " to " . $NewPsk . " (was " . $OldPsk . ")");
+ /interface/wifi/access-list/set $AccList passphrase=$NewPsk;
+
+ :if ([ :len [ /interface/wifi/actual-configuration/find where configuration.ssid=$Ssid ] ] > 0) do={
+ :if ($Seen->$Ssid = 1) do={
+ $LogPrint debug $ScriptName ("Already sent a mail for SSID " . $Ssid . ", skipping.");
+ } else={
+ :local Link ($DailyPskQrCodeUrl . \
+ "?scale=8&level=1&ssid=" . [ $UrlEncode $Ssid ] . "&pass=" . [ $UrlEncode $NewPsk ]);
+ $SendNotification2 ({ origin=$ScriptName; \
+ subject=([ $SymbolForNotification "calendar" ] . "daily PSK " . $Ssid); \
+ message=("This is the daily PSK on " . $Identity . ":\n\n" . \
+ [ $FormatLine "SSID" $Ssid ] . "\n" . \
+ [ $FormatLine "PSK" $NewPsk ] . "\n" . \
+ [ $FormatLine "Date" $Date ] . "\n\n" . \
+ "A client device specific rule must not exist!"); link=$Link });
+ :set ($Seen->$Ssid) 1;
+ }
+ }
+ }
+ }
+} on-error={ }
diff --git a/dhcp-lease-comment.capsman b/dhcp-lease-comment.capsman
deleted file mode 100644
index e5da51f..0000000
--- a/dhcp-lease-comment.capsman
+++ /dev/null
@@ -1,20 +0,0 @@
-#!rsc
-# RouterOS script: dhcp-lease-comment.capsman
-# Copyright (c) 2013-2020 Christian Hesse <mail@eworm.de>
-#
-# update dhcp-server lease comment with infos from access-list
-#
-# !! Do not edit this file, it is generated from template!
-
-:foreach Lease in=[ / ip dhcp-server lease find where dynamic=yes status=bound ] do={
- :local LeaseVal [ / ip dhcp-server lease get $Lease ];
- :local NewComment;
- :local AccessList ([ / caps-man access-list find where mac-address=($LeaseVal->"mac-address") ]->0);
- :if ([ :len $AccessList ] > 0) do={
- :set NewComment [ / caps-man access-list get $AccessList comment ];
- }
- :if ([ :len $NewComment ] != 0 && $LeaseVal->"comment" != $NewComment) do={
- :log info ("Updating comment for DHCP lease " . $LeaseVal->"mac-address" . ": " . $NewComment);
- / ip dhcp-server lease set comment=$NewComment $Lease;
- }
-}
diff --git a/dhcp-lease-comment.capsman.rsc b/dhcp-lease-comment.capsman.rsc
new file mode 100644
index 0000000..b7f3589
--- /dev/null
+++ b/dhcp-lease-comment.capsman.rsc
@@ -0,0 +1,39 @@
+#!rsc by RouterOS
+# RouterOS script: dhcp-lease-comment.capsman
+# Copyright (c) 2013-2024 Christian Hesse <mail@eworm.de>
+# https://git.eworm.de/cgit/routeros-scripts/about/COPYING.md
+#
+# provides: lease-script, order=60
+# requires RouterOS, version=7.13
+#
+# update dhcp-server lease comment with infos from access-list
+# https://git.eworm.de/cgit/routeros-scripts/about/doc/dhcp-lease-comment.md
+#
+# !! Do not edit this file, it is generated from template!
+
+:global GlobalFunctionsReady;
+:while ($GlobalFunctionsReady != true) do={ :delay 500ms; }
+
+:do {
+ :local ScriptName [ :jobname ];
+
+ :global LogPrint;
+ :global ScriptLock;
+
+ :if ([ $ScriptLock $ScriptName ] = false) do={
+ :error false;
+ }
+
+ :foreach Lease in=[ /ip/dhcp-server/lease/find where dynamic=yes status=bound ] do={
+ :local LeaseVal [ /ip/dhcp-server/lease/get $Lease ];
+ :local NewComment;
+ :local AccessList ([ /caps-man/access-list/find where mac-address=($LeaseVal->"active-mac-address") ]->0);
+ :if ([ :len $AccessList ] > 0) do={
+ :set NewComment [ /caps-man/access-list/get $AccessList comment ];
+ }
+ :if ([ :len $NewComment ] != 0 && $LeaseVal->"comment" != $NewComment) do={
+ $LogPrint info $ScriptName ("Updating comment for DHCP lease " . $LeaseVal->"active-mac-address" . ": " . $NewComment);
+ /ip/dhcp-server/lease/set comment=$NewComment $Lease;
+ }
+ }
+} on-error={ }
diff --git a/dhcp-lease-comment.local b/dhcp-lease-comment.local
deleted file mode 100644
index 367ca0b..0000000
--- a/dhcp-lease-comment.local
+++ /dev/null
@@ -1,20 +0,0 @@
-#!rsc
-# RouterOS script: dhcp-lease-comment.local
-# Copyright (c) 2013-2020 Christian Hesse <mail@eworm.de>
-#
-# update dhcp-server lease comment with infos from access-list
-#
-# !! Do not edit this file, it is generated from template!
-
-:foreach Lease in=[ / ip dhcp-server lease find where dynamic=yes status=bound ] do={
- :local LeaseVal [ / ip dhcp-server lease get $Lease ];
- :local NewComment;
- :local AccessList ([ / interface wireless access-list find where mac-address=($LeaseVal->"mac-address") ]->0);
- :if ([ :len $AccessList ] > 0) do={
- :set NewComment [ / interface wireless access-list get $AccessList comment ];
- }
- :if ([ :len $NewComment ] != 0 && $LeaseVal->"comment" != $NewComment) do={
- :log info ("Updating comment for DHCP lease " . $LeaseVal->"mac-address" . ": " . $NewComment);
- / ip dhcp-server lease set comment=$NewComment $Lease;
- }
-}
diff --git a/dhcp-lease-comment.local.rsc b/dhcp-lease-comment.local.rsc
new file mode 100644
index 0000000..e35bbe7
--- /dev/null
+++ b/dhcp-lease-comment.local.rsc
@@ -0,0 +1,39 @@
+#!rsc by RouterOS
+# RouterOS script: dhcp-lease-comment.local
+# Copyright (c) 2013-2024 Christian Hesse <mail@eworm.de>
+# https://git.eworm.de/cgit/routeros-scripts/about/COPYING.md
+#
+# provides: lease-script, order=60
+# requires RouterOS, version=7.13
+#
+# update dhcp-server lease comment with infos from access-list
+# https://git.eworm.de/cgit/routeros-scripts/about/doc/dhcp-lease-comment.md
+#
+# !! Do not edit this file, it is generated from template!
+
+:global GlobalFunctionsReady;
+:while ($GlobalFunctionsReady != true) do={ :delay 500ms; }
+
+:do {
+ :local ScriptName [ :jobname ];
+
+ :global LogPrint;
+ :global ScriptLock;
+
+ :if ([ $ScriptLock $ScriptName ] = false) do={
+ :error false;
+ }
+
+ :foreach Lease in=[ /ip/dhcp-server/lease/find where dynamic=yes status=bound ] do={
+ :local LeaseVal [ /ip/dhcp-server/lease/get $Lease ];
+ :local NewComment;
+ :local AccessList ([ /interface/wireless/access-list/find where mac-address=($LeaseVal->"active-mac-address") ]->0);
+ :if ([ :len $AccessList ] > 0) do={
+ :set NewComment [ /interface/wireless/access-list/get $AccessList comment ];
+ }
+ :if ([ :len $NewComment ] != 0 && $LeaseVal->"comment" != $NewComment) do={
+ $LogPrint info $ScriptName ("Updating comment for DHCP lease " . $LeaseVal->"active-mac-address" . ": " . $NewComment);
+ /ip/dhcp-server/lease/set comment=$NewComment $Lease;
+ }
+ }
+} on-error={ }
diff --git a/dhcp-lease-comment.template b/dhcp-lease-comment.template
deleted file mode 100644
index 3734ba4..0000000
--- a/dhcp-lease-comment.template
+++ /dev/null
@@ -1,21 +0,0 @@
-#!rsc
-# RouterOS script: dhcp-lease-comment%TEMPL%
-# Copyright (c) 2013-2020 Christian Hesse <mail@eworm.de>
-#
-# update dhcp-server lease comment with infos from access-list
-#
-# !! This is just a template! Replace '%PATH%' with 'caps-man'
-# !! or 'interface wireless'!
-
-:foreach Lease in=[ / ip dhcp-server lease find where dynamic=yes status=bound ] do={
- :local LeaseVal [ / ip dhcp-server lease get $Lease ];
- :local NewComment;
- :local AccessList ([ / %PATH% access-list find where mac-address=($LeaseVal->"mac-address") ]->0);
- :if ([ :len $AccessList ] > 0) do={
- :set NewComment [ / %PATH% access-list get $AccessList comment ];
- }
- :if ([ :len $NewComment ] != 0 && $LeaseVal->"comment" != $NewComment) do={
- :log info ("Updating comment for DHCP lease " . $LeaseVal->"mac-address" . ": " . $NewComment);
- / ip dhcp-server lease set comment=$NewComment $Lease;
- }
-}
diff --git a/dhcp-lease-comment.template.rsc b/dhcp-lease-comment.template.rsc
new file mode 100644
index 0000000..d4323ad
--- /dev/null
+++ b/dhcp-lease-comment.template.rsc
@@ -0,0 +1,44 @@
+#!rsc by RouterOS
+# RouterOS script: dhcp-lease-comment%TEMPL%
+# Copyright (c) 2013-2024 Christian Hesse <mail@eworm.de>
+# https://git.eworm.de/cgit/routeros-scripts/about/COPYING.md
+#
+# provides: lease-script, order=60
+# requires RouterOS, version=7.13
+#
+# update dhcp-server lease comment with infos from access-list
+# https://git.eworm.de/cgit/routeros-scripts/about/doc/dhcp-lease-comment.md
+#
+# !! This is just a template to generate the real script!
+# !! Pattern '%TEMPL%' is replaced, paths are filtered.
+
+:global GlobalFunctionsReady;
+:while ($GlobalFunctionsReady != true) do={ :delay 500ms; }
+
+:do {
+ :local ScriptName [ :jobname ];
+
+ :global LogPrint;
+ :global ScriptLock;
+
+ :if ([ $ScriptLock $ScriptName ] = false) do={
+ :error false;
+ }
+
+ :foreach Lease in=[ /ip/dhcp-server/lease/find where dynamic=yes status=bound ] do={
+ :local LeaseVal [ /ip/dhcp-server/lease/get $Lease ];
+ :local NewComment;
+ :local AccessList ([ /caps-man/access-list/find where mac-address=($LeaseVal->"active-mac-address") ]->0);
+ :local AccessList ([ /interface/wifi/access-list/find where mac-address=($LeaseVal->"active-mac-address") ]->0);
+ :local AccessList ([ /interface/wireless/access-list/find where mac-address=($LeaseVal->"active-mac-address") ]->0);
+ :if ([ :len $AccessList ] > 0) do={
+ :set NewComment [ /caps-man/access-list/get $AccessList comment ];
+ :set NewComment [ /interface/wifi/access-list/get $AccessList comment ];
+ :set NewComment [ /interface/wireless/access-list/get $AccessList comment ];
+ }
+ :if ([ :len $NewComment ] != 0 && $LeaseVal->"comment" != $NewComment) do={
+ $LogPrint info $ScriptName ("Updating comment for DHCP lease " . $LeaseVal->"active-mac-address" . ": " . $NewComment);
+ /ip/dhcp-server/lease/set comment=$NewComment $Lease;
+ }
+ }
+} on-error={ }
diff --git a/dhcp-lease-comment.wifi.rsc b/dhcp-lease-comment.wifi.rsc
new file mode 100644
index 0000000..f67ce6e
--- /dev/null
+++ b/dhcp-lease-comment.wifi.rsc
@@ -0,0 +1,39 @@
+#!rsc by RouterOS
+# RouterOS script: dhcp-lease-comment.wifi
+# Copyright (c) 2013-2024 Christian Hesse <mail@eworm.de>
+# https://git.eworm.de/cgit/routeros-scripts/about/COPYING.md
+#
+# provides: lease-script, order=60
+# requires RouterOS, version=7.13
+#
+# update dhcp-server lease comment with infos from access-list
+# https://git.eworm.de/cgit/routeros-scripts/about/doc/dhcp-lease-comment.md
+#
+# !! Do not edit this file, it is generated from template!
+
+:global GlobalFunctionsReady;
+:while ($GlobalFunctionsReady != true) do={ :delay 500ms; }
+
+:do {
+ :local ScriptName [ :jobname ];
+
+ :global LogPrint;
+ :global ScriptLock;
+
+ :if ([ $ScriptLock $ScriptName ] = false) do={
+ :error false;
+ }
+
+ :foreach Lease in=[ /ip/dhcp-server/lease/find where dynamic=yes status=bound ] do={
+ :local LeaseVal [ /ip/dhcp-server/lease/get $Lease ];
+ :local NewComment;
+ :local AccessList ([ /interface/wifi/access-list/find where mac-address=($LeaseVal->"active-mac-address") ]->0);
+ :if ([ :len $AccessList ] > 0) do={
+ :set NewComment [ /interface/wifi/access-list/get $AccessList comment ];
+ }
+ :if ([ :len $NewComment ] != 0 && $LeaseVal->"comment" != $NewComment) do={
+ $LogPrint info $ScriptName ("Updating comment for DHCP lease " . $LeaseVal->"active-mac-address" . ": " . $NewComment);
+ /ip/dhcp-server/lease/set comment=$NewComment $Lease;
+ }
+ }
+} on-error={ }
diff --git a/dhcp-to-dns b/dhcp-to-dns
deleted file mode 100644
index 5343513..0000000
--- a/dhcp-to-dns
+++ /dev/null
@@ -1,66 +0,0 @@
-#!rsc
-# RouterOS script: dhcp-to-dns
-# Copyright (c) 2013-2020 Christian Hesse <mail@eworm.de>
-#
-# check DHCP leases and add/remove/update DNS entries
-
-:global CharacterReplace;
-
-:global Identity;
-:global Domain;
-:global HostNameInZone;
-
-:local Zone;
-:if ($HostNameInZone = true) do={
- :set Zone ("dhcp." . $Identity . "." . $Domain);
-} else={
- :set Zone ("dhcp." . $Domain);
-}
-:local Ttl 5m;
-:local CommentPrefix "managed by dhcp-to-dns for ";
-
-:foreach DnsRecord in=[ / ip dns static find where comment ~ $CommentPrefix ] do={
- :local DnsRecordVal [ / ip dns static get $DnsRecord ];
- :local MacAddress [ $CharacterReplace ($DnsRecordVal->"comment") $CommentPrefix "" ];
- :if ([ / ip dhcp-server lease print count-only where mac-address=$MacAddress address=($DnsRecordVal->"address") dynamic=yes status=bound ] > 0) do={
- :log debug ("Lease for " . $MacAddress . " (" . $DnsRecordVal->"host-name" . ") still exists. Not deleting DNS entry.");
- } else={
- :local Found false;
- :log info ("Lease expired for " . $MacAddress . " (" . $DnsRecordVal->"name" . "), deleting DNS entry.");
- / ip dns static remove $DnsRecord;
- }
-}
-
-:foreach Lease in=[ / ip dhcp-server lease find where dynamic=yes status=bound ] do={
- :local LeaseVal [ / ip dhcp-server lease get $Lease ];
- :local Comment ($CommentPrefix . $LeaseVal->"mac-address");
- :local HostName [ $CharacterReplace ($LeaseVal->"host-name") " " "" ];
- :if ($HostName = "") do={
- :set HostName [ $CharacterReplace ($LeaseVal->"mac-address") ":" "-" ];
- }
-
- :local Fqdn ($HostName . "." . $Zone);
- :local DnsRecord [ / ip dns static find where name=$Fqdn ];
- :if ([ :len $DnsRecord ] > 0) do={
- :local DnsIp [ / ip dns static get $DnsRecord address ];
-
- :local DupMacLeases [ / ip dhcp-server lease find where mac-address=($LeaseVal->"mac-address") dynamic=yes status=bound ];
- :if ([ :len $DupMacLeases ] > 1) do={
- :set ($LeaseVal->"address") [ / ip dhcp-server lease get ($DupMacLeases->([ :len $DupMacLeases ] - 1)) address ];
- }
-
- :if ([ :len ($LeaseVal->"host-name") ] > 0) do={
- :set ($LeaseVal->"address") [ / ip dhcp-server lease get ([ find where host-name=($LeaseVal->"host-name") dynamic=yes status=bound ]->0) address ];
- }
-
- :if ($DnsIp = $LeaseVal->"address") do={
- :log debug ("DNS entry for " . $Fqdn . " does not need updating.");
- } else={
- :log info ("Replacing DNS entry for " . $Fqdn . ", new address is " . $LeaseVal->"address" . ".");
- / ip dns static set name=$Fqdn address=($LeaseVal->"address") ttl=$Ttl comment=$Comment $DnsRecord;
- }
- } else={
- :log info ("Adding new DNS entry for " . $Fqdn . ", address is " . $LeaseVal->"address" . ".");
- / ip dns static add name=$Fqdn address=($LeaseVal->"address") ttl=$Ttl comment=$Comment;
- }
-}
diff --git a/dhcp-to-dns.rsc b/dhcp-to-dns.rsc
new file mode 100644
index 0000000..a3d41c9
--- /dev/null
+++ b/dhcp-to-dns.rsc
@@ -0,0 +1,126 @@
+#!rsc by RouterOS
+# RouterOS script: dhcp-to-dns
+# Copyright (c) 2013-2024 Christian Hesse <mail@eworm.de>
+# https://git.eworm.de/cgit/routeros-scripts/about/COPYING.md
+#
+# provides: lease-script, order=20
+# requires RouterOS, version=7.13
+#
+# check DHCP leases and add/remove/update DNS entries
+# https://git.eworm.de/cgit/routeros-scripts/about/doc/dhcp-to-dns.md
+
+:global GlobalFunctionsReady;
+:while ($GlobalFunctionsReady != true) do={ :delay 500ms; }
+
+:do {
+ :local ScriptName [ :jobname ];
+
+ :global Domain;
+ :global Identity;
+
+ :global CleanName;
+ :global EitherOr;
+ :global IfThenElse;
+ :global LogPrint;
+ :global LogPrintOnce;
+ :global ParseKeyValueStore;
+ :global ScriptLock;
+
+ :if ([ $ScriptLock $ScriptName 10 ] = false) do={
+ :error false;
+ }
+
+ :local Ttl 5m;
+ :local CommentPrefix ("managed by " . $ScriptName);
+ :local CommentString ("--- " . $ScriptName . " above ---");
+
+ :if ([ :len [ /ip/dns/static/find where (name=$CommentString or (comment=$CommentString and name=-)) type=NXDOMAIN disabled ] ] = 0) do={
+ /ip/dns/static/add name=$CommentString type=NXDOMAIN disabled=yes;
+ $LogPrint warning $ScriptName ("Added disabled static dns record with name '" . $CommentString . "'.");
+ }
+ :local PlaceBefore ([ /ip/dns/static/find where (name=$CommentString or (comment=$CommentString and name=-)) type=NXDOMAIN disabled ]->0);
+
+ :foreach DnsRecord in=[ /ip/dns/static/find where comment~("^" . $CommentPrefix . "\\b") (!type or type=A) ] do={
+ :local DnsRecordVal [ /ip/dns/static/get $DnsRecord ];
+ :local DnsRecordInfo [ $ParseKeyValueStore ($DnsRecordVal->"comment") ];
+ :local MacInServer ($DnsRecordInfo->"macaddress" . " in " . $DnsRecordInfo->"server");
+
+ :if ([ :len [ /ip/dhcp-server/lease/find where active-mac-address=($DnsRecordInfo->"macaddress") \
+ active-address=($DnsRecordVal->"address") server=($DnsRecordInfo->"server") status=bound ] ] > 0) do={
+ $LogPrint debug $ScriptName ("Lease for " . $MacInServer . " (" . $DnsRecordVal->"name" . ") still exists. Not deleting record.");
+ } else={
+ :local Found false;
+ $LogPrint info $ScriptName ("Lease expired for " . $MacInServer . ", deleting record (" . $DnsRecordVal->"name" . ").");
+ /ip/dns/static/remove $DnsRecord;
+ /ip/dns/static/remove [ find where type=CNAME comment=($DnsRecordVal->"comment") ];
+ }
+ }
+
+ :foreach Lease in=[ /ip/dhcp-server/lease/find where status=bound ] do={
+ :local LeaseVal;
+ :do {
+ :set LeaseVal [ /ip/dhcp-server/lease/get $Lease ];
+ :if ([ :len [ /ip/dhcp-server/lease/find where active-mac-address=($LeaseVal->"active-mac-address") status=bound ] ] > 1) do={
+ $LogPrintOnce info $ScriptName ("Multiple bound leases found for mac-address " . ($LeaseVal->"active-mac-address") . "!");
+ }
+ } on-error={
+ $LogPrint debug $ScriptName ("A lease just vanished, ignoring.");
+ }
+
+ :if ([ :len ($LeaseVal->"active-address") ] > 0) do={
+ :local Comment ($CommentPrefix . ", macaddress=" . $LeaseVal->"active-mac-address" . ", server=" . $LeaseVal->"server");
+ :local MacDash [ $CleanName ($LeaseVal->"active-mac-address") ];
+ :local HostName [ $CleanName [ $EitherOr ([ $ParseKeyValueStore ($LeaseVal->"comment") ]->"hostname") ($LeaseVal->"host-name") ] ];
+ :local Network [ /ip/dhcp-server/network/find where ($LeaseVal->"active-address") in address ];
+ :local NetworkVal;
+ :if ([ :len $Network ] > 0) do={
+ :set NetworkVal [ /ip/dhcp-server/network/get ($Network->0) ];
+ }
+ :local NetworkInfo [ $ParseKeyValueStore ($NetworkVal->"comment") ];
+ :local NetDomain ([ $IfThenElse ([ :len ($NetworkInfo->"name-extra") ] > 0) ($NetworkInfo->"name-extra" . ".") ] . \
+ [ $EitherOr [ $EitherOr ($NetworkInfo->"domain") ($NetworkVal->"domain") ] $Domain ]);
+ :local FullA ($MacDash . "." . $NetDomain);
+ :local FullCN ($HostName . "." . $NetDomain);
+ :local MacInServer ($LeaseVal->"active-mac-address" . " in " . $LeaseVal->"server");
+
+ :local DnsRecord [ /ip/dns/static/find where comment=$Comment (!type or type=A) ];
+ :if ([ :len $DnsRecord ] > 0) do={
+ :local DnsRecordVal [ /ip/dns/static/get $DnsRecord ];
+
+ :if ($DnsRecordVal->"address" = $LeaseVal->"active-address" && $DnsRecordVal->"name" = $FullA) do={
+ $LogPrint debug $ScriptName ("The A record for " . $MacInServer . " (" . $FullA . ") does not need updating.");
+ } else={
+ $LogPrint info $ScriptName ("Updating A record for " . $MacInServer . " (" . $FullA . " -> " . $LeaseVal->"active-address" . ").");
+ /ip/dns/static/set address=($LeaseVal->"active-address") name=$FullA $DnsRecord;
+ }
+
+ :local CName [ /ip/dns/static/find where comment=$Comment type=CNAME ];
+ :if ([ :len $CName ] > 0) do={
+ :local CNameVal [ /ip/dns/static/get $CName ];
+ :if ($CNameVal->"name" != $FullCN || $CNameVal->"cname" != $FullA) do={
+ $LogPrint info $ScriptName ("Deleting CNAME record with wrong data for " . $MacInServer . ".");
+ /ip/dns/static/remove $CName;
+ }
+ }
+ :if ([ :len $HostName ] > 0 && [ :len [ /ip/dns/static/find where name=$FullCN type=CNAME ] ] = 0) do={
+ $LogPrint info $ScriptName ("Adding CNAME record for " . $MacInServer . " (" . $FullCN . " -> " . $FullA . ").");
+ /ip/dns/static/add name=$FullCN type=CNAME cname=$FullA ttl=$Ttl comment=$Comment place-before=$PlaceBefore;
+ }
+
+ } else={
+ $LogPrint info $ScriptName ("Adding A record for " . $MacInServer . " (" . $FullA . " -> " . $LeaseVal->"active-address" . ").");
+ /ip/dns/static/add name=$FullA type=A address=($LeaseVal->"active-address") ttl=$Ttl comment=$Comment place-before=$PlaceBefore;
+ :if ([ :len $HostName ] > 0 && [ :len [ /ip/dns/static/find where name=$FullCN type=CNAME ] ] = 0) do={
+ $LogPrint info $ScriptName ("Adding CNAME record for " . $MacInServer . " (" . $FullCN . " -> " . $FullA . ").");
+ /ip/dns/static/add name=$FullCN type=CNAME cname=$FullA ttl=$Ttl comment=$Comment place-before=$PlaceBefore;
+ }
+ }
+
+ :if ([ :len [ /ip/dns/static/find where name=$FullA (!type or type=A) ] ] > 1) do={
+ $LogPrintOnce warning $ScriptName ("The name '" . $FullA . "' appeared in more than one A record!");
+ }
+ } else={
+ $LogPrint debug $ScriptName ("No address available... Ignoring.");
+ }
+ }
+} on-error={ }
diff --git a/doc/accesslist-duplicates.d/01-example.avif b/doc/accesslist-duplicates.d/01-example.avif
new file mode 100644
index 0000000..11b3fc5
--- /dev/null
+++ b/doc/accesslist-duplicates.d/01-example.avif
Binary files differ
diff --git a/doc/accesslist-duplicates.md b/doc/accesslist-duplicates.md
new file mode 100644
index 0000000..a1f9198
--- /dev/null
+++ b/doc/accesslist-duplicates.md
@@ -0,0 +1,57 @@
+Find and remove access list duplicates
+======================================
+
+[![GitHub stars](https://img.shields.io/github/stars/eworm-de/routeros-scripts?logo=GitHub&style=flat&color=red)](https://github.com/eworm-de/routeros-scripts/stargazers)
+[![GitHub forks](https://img.shields.io/github/forks/eworm-de/routeros-scripts?logo=GitHub&style=flat&color=green)](https://github.com/eworm-de/routeros-scripts/network)
+[![GitHub watchers](https://img.shields.io/github/watchers/eworm-de/routeros-scripts?logo=GitHub&style=flat&color=blue)](https://github.com/eworm-de/routeros-scripts/watchers)
+[![required RouterOS version](https://img.shields.io/badge/RouterOS-7.13-yellow?style=flat)](https://mikrotik.com/download/changelogs/)
+[![Telegram group @routeros_scripts](https://img.shields.io/badge/Telegram-%40routeros__scripts-%2326A5E4?logo=telegram&style=flat)](https://t.me/routeros_scripts)
+[![donate with PayPal](https://img.shields.io/badge/Like_it%3F-Donate!-orange?logo=githubsponsors&logoColor=orange&style=flat)](https://www.paypal.com/cgi-bin/webscr?cmd=_s-xclick&hosted_button_id=A4ZXBD6YS2W8J)
+
+[⬅️ Go back to main README](../README.md)
+
+> ℹ️️ **Info**: This script can not be used on its own but requires the base
+> installation. See [main README](../README.md) for details.
+
+Description
+-----------
+
+This script is supposed to run interactively to find and remove duplicate
+entries in wireless access list.
+
+Requirements and installation
+-----------------------------
+
+Depending on whether you use `wifi` package (`/interface/wifi`), legacy
+wifi with CAPsMAN (`/caps-man`) or local wireless interface
+(`/interface/wireless`) you need to install a different script.
+
+For `wifi`:
+
+ $ScriptInstallUpdate accesslist-duplicates.wifi;
+
+For legacy CAPsMAN:
+
+ $ScriptInstallUpdate accesslist-duplicates.capsman;
+
+For legacy local interface:
+
+ $ScriptInstallUpdate accesslist-duplicates.local;
+
+Usage and invocation
+--------------------
+
+Run this script from a terminal:
+
+ /system/script/run accesslist-duplicates.wifi;
+
+![screenshot: example](accesslist-duplicates.d/01-example.avif)
+
+See also
+--------
+
+* [Collect MAC addresses in wireless access list](collect-wireless-mac.md)
+
+---
+[⬅️ Go back to main README](../README.md)
+[⬆️ Go back to top](#top)
diff --git a/doc/backup-cloud.d/notification.avif b/doc/backup-cloud.d/notification.avif
new file mode 100644
index 0000000..e533908
--- /dev/null
+++ b/doc/backup-cloud.d/notification.avif
Binary files differ
diff --git a/doc/backup-cloud.md b/doc/backup-cloud.md
new file mode 100644
index 0000000..be6e06d
--- /dev/null
+++ b/doc/backup-cloud.md
@@ -0,0 +1,76 @@
+Upload backup to Mikrotik cloud
+===============================
+
+[![GitHub stars](https://img.shields.io/github/stars/eworm-de/routeros-scripts?logo=GitHub&style=flat&color=red)](https://github.com/eworm-de/routeros-scripts/stargazers)
+[![GitHub forks](https://img.shields.io/github/forks/eworm-de/routeros-scripts?logo=GitHub&style=flat&color=green)](https://github.com/eworm-de/routeros-scripts/network)
+[![GitHub watchers](https://img.shields.io/github/watchers/eworm-de/routeros-scripts?logo=GitHub&style=flat&color=blue)](https://github.com/eworm-de/routeros-scripts/watchers)
+[![required RouterOS version](https://img.shields.io/badge/RouterOS-7.13-yellow?style=flat)](https://mikrotik.com/download/changelogs/)
+[![Telegram group @routeros_scripts](https://img.shields.io/badge/Telegram-%40routeros__scripts-%2326A5E4?logo=telegram&style=flat)](https://t.me/routeros_scripts)
+[![donate with PayPal](https://img.shields.io/badge/Like_it%3F-Donate!-orange?logo=githubsponsors&logoColor=orange&style=flat)](https://www.paypal.com/cgi-bin/webscr?cmd=_s-xclick&hosted_button_id=A4ZXBD6YS2W8J)
+
+[⬅️ Go back to main README](../README.md)
+
+> ℹ️ **Info**: This script can not be used on its own but requires the base
+> installation. See [main README](../README.md) for details.
+
+Description
+-----------
+
+This script uploads
+[binary backup to Mikrotik cloud](https://wiki.mikrotik.com/wiki/Manual:IP/Cloud#Backup).
+
+> ⚠️ **Warning**: The used command can hit errors that a script can with
+> workaround only. A notification *should* be sent anyway. But it can result
+> in malfunction of fetch command (where all up- and downloads break) for
+> some time. Failed notifications are queued then.
+
+### Sample notification
+
+![backup-cloud notification](backup-cloud.d/notification.avif)
+
+Requirements and installation
+-----------------------------
+
+Just install the script:
+
+ $ScriptInstallUpdate backup-cloud;
+
+Configuration
+-------------
+
+The configuration goes to `global-config-overlay`, these are the parameters:
+
+* `BackupPassword`: password to encrypt the backup with
+* `BackupRandomDelay`: delay up to amount of seconds when run from scheduler
+
+> ℹ️ **Info**: Copy relevant configuration from
+> [`global-config`](../global-config.rsc) (the one without `-overlay`) to
+> your local `global-config-overlay` and modify it to your specific needs.
+
+Also notification settings are required for
+[e-mail](mod/notification-email.md),
+[matrix](mod/notification-matrix.md),
+[ntfy](mod/notification-ntfy.md) and/or
+[telegram](mod/notification-telegram.md).
+
+Usage and invocation
+--------------------
+
+Just run the script:
+
+ /system/script/run backup-cloud;
+
+Creating a scheduler may be an option:
+
+ /system/scheduler/add interval=1w name=backup-cloud on-event="/system/script/run backup-cloud;" start-time=09:20:00;
+
+See also
+--------
+
+* [Send backup via e-mail](backup-email.md)
+* [Save configuration to fallback partition](doc/backup-partition.md)
+* [Upload backup to server](backup-upload.md)
+
+---
+[⬅️ Go back to main README](../README.md)
+[⬆️ Go back to top](#top)
diff --git a/doc/backup-email.md b/doc/backup-email.md
new file mode 100644
index 0000000..a506543
--- /dev/null
+++ b/doc/backup-email.md
@@ -0,0 +1,68 @@
+Send backup via e-mail
+======================
+
+[![GitHub stars](https://img.shields.io/github/stars/eworm-de/routeros-scripts?logo=GitHub&style=flat&color=red)](https://github.com/eworm-de/routeros-scripts/stargazers)
+[![GitHub forks](https://img.shields.io/github/forks/eworm-de/routeros-scripts?logo=GitHub&style=flat&color=green)](https://github.com/eworm-de/routeros-scripts/network)
+[![GitHub watchers](https://img.shields.io/github/watchers/eworm-de/routeros-scripts?logo=GitHub&style=flat&color=blue)](https://github.com/eworm-de/routeros-scripts/watchers)
+[![required RouterOS version](https://img.shields.io/badge/RouterOS-7.13-yellow?style=flat)](https://mikrotik.com/download/changelogs/)
+[![Telegram group @routeros_scripts](https://img.shields.io/badge/Telegram-%40routeros__scripts-%2326A5E4?logo=telegram&style=flat)](https://t.me/routeros_scripts)
+[![donate with PayPal](https://img.shields.io/badge/Like_it%3F-Donate!-orange?logo=githubsponsors&logoColor=orange&style=flat)](https://www.paypal.com/cgi-bin/webscr?cmd=_s-xclick&hosted_button_id=A4ZXBD6YS2W8J)
+
+[⬅️ Go back to main README](../README.md)
+
+> ℹ️ **Info**: This script can not be used on its own but requires the base
+> installation. See [main README](../README.md) for details.
+
+Description
+-----------
+
+This script sends binary backup (`/system/backup/save`) and complete
+configuration export (`/export terse show-sensitive`) via e-mail.
+
+Requirements and installation
+-----------------------------
+
+Just install the script and the required module:
+
+ $ScriptInstallUpdate mod/notification-email,backup-email;
+
+Also make sure you configure
+[sending notifications via e-mail](mod/notification-email.md).
+
+Configuration
+-------------
+
+The configuration goes to `global-config-overlay`, these are the parameters:
+
+* `BackupSendBinary`: whether to send binary backup
+* `BackupSendExport`: whether to send configuration export
+* `BackupSendGlobalConfig`: whether to send `global-config-overlay`
+* `BackupPassword`: password to encrypt the backup with
+* `BackupRandomDelay`: delay up to amount of seconds when run from scheduler
+
+> ℹ️ **Info**: Copy relevant configuration from
+> [`global-config`](../global-config.rsc) (the one without `-overlay`) to
+> your local `global-config-overlay` and modify it to your specific needs.
+
+Usage and invocation
+--------------------
+
+Just run the script:
+
+ /system/script/run backup-email;
+
+Creating a scheduler may be an option:
+
+ /system/scheduler/add interval=1w name=backup-email on-event="/system/script/run backup-email;" start-time=09:15:00;
+
+See also
+--------
+
+* [Upload backup to Mikrotik cloud](backup-cloud.md)
+* [Save configuration to fallback partition](doc/backup-partition.md)
+* [Send notifications via e-mail](mod/notification-email.md)
+* [Upload backup to server](backup-upload.md)
+
+---
+[⬅️ Go back to main README](../README.md)
+[⬆️ Go back to top](#top)
diff --git a/doc/backup-partition.md b/doc/backup-partition.md
new file mode 100644
index 0000000..ba20657
--- /dev/null
+++ b/doc/backup-partition.md
@@ -0,0 +1,61 @@
+Save configuration to fallback partition
+========================================
+
+[![GitHub stars](https://img.shields.io/github/stars/eworm-de/routeros-scripts?logo=GitHub&style=flat&color=red)](https://github.com/eworm-de/routeros-scripts/stargazers)
+[![GitHub forks](https://img.shields.io/github/forks/eworm-de/routeros-scripts?logo=GitHub&style=flat&color=green)](https://github.com/eworm-de/routeros-scripts/network)
+[![GitHub watchers](https://img.shields.io/github/watchers/eworm-de/routeros-scripts?logo=GitHub&style=flat&color=blue)](https://github.com/eworm-de/routeros-scripts/watchers)
+[![required RouterOS version](https://img.shields.io/badge/RouterOS-7.13-yellow?style=flat)](https://mikrotik.com/download/changelogs/)
+[![Telegram group @routeros_scripts](https://img.shields.io/badge/Telegram-%40routeros__scripts-%2326A5E4?logo=telegram&style=flat)](https://t.me/routeros_scripts)
+[![donate with PayPal](https://img.shields.io/badge/Like_it%3F-Donate!-orange?logo=githubsponsors&logoColor=orange&style=flat)](https://www.paypal.com/cgi-bin/webscr?cmd=_s-xclick&hosted_button_id=A4ZXBD6YS2W8J)
+
+[⬅️ Go back to main README](../README.md)
+
+> ℹ️ **Info**: This script can not be used on its own but requires the base
+> installation. See [main README](../README.md) for details.
+
+Description
+-----------
+
+This script saves the current configuration to fallback
+[partition](https://wiki.mikrotik.com/wiki/Manual:Partitions).
+
+For this to work you need a device with sufficient flash storage that is
+properly partitioned.
+
+To make you aware of a possible issue a scheduler logging a warning is
+added in the backup partition's configuration. You may want to use
+[log-forward](log-forward.md) to be notified.
+
+> ⚠️ **Warning**: Only the configuration is saved to backup partition.
+> Every now and then you should copy your installation over for a recent
+> RouterOS version!
+
+Requirements and installation
+-----------------------------
+
+Just install the script:
+
+ $ScriptInstallUpdate backup-partition;
+
+Usage and invocation
+--------------------
+
+Just run the script:
+
+ /system/script/run backup-partition;
+
+Creating a scheduler may be an option:
+
+ /system/scheduler/add interval=1w name=backup-partition on-event="/system/script/run backup-partition;" start-time=09:30:00;
+
+See also
+--------
+
+* [Upload backup to Mikrotik cloud](backup-cloud.md)
+* [Send backup via e-mail](backup-email.md)
+* [Upload backup to server](backup-upload.md)
+* [Forward log messages via notification](log-forward.md)
+
+---
+[⬅️ Go back to main README](../README.md)
+[⬆️ Go back to top](#top)
diff --git a/doc/backup-upload.d/notification.avif b/doc/backup-upload.d/notification.avif
new file mode 100644
index 0000000..83cfb18
--- /dev/null
+++ b/doc/backup-upload.d/notification.avif
Binary files differ
diff --git a/doc/backup-upload.md b/doc/backup-upload.md
new file mode 100644
index 0000000..f524adb
--- /dev/null
+++ b/doc/backup-upload.md
@@ -0,0 +1,92 @@
+Upload backup to server
+=======================
+
+[![GitHub stars](https://img.shields.io/github/stars/eworm-de/routeros-scripts?logo=GitHub&style=flat&color=red)](https://github.com/eworm-de/routeros-scripts/stargazers)
+[![GitHub forks](https://img.shields.io/github/forks/eworm-de/routeros-scripts?logo=GitHub&style=flat&color=green)](https://github.com/eworm-de/routeros-scripts/network)
+[![GitHub watchers](https://img.shields.io/github/watchers/eworm-de/routeros-scripts?logo=GitHub&style=flat&color=blue)](https://github.com/eworm-de/routeros-scripts/watchers)
+[![required RouterOS version](https://img.shields.io/badge/RouterOS-7.13-yellow?style=flat)](https://mikrotik.com/download/changelogs/)
+[![Telegram group @routeros_scripts](https://img.shields.io/badge/Telegram-%40routeros__scripts-%2326A5E4?logo=telegram&style=flat)](https://t.me/routeros_scripts)
+[![donate with PayPal](https://img.shields.io/badge/Like_it%3F-Donate!-orange?logo=githubsponsors&logoColor=orange&style=flat)](https://www.paypal.com/cgi-bin/webscr?cmd=_s-xclick&hosted_button_id=A4ZXBD6YS2W8J)
+
+[⬅️ Go back to main README](../README.md)
+
+> ℹ️ **Info**: This script can not be used on its own but requires the base
+> installation. See [main README](../README.md) for details.
+
+Description
+-----------
+
+This script uploads binary backup (`/system/backup/save`) and complete
+configuration export (`/export terse show-sensitive`) to external server.
+
+> ⚠️ **Warning**: The used command can hit errors that a script can not handle.
+> This may result in script termination (where no notification is sent) or
+> malfunction of fetch command (where all up- and downloads break) for some
+> time. Failed notifications are queued then.
+
+### Sample notification
+
+![backup-upload notification](backup-upload.d/notification.avif)
+
+Requirements and installation
+-----------------------------
+
+Just install the script:
+
+ $ScriptInstallUpdate backup-upload;
+
+Configuration
+-------------
+
+The configuration goes to `global-config-overlay`, these are the parameters:
+
+* `BackupSendBinary`: whether to send binary backup
+* `BackupSendExport`: whether to send configuration export
+* `BackupSendGlobalConfig`: whether to send `global-config-overlay`
+* `BackupPassword`: password to encrypt the backup with
+* `BackupRandomDelay`: delay up to amount of seconds when run from scheduler
+* `BackupUploadUrl`: url to upload to
+* `BackupUploadUser`: username for server authentication
+* `BackupUploadPass`: password for server authentication
+
+> ℹ️ **Info**: Copy relevant configuration from
+> [`global-config`](../global-config.rsc) (the one without `-overlay`) to
+> your local `global-config-overlay` and modify it to your specific needs.
+
+Also notification settings are required for
+[e-mail](mod/notification-email.md),
+[matrix](mod/notification-matrix.md),
+[ntfy](mod/notification-ntfy.md) and/or
+[telegram](mod/notification-telegram.md).
+
+### Issues with SFTP client
+
+The RouterOS SFTP client is picky if it comes to authentication methods.
+I had to disable all but password authentication on server side. For openssh
+edit `/etc/ssh/sshd_config` and add a directive like this, changed for your
+needs:
+
+ Match User mikrotik
+ AuthenticationMethods password
+
+Usage and invocation
+--------------------
+
+Just run the script:
+
+ /system/script/run backup-upload;
+
+Creating a scheduler may be an option:
+
+ /system/scheduler/add interval=1w name=backup-upload on-event="/system/script/run backup-upload;" start-time=09:25:00;
+
+See also
+--------
+
+* [Upload backup to Mikrotik cloud](backup-cloud.md)
+* [Send backup via e-mail](backup-email.md)
+* [Save configuration to fallback partition](doc/backup-partition.md)
+
+---
+[⬅️ Go back to main README](../README.md)
+[⬆️ Go back to top](#top)
diff --git a/doc/capsman-download-packages.md b/doc/capsman-download-packages.md
new file mode 100644
index 0000000..c68900e
--- /dev/null
+++ b/doc/capsman-download-packages.md
@@ -0,0 +1,83 @@
+Download packages for CAP upgrade from CAPsMAN
+=============================================
+
+[![GitHub stars](https://img.shields.io/github/stars/eworm-de/routeros-scripts?logo=GitHub&style=flat&color=red)](https://github.com/eworm-de/routeros-scripts/stargazers)
+[![GitHub forks](https://img.shields.io/github/forks/eworm-de/routeros-scripts?logo=GitHub&style=flat&color=green)](https://github.com/eworm-de/routeros-scripts/network)
+[![GitHub watchers](https://img.shields.io/github/watchers/eworm-de/routeros-scripts?logo=GitHub&style=flat&color=blue)](https://github.com/eworm-de/routeros-scripts/watchers)
+[![required RouterOS version](https://img.shields.io/badge/RouterOS-7.13-yellow?style=flat)](https://mikrotik.com/download/changelogs/)
+[![Telegram group @routeros_scripts](https://img.shields.io/badge/Telegram-%40routeros__scripts-%2326A5E4?logo=telegram&style=flat)](https://t.me/routeros_scripts)
+[![donate with PayPal](https://img.shields.io/badge/Like_it%3F-Donate!-orange?logo=githubsponsors&logoColor=orange&style=flat)](https://www.paypal.com/cgi-bin/webscr?cmd=_s-xclick&hosted_button_id=A4ZXBD6YS2W8J)
+
+[⬅️ Go back to main README](../README.md)
+
+> ℹ️ **Info**: This script can not be used on its own but requires the base
+> installation. See [main README](../README.md) for details.
+
+Description
+-----------
+
+CAPsMAN can upgrate CAP devices. If CAPsMAN device and CAP device(s) are
+differnet architecture you need to store packages for CAP device's
+architecture on local storage.
+
+This script automatically downloads these packages.
+
+Requirements and installation
+-----------------------------
+
+Make sure you have the `package-path` set in your CAPsMAN configuration,
+as that is where packages are downloaded to and where the system expects
+them.
+
+Then just install the script on CAPsMAN device.
+Depending on whether you use `wifi` package (`/interface/wifi`) or legacy
+wifi with CAPsMAN (`/caps-man`) you need to install a different script.
+
+For `wifi`:
+
+ $ScriptInstallUpdate capsman-download-packages.wifi;
+
+For legacy CAPsMAN:
+
+ $ScriptInstallUpdate capsman-download-packages.capsman;
+
+Optionally add a scheduler to run after startup. For `wifi`:
+
+ /system/scheduler/add name=capsman-download-packages on-event="/system/script/run capsman-download-packages.wifi;" start-time=startup;
+
+For legacy CAPsMAN:
+
+ /system/scheduler/add name=capsman-download-packages on-event="/system/script/run capsman-download-packages.capsman;" start-time=startup;
+
+Packages available in local storage in older version are downloaded
+unconditionally.
+
+If no packages are found the script downloads a default set of packages:
+
+ * `wifi`: `routeros` and `wifi-qcom` for *arm* and *arm64*, `wifi-qcom-ac` for *arm*
+ * legacy CAPsMAN: `routeros` and `wireless` for *arm* and *mipsbe*
+
+> ℹ️ **Info**: If you have packages in the directory and things go wrong for
+> what ever unknown reason: Remove **all** packages and start over.
+
+Usage and invocation
+--------------------
+
+Run the script manually:
+
+ /system/script/run capsman-download-packages.wifi;
+
+... or from scheduler.
+
+After package download all out-of-date CAP devices are upgraded automatically.
+For a rolling upgrade install extra script
+[capsman-rolling-upgrade](capsman-rolling-upgrade.md).
+
+See also
+--------
+
+* [Run rolling CAP upgrades from CAPsMAN](capsman-rolling-upgrade.md)
+
+---
+[⬅️ Go back to main README](../README.md)
+[⬆️ Go back to top](#top)
diff --git a/doc/capsman-rolling-upgrade.md b/doc/capsman-rolling-upgrade.md
new file mode 100644
index 0000000..27d855f
--- /dev/null
+++ b/doc/capsman-rolling-upgrade.md
@@ -0,0 +1,60 @@
+Run rolling CAP upgrades from CAPsMAN
+=====================================
+
+[![GitHub stars](https://img.shields.io/github/stars/eworm-de/routeros-scripts?logo=GitHub&style=flat&color=red)](https://github.com/eworm-de/routeros-scripts/stargazers)
+[![GitHub forks](https://img.shields.io/github/forks/eworm-de/routeros-scripts?logo=GitHub&style=flat&color=green)](https://github.com/eworm-de/routeros-scripts/network)
+[![GitHub watchers](https://img.shields.io/github/watchers/eworm-de/routeros-scripts?logo=GitHub&style=flat&color=blue)](https://github.com/eworm-de/routeros-scripts/watchers)
+[![required RouterOS version](https://img.shields.io/badge/RouterOS-7.13-yellow?style=flat)](https://mikrotik.com/download/changelogs/)
+[![Telegram group @routeros_scripts](https://img.shields.io/badge/Telegram-%40routeros__scripts-%2326A5E4?logo=telegram&style=flat)](https://t.me/routeros_scripts)
+[![donate with PayPal](https://img.shields.io/badge/Like_it%3F-Donate!-orange?logo=githubsponsors&logoColor=orange&style=flat)](https://www.paypal.com/cgi-bin/webscr?cmd=_s-xclick&hosted_button_id=A4ZXBD6YS2W8J)
+
+[⬅️ Go back to main README](../README.md)
+
+> ℹ️ **Info**: This script can not be used on its own but requires the base
+> installation. See [main README](../README.md) for details.
+
+Description
+-----------
+
+CAPsMAN can upgrade CAP devices. This script runs a rolling upgrade for
+out-of-date CAP devices. The idea is to have just a fraction of devices
+reboot at a time, having the others to serve wireless connectivity.
+
+Note that the script does not wait for the CAPs to reconnect, it just defers
+the upgrade commands. The more CAPs you have the more will upgrade in
+parallel.
+
+Requirements and installation
+-----------------------------
+
+Just install the script on CAPsMAN device.
+Depending on whether you use `wifi` package (`/interface/wifi`) or legacy
+wifi with CAPsMAN (`/caps-man`) you need to install a different script.
+
+For `wifi`:
+
+ $ScriptInstallUpdate capsman-rolling-upgrade.wifi;
+
+For legacy CAPsMAN:
+
+ $ScriptInstallUpdate capsman-rolling-upgrade.capsman;
+
+Usage and invocation
+--------------------
+
+This script is intended as an add-on to
+[capsman-download-packages](capsman-download-packages.md), being invoked by
+that script when required.
+
+Alternatively run it manually:
+
+ /system/script/run capsman-rolling-upgrade.wifi;
+
+See also
+--------
+
+* [Download packages for CAP upgrade from CAPsMAN](capsman-download-packages.md)
+
+---
+[⬅️ Go back to main README](../README.md)
+[⬆️ Go back to top](#top)
diff --git a/doc/certificate-renew-issued.md b/doc/certificate-renew-issued.md
new file mode 100644
index 0000000..91a1914
--- /dev/null
+++ b/doc/certificate-renew-issued.md
@@ -0,0 +1,61 @@
+Renew locally issued certificates
+=================================
+
+[![GitHub stars](https://img.shields.io/github/stars/eworm-de/routeros-scripts?logo=GitHub&style=flat&color=red)](https://github.com/eworm-de/routeros-scripts/stargazers)
+[![GitHub forks](https://img.shields.io/github/forks/eworm-de/routeros-scripts?logo=GitHub&style=flat&color=green)](https://github.com/eworm-de/routeros-scripts/network)
+[![GitHub watchers](https://img.shields.io/github/watchers/eworm-de/routeros-scripts?logo=GitHub&style=flat&color=blue)](https://github.com/eworm-de/routeros-scripts/watchers)
+[![required RouterOS version](https://img.shields.io/badge/RouterOS-7.13-yellow?style=flat)](https://mikrotik.com/download/changelogs/)
+[![Telegram group @routeros_scripts](https://img.shields.io/badge/Telegram-%40routeros__scripts-%2326A5E4?logo=telegram&style=flat)](https://t.me/routeros_scripts)
+[![donate with PayPal](https://img.shields.io/badge/Like_it%3F-Donate!-orange?logo=githubsponsors&logoColor=orange&style=flat)](https://www.paypal.com/cgi-bin/webscr?cmd=_s-xclick&hosted_button_id=A4ZXBD6YS2W8J)
+
+[⬅️ Go back to main README](../README.md)
+
+> ℹ️ **Info**: This script can not be used on its own but requires the base
+> installation. See [main README](../README.md) for details.
+
+Description
+-----------
+
+This script renews certificates issued by a local certificate authority (CA).
+Optionally the certificates are exported with individual passphrases for
+easy pick-up.
+
+Requirements and installation
+-----------------------------
+
+Just install the script:
+
+ $ScriptInstallUpdate certificate-renew-issued;
+
+Configuration
+-------------
+
+The configuration goes to `global-config-overlay`, there is just one
+parameter:
+
+* `CertRenewPass`: an array holding individual passphrases for certificates
+
+> ℹ️ **Info**: Copy relevant configuration from
+> [`global-config`](../global-config.rsc) (the one without `-overlay`) to
+> your local `global-config-overlay` and modify it to your specific needs.
+
+Usage and invocation
+--------------------
+
+Run the script to renew certificates issued from a local CA.
+
+ /system/script/run certificate-renew-issued;
+
+Only scripts with a remaining lifetime of three weeks or less are renewed.
+The old certificate is revoked automatically. If a passphrase for a specific
+certificate is given in `CertRenewPass` the certificate is exported and
+PKCS#12 file (`cert-issued/CN.p12`) can be found on device's storage.
+
+See also
+--------
+
+* [Renew certificates and notify on expiration](check-certificates.md)
+
+---
+[⬅️ Go back to main README](../README.md)
+[⬆️ Go back to top](#top)
diff --git a/doc/check-certificates.d/notification.avif b/doc/check-certificates.d/notification.avif
new file mode 100644
index 0000000..7c250da
--- /dev/null
+++ b/doc/check-certificates.d/notification.avif
Binary files differ
diff --git a/doc/check-certificates.md b/doc/check-certificates.md
new file mode 100644
index 0000000..636f719
--- /dev/null
+++ b/doc/check-certificates.md
@@ -0,0 +1,97 @@
+Renew certificates and notify on expiration
+===========================================
+
+[![GitHub stars](https://img.shields.io/github/stars/eworm-de/routeros-scripts?logo=GitHub&style=flat&color=red)](https://github.com/eworm-de/routeros-scripts/stargazers)
+[![GitHub forks](https://img.shields.io/github/forks/eworm-de/routeros-scripts?logo=GitHub&style=flat&color=green)](https://github.com/eworm-de/routeros-scripts/network)
+[![GitHub watchers](https://img.shields.io/github/watchers/eworm-de/routeros-scripts?logo=GitHub&style=flat&color=blue)](https://github.com/eworm-de/routeros-scripts/watchers)
+[![required RouterOS version](https://img.shields.io/badge/RouterOS-7.13-yellow?style=flat)](https://mikrotik.com/download/changelogs/)
+[![Telegram group @routeros_scripts](https://img.shields.io/badge/Telegram-%40routeros__scripts-%2326A5E4?logo=telegram&style=flat)](https://t.me/routeros_scripts)
+[![donate with PayPal](https://img.shields.io/badge/Like_it%3F-Donate!-orange?logo=githubsponsors&logoColor=orange&style=flat)](https://www.paypal.com/cgi-bin/webscr?cmd=_s-xclick&hosted_button_id=A4ZXBD6YS2W8J)
+
+[⬅️ Go back to main README](../README.md)
+
+> ℹ️ **Info**: This script can not be used on its own but requires the base
+> installation. See [main README](../README.md) for details.
+
+Description
+-----------
+
+This script tries to download and renew certificates, then notifies about
+certificates that are still about to expire.
+
+### Sample notification
+
+![check-certificates notification](check-certificates.d/notification.avif)
+
+Requirements and installation
+-----------------------------
+
+Just install the script:
+
+ $ScriptInstallUpdate check-certificates;
+
+Configuration
+-------------
+
+For automatic download and renewal of certificates you need configuration
+in `global-config-overlay`, these are the parameters:
+
+* `CertRenewPass`: an array of passphrases to try
+* `CertRenewTime`: on what remaining time to try a renew
+* `CertRenewUrl`: the url to download certificates from
+* `CertWarnTime`: on what remaining time to warn via notification
+
+> ℹ️ **Info**: Copy relevant configuration from
+> [`global-config`](../global-config.rsc) (the one without `-overlay`) to
+> your local `global-config-overlay` and modify it to your specific needs.
+
+Certificates on the web server should be named by their common name, like
+`CN.pem` (`PEM` format) or`CN.p12` (`PKCS#12` format). Alternatively any
+subject alternative name (aka *Subject Alt Name* or *SAN*) can be used.
+
+Also notification settings are required for
+[e-mail](mod/notification-email.md),
+[matrix](mod/notification-matrix.md),
+[ntfy](mod/notification-ntfy.md) and/or
+[telegram](mod/notification-telegram.md).
+
+Usage and invocation
+--------------------
+
+Just run the script:
+
+ /system/script/run check-certificates;
+
+... or create a scheduler for periodic execution:
+
+ /system/scheduler/add interval=1d name=check-certificates on-event="/system/script/run check-certificates;" start-time=startup;
+
+
+Tips & Tricks
+-------------
+
+### Schedule at startup
+
+The script checks for full connectivity before acting, so scheduling at
+startup is perfectly valid:
+
+ /system/scheduler/add name=check-certificates@startup on-event="/system/script/run check-certificates;" start-time=startup;
+
+### Initial import
+
+Given you have a certificate on you server, you can use `check-certificates`
+for the initial import. Just create a *dummy* certificate with short lifetime
+that matches criteria to be renewed:
+
+ /certificate/add name=example.com common-name=example.com days-valid=1;
+ /certificate/sign example.com;
+ /system/script/run check-certificates;
+
+See also
+--------
+
+* [Renew locally issued certificates](certificate-renew-issued.md)
+
+---
+[⬅️ Go back to main README](../README.md)
+[⬆️ Go back to top](#top)
diff --git a/doc/check-health.d/notification-01-cpu-utilization-high.avif b/doc/check-health.d/notification-01-cpu-utilization-high.avif
new file mode 100644
index 0000000..326e7fe
--- /dev/null
+++ b/doc/check-health.d/notification-01-cpu-utilization-high.avif
Binary files differ
diff --git a/doc/check-health.d/notification-02-cpu-utilization-ok.avif b/doc/check-health.d/notification-02-cpu-utilization-ok.avif
new file mode 100644
index 0000000..811ccd7
--- /dev/null
+++ b/doc/check-health.d/notification-02-cpu-utilization-ok.avif
Binary files differ
diff --git a/doc/check-health.d/notification-03-ram-utilization-high.avif b/doc/check-health.d/notification-03-ram-utilization-high.avif
new file mode 100644
index 0000000..59155c5
--- /dev/null
+++ b/doc/check-health.d/notification-03-ram-utilization-high.avif
Binary files differ
diff --git a/doc/check-health.d/notification-04-ram-utilization-ok.avif b/doc/check-health.d/notification-04-ram-utilization-ok.avif
new file mode 100644
index 0000000..d995b9a
--- /dev/null
+++ b/doc/check-health.d/notification-04-ram-utilization-ok.avif
Binary files differ
diff --git a/doc/check-health.d/notification-05-voltage.avif b/doc/check-health.d/notification-05-voltage.avif
new file mode 100644
index 0000000..17a385b
--- /dev/null
+++ b/doc/check-health.d/notification-05-voltage.avif
Binary files differ
diff --git a/doc/check-health.d/notification-06-temperature-high.avif b/doc/check-health.d/notification-06-temperature-high.avif
new file mode 100644
index 0000000..60d3802
--- /dev/null
+++ b/doc/check-health.d/notification-06-temperature-high.avif
Binary files differ
diff --git a/doc/check-health.d/notification-07-temperature-ok.avif b/doc/check-health.d/notification-07-temperature-ok.avif
new file mode 100644
index 0000000..4afed02
--- /dev/null
+++ b/doc/check-health.d/notification-07-temperature-ok.avif
Binary files differ
diff --git a/doc/check-health.d/notification-08-psu-fail.avif b/doc/check-health.d/notification-08-psu-fail.avif
new file mode 100644
index 0000000..ad049ac
--- /dev/null
+++ b/doc/check-health.d/notification-08-psu-fail.avif
Binary files differ
diff --git a/doc/check-health.d/notification-09-psu-ok.avif b/doc/check-health.d/notification-09-psu-ok.avif
new file mode 100644
index 0000000..26f5a74
--- /dev/null
+++ b/doc/check-health.d/notification-09-psu-ok.avif
Binary files differ
diff --git a/doc/check-health.md b/doc/check-health.md
new file mode 100644
index 0000000..f94a0bf
--- /dev/null
+++ b/doc/check-health.md
@@ -0,0 +1,96 @@
+Notify about health state
+=========================
+
+[![GitHub stars](https://img.shields.io/github/stars/eworm-de/routeros-scripts?logo=GitHub&style=flat&color=red)](https://github.com/eworm-de/routeros-scripts/stargazers)
+[![GitHub forks](https://img.shields.io/github/forks/eworm-de/routeros-scripts?logo=GitHub&style=flat&color=green)](https://github.com/eworm-de/routeros-scripts/network)
+[![GitHub watchers](https://img.shields.io/github/watchers/eworm-de/routeros-scripts?logo=GitHub&style=flat&color=blue)](https://github.com/eworm-de/routeros-scripts/watchers)
+[![required RouterOS version](https://img.shields.io/badge/RouterOS-7.13-yellow?style=flat)](https://mikrotik.com/download/changelogs/)
+[![Telegram group @routeros_scripts](https://img.shields.io/badge/Telegram-%40routeros__scripts-%2326A5E4?logo=telegram&style=flat)](https://t.me/routeros_scripts)
+[![donate with PayPal](https://img.shields.io/badge/Like_it%3F-Donate!-orange?logo=githubsponsors&logoColor=orange&style=flat)](https://www.paypal.com/cgi-bin/webscr?cmd=_s-xclick&hosted_button_id=A4ZXBD6YS2W8J)
+
+[⬅️ Go back to main README](../README.md)
+
+> ℹ️ **Info**: This script can not be used on its own but requires the base
+> installation. See [main README](../README.md) for details.
+
+Description
+-----------
+
+This script is run from scheduler periodically, sending notification on
+health related events:
+
+* high CPU utilization
+* high RAM utilization (low available RAM)
+* voltage jumps up or down more than configured threshold
+* voltage drops below hard lower limit
+* power supply failed or recovered
+* temperature is above or below threshold
+
+Note that bad initial state will not trigger an event.
+
+Monitoring CPU and RAM utilization (available processing and memory
+resources) works on all devices. Other than that only sensors available
+in hardware can be checked. See what your hardware supports:
+
+ /system/health/print;
+
+### Sample notifications
+
+#### CPU utilization
+
+![check-health notification cpu utilization high](check-health.d/notification-01-cpu-utilization-high.avif)
+![check-health notification cpu utilization ok](check-health.d/notification-02-cpu-utilization-ok.avif)
+
+#### RAM utilization (low available RAM)
+
+![check-health notification ram utilization high](check-health.d/notification-03-ram-utilization-high.avif)
+![check-health notification ram utilization ok](check-health.d/notification-04-ram-utilization-ok.avif)
+
+#### Voltage
+
+![check-health notification voltage](check-health.d/notification-05-voltage.avif)
+
+#### Temperature
+
+![check-health notification temperature high](check-health.d/notification-06-temperature-high.avif)
+![check-health notification temperature ok](check-health.d/notification-07-temperature-ok.avif)
+
+#### PSU state
+
+![check-health notification psu fail](check-health.d/notification-08-psu-fail.avif)
+![check-health notification psu ok](check-health.d/notification-09-psu-ok.avif)
+
+Requirements and installation
+-----------------------------
+
+Just install the script and create a scheduler:
+
+ $ScriptInstallUpdate check-health;
+ /system/scheduler/add interval=53s name=check-health on-event="/system/script/run check-health;" start-time=startup;
+
+> ℹ️ **Info**: Running lots of scripts simultaneously can tamper the
+> precision of cpu utilization, escpecially on devices with limited
+> resources. Thus an unusual interval is used here.
+
+Configuration
+-------------
+
+The configuration goes to `global-config-overlay`, these are the parameters:
+
+* `CheckHealthTemperature`: an array specifying temperature thresholds for sensors
+* `CheckHealthVoltageLow`: value (in volt*10) giving a hard lower limit
+* `CheckHealthVoltagePercent`: percentage value to trigger voltage jumps
+
+> ℹ️ **Info**: Copy relevant configuration from
+> [`global-config`](../global-config.rsc) (the one without `-overlay`) to
+> your local `global-config-overlay` and modify it to your specific needs.
+
+Also notification settings are required for
+[e-mail](mod/notification-email.md),
+[matrix](mod/notification-matrix.md),
+[ntfy](mod/notification-ntfy.md) and/or
+[telegram](mod/notification-telegram.md).
+
+---
+[⬅️ Go back to main README](../README.md)
+[⬆️ Go back to top](#top)
diff --git a/doc/check-lte-firmware-upgrade.d/notification.avif b/doc/check-lte-firmware-upgrade.d/notification.avif
new file mode 100644
index 0000000..c440da5
--- /dev/null
+++ b/doc/check-lte-firmware-upgrade.d/notification.avif
Binary files differ
diff --git a/doc/check-lte-firmware-upgrade.md b/doc/check-lte-firmware-upgrade.md
new file mode 100644
index 0000000..59a62c7
--- /dev/null
+++ b/doc/check-lte-firmware-upgrade.md
@@ -0,0 +1,58 @@
+Notify on LTE firmware upgrade
+==============================
+
+[![GitHub stars](https://img.shields.io/github/stars/eworm-de/routeros-scripts?logo=GitHub&style=flat&color=red)](https://github.com/eworm-de/routeros-scripts/stargazers)
+[![GitHub forks](https://img.shields.io/github/forks/eworm-de/routeros-scripts?logo=GitHub&style=flat&color=green)](https://github.com/eworm-de/routeros-scripts/network)
+[![GitHub watchers](https://img.shields.io/github/watchers/eworm-de/routeros-scripts?logo=GitHub&style=flat&color=blue)](https://github.com/eworm-de/routeros-scripts/watchers)
+[![required RouterOS version](https://img.shields.io/badge/RouterOS-7.13-yellow?style=flat)](https://mikrotik.com/download/changelogs/)
+[![Telegram group @routeros_scripts](https://img.shields.io/badge/Telegram-%40routeros__scripts-%2326A5E4?logo=telegram&style=flat)](https://t.me/routeros_scripts)
+[![donate with PayPal](https://img.shields.io/badge/Like_it%3F-Donate!-orange?logo=githubsponsors&logoColor=orange&style=flat)](https://www.paypal.com/cgi-bin/webscr?cmd=_s-xclick&hosted_button_id=A4ZXBD6YS2W8J)
+
+[⬅️ Go back to main README](../README.md)
+
+> ℹ️ **Info**: This script can not be used on its own but requires the base
+> installation. See [main README](../README.md) for details.
+
+Description
+-----------
+
+This script is run from scheduler periodically, checking for LTE firmware
+upgrades. Currently supported LTE hardware:
+
+* R11e-LTE
+* R11e-LTE-US
+* R11e-4G
+* R11e-LTE6
+
+### Sample notification
+
+![check-lte-firmware-upgrade notification](check-lte-firmware-upgrade.d/notification.avif)
+
+Requirements and installation
+-----------------------------
+
+Just install the script:
+
+ $ScriptInstallUpdate check-lte-firmware-upgrade;
+
+... and create a scheduler:
+
+ /system/scheduler/add interval=1d name=check-lte-firmware-upgrade on-event="/system/script/run check-lte-firmware-upgrade;" start-time=startup;
+
+Configuration
+-------------
+
+Also notification settings are required for
+[e-mail](mod/notification-email.md),
+[matrix](mod/notification-matrix.md) and/or
+[telegram](mod/notification-telegram.md).
+
+See also
+--------
+
+* [Notify on RouterOS update](check-routeros-update.md)
+* [Install LTE firmware upgrade](unattended-lte-firmware-upgrade.md)
+
+---
+[⬅️ Go back to main README](../README.md)
+[⬆️ Go back to top](#top)
diff --git a/doc/check-routeros-update.d/notification.avif b/doc/check-routeros-update.d/notification.avif
new file mode 100644
index 0000000..50317cf
--- /dev/null
+++ b/doc/check-routeros-update.d/notification.avif
Binary files differ
diff --git a/doc/check-routeros-update.md b/doc/check-routeros-update.md
new file mode 100644
index 0000000..f9d485c
--- /dev/null
+++ b/doc/check-routeros-update.md
@@ -0,0 +1,107 @@
+Notify on RouterOS update
+=========================
+
+[![GitHub stars](https://img.shields.io/github/stars/eworm-de/routeros-scripts?logo=GitHub&style=flat&color=red)](https://github.com/eworm-de/routeros-scripts/stargazers)
+[![GitHub forks](https://img.shields.io/github/forks/eworm-de/routeros-scripts?logo=GitHub&style=flat&color=green)](https://github.com/eworm-de/routeros-scripts/network)
+[![GitHub watchers](https://img.shields.io/github/watchers/eworm-de/routeros-scripts?logo=GitHub&style=flat&color=blue)](https://github.com/eworm-de/routeros-scripts/watchers)
+[![required RouterOS version](https://img.shields.io/badge/RouterOS-7.13-yellow?style=flat)](https://mikrotik.com/download/changelogs/)
+[![Telegram group @routeros_scripts](https://img.shields.io/badge/Telegram-%40routeros__scripts-%2326A5E4?logo=telegram&style=flat)](https://t.me/routeros_scripts)
+[![donate with PayPal](https://img.shields.io/badge/Like_it%3F-Donate!-orange?logo=githubsponsors&logoColor=orange&style=flat)](https://www.paypal.com/cgi-bin/webscr?cmd=_s-xclick&hosted_button_id=A4ZXBD6YS2W8J)
+
+[⬅️ Go back to main README](../README.md)
+
+> ℹ️ **Info**: This script can not be used on its own but requires the base
+> installation. See [main README](../README.md) for details.
+
+Description
+-----------
+
+The primary use of this script is to notify about RouterOS updates.
+
+Run from a terminal you can start the update process or schedule it.
+
+Centrally managing update process of several devices is possibly by
+specifying versions safe to be updated on a web server. Versions seen
+in neighbor discovery can be specified to be safe as well.
+
+Also installing patch updates (where just last digit is increased)
+automatically is supported.
+
+> ⚠️ **Warning**: Installing updates is important from a security point
+> of view. At the same time it can be source of serve breakage. So test
+> versions in lab and read
+> [changelog](https://mikrotik.com/download/changelogs/) and
+> [forum](https://forum.mikrotik.com/viewforum.php?f=21) before deploying
+> to your production environment! Automatic updates should be handled
+> with care!
+
+### Sample notification
+
+![check-routeros-update notification](check-routeros-update.d/notification.avif)
+
+Requirements and installation
+-----------------------------
+
+Just install the script:
+
+ $ScriptInstallUpdate check-routeros-update;
+
+And add a scheduler for automatic update notification:
+
+ /system/scheduler/add interval=1d name=check-routeros-update on-event="/system/script/run check-routeros-update;" start-time=startup;
+
+Configuration
+-------------
+
+No extra configuration is required to receive notifications. Several
+mechanisms are availalbe to enable automatic installation of updates.
+The configuration goes to `global-config-overlay`, these are the parameters:
+
+* `SafeUpdateNeighbor`: install updates automatically if at least one other
+ device is seen in neighbor list with new version
+* `SafeUpdateNeighborIdentity`: regular expression to match identity for
+ trusted devices, leave empty to match all
+* `SafeUpdatePatch`: install patch updates (where just last digit changes)
+ automatically
+* `SafeUpdateUrl`: url on webserver to check for safe update, the channel
+ (`long-term`, `stable` or `testing`) is appended
+* `SafeUpdateAll`: install **all** updates automatically
+
+> ℹ️ **Info**: Copy relevant configuration from
+> [`global-config`](../global-config.rsc) (the one without `-overlay`) to
+> your local `global-config-overlay` and modify it to your specific needs.
+
+Also notification settings are required for
+[e-mail](mod/notification-email.md),
+[matrix](mod/notification-matrix.md),
+[ntfy](mod/notification-ntfy.md) and/or
+[telegram](mod/notification-telegram.md).
+
+Usage and invocation
+--------------------
+
+Be notified when run from scheduler or run it manually:
+
+ /system/script/run check-routeros-update;
+
+If an update is found you can install it right away.
+
+Installing script [packages-update](packages-update.md) gives extra options.
+
+Tips & Tricks
+-------------
+
+The script checks for full connectivity before acting, so scheduling at
+startup is perfectly valid:
+
+ /system/scheduler/add name=check-routeros-update@startup on-event="/system/script/run check-routeros-update;" start-time=startup;
+
+See also
+--------
+
+* [Automatically upgrade firmware and reboot](firmware-upgrade-reboot.md)
+* [Manage system update](packages-update.md)
+
+---
+[⬅️ Go back to main README](../README.md)
+[⬆️ Go back to top](#top)
diff --git a/doc/cloud-backup.md b/doc/cloud-backup.md
new file mode 100644
index 0000000..e161cfa
--- /dev/null
+++ b/doc/cloud-backup.md
@@ -0,0 +1 @@
+This script has been renamed. Please see [backup-cloud](backup-cloud.md).
diff --git a/doc/collect-wireless-mac.d/notification.avif b/doc/collect-wireless-mac.d/notification.avif
new file mode 100644
index 0000000..a2833f0
--- /dev/null
+++ b/doc/collect-wireless-mac.d/notification.avif
Binary files differ
diff --git a/doc/collect-wireless-mac.md b/doc/collect-wireless-mac.md
new file mode 100644
index 0000000..57032d8
--- /dev/null
+++ b/doc/collect-wireless-mac.md
@@ -0,0 +1,77 @@
+Collect MAC addresses in wireless access list
+=============================================
+
+[![GitHub stars](https://img.shields.io/github/stars/eworm-de/routeros-scripts?logo=GitHub&style=flat&color=red)](https://github.com/eworm-de/routeros-scripts/stargazers)
+[![GitHub forks](https://img.shields.io/github/forks/eworm-de/routeros-scripts?logo=GitHub&style=flat&color=green)](https://github.com/eworm-de/routeros-scripts/network)
+[![GitHub watchers](https://img.shields.io/github/watchers/eworm-de/routeros-scripts?logo=GitHub&style=flat&color=blue)](https://github.com/eworm-de/routeros-scripts/watchers)
+[![required RouterOS version](https://img.shields.io/badge/RouterOS-7.13-yellow?style=flat)](https://mikrotik.com/download/changelogs/)
+[![Telegram group @routeros_scripts](https://img.shields.io/badge/Telegram-%40routeros__scripts-%2326A5E4?logo=telegram&style=flat)](https://t.me/routeros_scripts)
+[![donate with PayPal](https://img.shields.io/badge/Like_it%3F-Donate!-orange?logo=githubsponsors&logoColor=orange&style=flat)](https://www.paypal.com/cgi-bin/webscr?cmd=_s-xclick&hosted_button_id=A4ZXBD6YS2W8J)
+
+[⬅️ Go back to main README](../README.md)
+
+> ℹ️ **Info**: This script can not be used on its own but requires the base
+> installation. See [main README](../README.md) for details.
+
+Description
+-----------
+
+This script adds unknown MAC addresses of connected wireless devices to
+address list. In addition a notification is sent.
+
+By default the access list entry is disabled, but you can easily enable
+and modify it to your needs.
+
+### Sample notification
+
+![collect-wireless-mac notification](collect-wireless-mac.d/notification.avif)
+
+Requirements and installation
+-----------------------------
+
+Depending on whether you use `wifi` package (`/interface/wifi`), legacy
+wifi with CAPsMAN (`/caps-man`) or local wireless interface
+(`/interface/wireless`) you need to install a different script.
+
+For `wifi`:
+
+ $ScriptInstallUpdate collect-wireless-mac.wifi;
+
+For legacy CAPsMAN:
+
+ $ScriptInstallUpdate collect-wireless-mac.capsman;
+
+For legacy local interface:
+
+ $ScriptInstallUpdate collect-wireless-mac.local;
+
+Configuration
+-------------
+
+On first run a disabled access list entry acting as marker (with comment
+"`--- collected above ---`") is added. Move this entry to define where new
+entries are to be added.
+
+Also notification settings are required for
+[e-mail](mod/notification-email.md),
+[matrix](mod/notification-matrix.md),
+[ntfy](mod/notification-ntfy.md) and/or
+[telegram](mod/notification-telegram.md).
+
+Usage and invocation
+--------------------
+
+Run this script from a dhcp server as lease-script to collect the MAC
+address when a new address is leased. You may want to use
+[lease-script](lease-script.md).
+
+See also
+--------
+
+* [Comment DHCP leases with info from access list](dhcp-lease-comment.md)
+* [Create DNS records for DHCP leases](dhcp-to-dns.md)
+* [Run other scripts on DHCP lease](lease-script.md)
+
+---
+[⬅️ Go back to main README](../README.md)
+[⬆️ Go back to top](#top)
diff --git a/doc/daily-psk.d/notification.avif b/doc/daily-psk.d/notification.avif
new file mode 100644
index 0000000..dd0b1b6
--- /dev/null
+++ b/doc/daily-psk.d/notification.avif
Binary files differ
diff --git a/doc/daily-psk.md b/doc/daily-psk.md
new file mode 100644
index 0000000..3894d52
--- /dev/null
+++ b/doc/daily-psk.md
@@ -0,0 +1,88 @@
+Use wireless network with daily psk
+===================================
+
+[![GitHub stars](https://img.shields.io/github/stars/eworm-de/routeros-scripts?logo=GitHub&style=flat&color=red)](https://github.com/eworm-de/routeros-scripts/stargazers)
+[![GitHub forks](https://img.shields.io/github/forks/eworm-de/routeros-scripts?logo=GitHub&style=flat&color=green)](https://github.com/eworm-de/routeros-scripts/network)
+[![GitHub watchers](https://img.shields.io/github/watchers/eworm-de/routeros-scripts?logo=GitHub&style=flat&color=blue)](https://github.com/eworm-de/routeros-scripts/watchers)
+[![required RouterOS version](https://img.shields.io/badge/RouterOS-7.13-yellow?style=flat)](https://mikrotik.com/download/changelogs/)
+[![Telegram group @routeros_scripts](https://img.shields.io/badge/Telegram-%40routeros__scripts-%2326A5E4?logo=telegram&style=flat)](https://t.me/routeros_scripts)
+[![donate with PayPal](https://img.shields.io/badge/Like_it%3F-Donate!-orange?logo=githubsponsors&logoColor=orange&style=flat)](https://www.paypal.com/cgi-bin/webscr?cmd=_s-xclick&hosted_button_id=A4ZXBD6YS2W8J)
+
+[⬅️ Go back to main README](../README.md)
+
+> ℹ️ **Info**: This script can not be used on its own but requires the base
+> installation. See [main README](../README.md) for details.
+
+Description
+-----------
+
+This script is supposed to provide a wifi network which changes the
+passphrase to a pseudo-random string daily.
+
+### Sample notification
+
+![daily-psk notification](daily-psk.d/notification.avif)
+
+Requirements and installation
+-----------------------------
+
+Just install this script.
+
+Depending on whether you use `wifi` package (`/interface/wifi`), legacy
+wifi with CAPsMAN (`/caps-man`) or local wireless interface
+(`/interface/wireless`) you need to install a different script and add
+schedulers to run the script:
+
+For `wifi`:
+
+ $ScriptInstallUpdate daily-psk.wifi;
+ /system/scheduler/add interval=1d name=daily-psk on-event="/system/script/run daily-psk.wifi;" start-time=03:00:00;
+ /system/scheduler/add name=daily-psk@startup on-event="/system/script/run daily-psk.wifi;" start-time=startup;
+
+For legacy CAPsMAN:
+
+ $ScriptInstallUpdate daily-psk.capsman;
+ /system/scheduler/add interval=1d name=daily-psk on-event="/system/script/run daily-psk.capsman;" start-time=03:00:00;
+ /system/scheduler/add name=daily-psk@startup on-event="/system/script/run daily-psk.capsman;" start-time=startup;
+
+For legacy local interface:
+
+ $ScriptInstallUpdate daily-psk.local;
+ /system/scheduler/add interval=1d name=daily-psk on-event="/system/script/run daily-psk.local;" start-time=03:00:00;
+ /system/scheduler/add name=daily-psk@startup on-event="/system/script/run daily-psk.local;" start-time=startup;
+
+These will update the passphrase on boot and nightly at 3:00.
+
+Configuration
+-------------
+
+The configuration goes to `global-config-overlay`, these are the parameters:
+
+* `DailyPskMatchComment`: pattern to match the wireless access list comment
+* `DailyPskSecrets`: an array with pseudo random strings
+
+> ℹ️ **Info**: Copy relevant configuration from
+> [`global-config`](../global-config.rsc) (the one without `-overlay`) to
+> your local `global-config-overlay` and modify it to your specific needs.
+
+Then add an access list entry. For `wifi`:
+
+ /interface/wifi/access-list/add comment="Daily PSK" ssid-regexp="-guest\$" passphrase="ToBeChangedDaily";
+
+For legacy CAPsMAN:
+
+ /caps-man/access-list/add comment="Daily PSK" ssid-regexp="-guest\$" private-passphrase="ToBeChangedDaily";
+
+For legacy local interface:
+
+ /interface/wireless/access-list/add comment="Daily PSK" interface=wl-daily private-pre-shared-key="ToBeChangedDaily";
+
+Also notification settings are required for
+[e-mail](mod/notification-email.md),
+[trix](mod/notification-matrix.md),
+[ntfy](mod/notification-ntfy.md) and/or
+[telegram](mod/notification-telegram.md).
+
+---
+[⬅️ Go back to main README](../README.md)
+[⬆️ Go back to top](#top)
diff --git a/doc/dhcp-lease-comment.md b/doc/dhcp-lease-comment.md
new file mode 100644
index 0000000..f95b124
--- /dev/null
+++ b/doc/dhcp-lease-comment.md
@@ -0,0 +1,64 @@
+Comment DHCP leases with info from access list
+==============================================
+
+[![GitHub stars](https://img.shields.io/github/stars/eworm-de/routeros-scripts?logo=GitHub&style=flat&color=red)](https://github.com/eworm-de/routeros-scripts/stargazers)
+[![GitHub forks](https://img.shields.io/github/forks/eworm-de/routeros-scripts?logo=GitHub&style=flat&color=green)](https://github.com/eworm-de/routeros-scripts/network)
+[![GitHub watchers](https://img.shields.io/github/watchers/eworm-de/routeros-scripts?logo=GitHub&style=flat&color=blue)](https://github.com/eworm-de/routeros-scripts/watchers)
+[![required RouterOS version](https://img.shields.io/badge/RouterOS-7.13-yellow?style=flat)](https://mikrotik.com/download/changelogs/)
+[![Telegram group @routeros_scripts](https://img.shields.io/badge/Telegram-%40routeros__scripts-%2326A5E4?logo=telegram&style=flat)](https://t.me/routeros_scripts)
+[![donate with PayPal](https://img.shields.io/badge/Like_it%3F-Donate!-orange?logo=githubsponsors&logoColor=orange&style=flat)](https://www.paypal.com/cgi-bin/webscr?cmd=_s-xclick&hosted_button_id=A4ZXBD6YS2W8J)
+
+[⬅️ Go back to main README](../README.md)
+
+> ℹ️ **Info**: This script can not be used on its own but requires the base
+> installation. See [main README](../README.md) for details.
+
+Description
+-----------
+
+This script adds comments to dynamic dhcp server leases. Infos are taken
+from wireless access list.
+
+Requirements and installation
+-----------------------------
+
+Depending on whether you use `wifi` package (`/interface/wifi`), legacy
+wifi with CAPsMAN (`/caps-man`) or local wireless interface
+(`/interface/wireless`) you need to install a different script.
+
+For `wifi`:
+
+ $ScriptInstallUpdate dhcp-lease-comment.wifi;
+
+For legacy CAPsMAN:
+
+ $ScriptInstallUpdate dhcp-lease-comment.capsman;
+
+For legacy local interface:
+
+ $ScriptInstallUpdate dhcp-lease-comment.local;
+
+Configuration
+-------------
+
+Infos are taken from wireless access list. Add entries with proper comments
+there. You may want to use [collect-wireless-mac](collect-wireless-mac.md)
+to prepare entries.
+
+Usage and invocation
+--------------------
+
+Run this script from a dhcp server as lease-script to update the comment
+just after a new address is leased. You may want to use
+[lease-script](lease-script.md).
+
+See also
+--------
+
+* [Collect MAC addresses in wireless access list](collect-wireless-mac.md)
+* [Create DNS records for DHCP leases](dhcp-to-dns.md)
+* [Run other scripts on DHCP lease](lease-script.md)
+
+---
+[⬅️ Go back to main README](../README.md)
+[⬆️ Go back to top](#top)
diff --git a/doc/dhcp-to-dns.md b/doc/dhcp-to-dns.md
new file mode 100644
index 0000000..13d5ef3
--- /dev/null
+++ b/doc/dhcp-to-dns.md
@@ -0,0 +1,93 @@
+Create DNS records for DHCP leases
+==================================
+
+[![GitHub stars](https://img.shields.io/github/stars/eworm-de/routeros-scripts?logo=GitHub&style=flat&color=red)](https://github.com/eworm-de/routeros-scripts/stargazers)
+[![GitHub forks](https://img.shields.io/github/forks/eworm-de/routeros-scripts?logo=GitHub&style=flat&color=green)](https://github.com/eworm-de/routeros-scripts/network)
+[![GitHub watchers](https://img.shields.io/github/watchers/eworm-de/routeros-scripts?logo=GitHub&style=flat&color=blue)](https://github.com/eworm-de/routeros-scripts/watchers)
+[![required RouterOS version](https://img.shields.io/badge/RouterOS-7.13-yellow?style=flat)](https://mikrotik.com/download/changelogs/)
+[![Telegram group @routeros_scripts](https://img.shields.io/badge/Telegram-%40routeros__scripts-%2326A5E4?logo=telegram&style=flat)](https://t.me/routeros_scripts)
+[![donate with PayPal](https://img.shields.io/badge/Like_it%3F-Donate!-orange?logo=githubsponsors&logoColor=orange&style=flat)](https://www.paypal.com/cgi-bin/webscr?cmd=_s-xclick&hosted_button_id=A4ZXBD6YS2W8J)
+
+[⬅️ Go back to main README](../README.md)
+
+> ℹ️ **Info**: This script can not be used on its own but requires the base
+> installation. See [main README](../README.md) for details.
+
+Description
+-----------
+
+This script adds (and updates & removes) dns records based on dhcp server
+leases. An A record based on mac address is created for all bound lease,
+additionally a CNAME record is created from host name if available.
+
+Requirements and installation
+-----------------------------
+
+Just install the script:
+
+ $ScriptInstallUpdate dhcp-to-dns;
+
+Then run it from dhcp server as lease script. You may want to use
+[lease-script](lease-script.md).
+
+A scheduler cares about cleanup:
+
+ /system/scheduler/add interval=15m name=dhcp-to-dns on-event="/system/script/run dhcp-to-dns;" start-time=startup;
+
+Configuration
+-------------
+
+On first run a disabled static dns record acting as marker (with comment
+"`--- dhcp-to-dns above ---`") is added. Move this entry to define where new
+entries are to be added.
+
+The configuration goes to dhcp server's network definition. The domain is
+used to form the dns name:
+
+ /ip/dhcp-server/network/add address=10.0.0.0/24 domain=example.com;
+
+A bound lease for mac address `00:11:22:33:44:55` with ip address
+`10.0.0.50` would result in an A record `00-11-22-33-44-55.example.com`
+pointing to the given ip address.
+
+Additional options can be given from comment, to add an extra level in
+dns name or define a different domain.
+
+ /ip/dhcp-server/network/add address=10.0.0.0/24 domain=example.com comment="domain=another-domain.com, name-extra=dhcp";
+
+This example would result in name `00-11-22-33-44-55.dhcp.another-domain.com`
+for the same lease.
+
+If no domain is found in dhcp server's network definition a fallback from
+`global-config-overlay` is used. This is the parameter:
+
+* `Domain`: the domain used for dns records
+
+> ℹ️ **Info**: Copy relevant configuration from
+> [`global-config`](../global-config.rsc) (the one without `-overlay`) to
+> your local `global-config-overlay` and modify it to your specific needs.
+
+### Host name from DHCP lease comment
+
+Overwriting the host name from dhcp lease comment is supported, just add
+something like `hostname=new-hostname` in comment, and separate it by comma
+from other information if required:
+
+ /ip/dhcp-server/lease/add address=10.0.0.50 comment="my device, hostname=new-hostname" mac-address=00:11:22:33:44:55 server=dhcp;
+
+Note this information can be configured in wireless access list with
+[dhcp-lease-comment](dhcp-lease-comment.md), though it comes with a delay
+then due to script execution order. Decrease the scheduler interval to
+reduce the effect.
+
+See also
+--------
+
+* [Collect MAC addresses in wireless access list](collect-wireless-mac.md)
+* [Comment DHCP leases with info from access list](dhcp-lease-comment.md)
+* [Create DNS records for IPSec peers](ipsec-to-dns.md)
+* [Run other scripts on DHCP lease](lease-script.md)
+
+---
+[⬅️ Go back to main README](../README.md)
+[⬆️ Go back to top](#top)
diff --git a/doc/early-errors.md b/doc/early-errors.md
new file mode 100644
index 0000000..b3c6800
--- /dev/null
+++ b/doc/early-errors.md
@@ -0,0 +1,2 @@
+This script has been replaced. Please migrate to
+[Forward log messages via notification](log-forward.md).
diff --git a/doc/email-backup.md b/doc/email-backup.md
new file mode 100644
index 0000000..d674743
--- /dev/null
+++ b/doc/email-backup.md
@@ -0,0 +1 @@
+This script has been renamed. Please see [backup-email](backup-email.md). \ No newline at end of file
diff --git a/doc/firmware-upgrade-reboot.md b/doc/firmware-upgrade-reboot.md
new file mode 100644
index 0000000..bac17a7
--- /dev/null
+++ b/doc/firmware-upgrade-reboot.md
@@ -0,0 +1,43 @@
+Automatically upgrade firmware and reboot
+=========================================
+
+[![GitHub stars](https://img.shields.io/github/stars/eworm-de/routeros-scripts?logo=GitHub&style=flat&color=red)](https://github.com/eworm-de/routeros-scripts/stargazers)
+[![GitHub forks](https://img.shields.io/github/forks/eworm-de/routeros-scripts?logo=GitHub&style=flat&color=green)](https://github.com/eworm-de/routeros-scripts/network)
+[![GitHub watchers](https://img.shields.io/github/watchers/eworm-de/routeros-scripts?logo=GitHub&style=flat&color=blue)](https://github.com/eworm-de/routeros-scripts/watchers)
+[![required RouterOS version](https://img.shields.io/badge/RouterOS-7.13-yellow?style=flat)](https://mikrotik.com/download/changelogs/)
+[![Telegram group @routeros_scripts](https://img.shields.io/badge/Telegram-%40routeros__scripts-%2326A5E4?logo=telegram&style=flat)](https://t.me/routeros_scripts)
+[![donate with PayPal](https://img.shields.io/badge/Like_it%3F-Donate!-orange?logo=githubsponsors&logoColor=orange&style=flat)](https://www.paypal.com/cgi-bin/webscr?cmd=_s-xclick&hosted_button_id=A4ZXBD6YS2W8J)
+
+[⬅️ Go back to main README](../README.md)
+
+> ℹ️ **Info**: This script can not be used on its own but requires the base
+> installation. See [main README](../README.md) for details.
+
+Description
+-----------
+
+RouterOS and firmware are upgraded separately, activating the latter
+requires an extra reboot. This script handles upgrade and reboot.
+
+> ⚠️ **Warning**: This *should* be bullet proof, but I can not guarantee. In
+> worst case it has potential to cause a boot loop, so handle with care!
+
+Requirements and installation
+-----------------------------
+
+Just install the script and create a scheduler:
+
+ $ScriptInstallUpdate firmware-upgrade-reboot;
+ /system/scheduler/add name=firmware-upgrade-reboot on-event="/system/script/run firmware-upgrade-reboot;" start-time=startup;
+
+Enjoy firmware being up to date and in sync with RouterOS.
+
+See also
+--------
+
+* [Notify on RouterOS update](check-routeros-update.md)
+* [Manage system update](packages-update.md)
+
+---
+[⬅️ Go back to main README](../README.md)
+[⬆️ Go back to top](#top)
diff --git a/doc/fw-addr-lists.md b/doc/fw-addr-lists.md
new file mode 100644
index 0000000..ac34c88
--- /dev/null
+++ b/doc/fw-addr-lists.md
@@ -0,0 +1,132 @@
+Download, import and update firewall address-lists
+==================================================
+
+[![GitHub stars](https://img.shields.io/github/stars/eworm-de/routeros-scripts?logo=GitHub&style=flat&color=red)](https://github.com/eworm-de/routeros-scripts/stargazers)
+[![GitHub forks](https://img.shields.io/github/forks/eworm-de/routeros-scripts?logo=GitHub&style=flat&color=green)](https://github.com/eworm-de/routeros-scripts/network)
+[![GitHub watchers](https://img.shields.io/github/watchers/eworm-de/routeros-scripts?logo=GitHub&style=flat&color=blue)](https://github.com/eworm-de/routeros-scripts/watchers)
+[![required RouterOS version](https://img.shields.io/badge/RouterOS-7.13-yellow?style=flat)](https://mikrotik.com/download/changelogs/)
+[![Telegram group @routeros_scripts](https://img.shields.io/badge/Telegram-%40routeros__scripts-%2326A5E4?logo=telegram&style=flat)](https://t.me/routeros_scripts)
+[![donate with PayPal](https://img.shields.io/badge/Like_it%3F-Donate!-orange?logo=githubsponsors&logoColor=orange&style=flat)](https://www.paypal.com/cgi-bin/webscr?cmd=_s-xclick&hosted_button_id=A4ZXBD6YS2W8J)
+
+[⬅️ Go back to main README](../README.md)
+
+> ℹ️ **Info**: This script can not be used on its own but requires the base
+> installation. See [main README](../README.md) for details.
+
+Description
+-----------
+
+This script downloads, imports and updates firewall address-lists. Its main
+purpose is to block attacking ip addresses, spam hosts, command-and-control
+servers and similar malicious entities. The default configuration contains
+lists from [abuse.ch](https://abuse.ch/) and
+[dshield.org](https://dshield.org/), and
+lists from [spamhaus.org](https://spamhaus.org/) are prepared.
+
+The address-lists are updated in place, so after initial import you will not
+see situation when the lists are not populated.
+
+To mitigate man-in-the-middle attacks with altered lists the server's
+certificate is checked.
+
+> ⚠️ **Warning**: The script does not limit the size of a list, but keep in
+> mind that huge lists can exhaust your device's resources (RAM and CPU),
+> and may take a long time to process.
+
+Requirements and installation
+-----------------------------
+
+Just install the script:
+
+ $ScriptInstallUpdate fw-addr-lists;
+
+And add two schedulers, first one for initial import after startup, second
+one for subsequent updates:
+
+ /system/scheduler/add name="fw-addr-lists@startup" start-time=startup on-event="/system/script/run fw-addr-lists;";
+ /system/scheduler/add name="fw-addr-lists" start-time=startup interval=2h on-event="/system/script/run fw-addr-lists;";
+
+> ℹ️ **Info**: Modify the interval to your needs, but it is recommended to
+> use less than half of the configured timeout for expiration.
+
+Configuration
+-------------
+
+The configuration goes to `global-config-overlay`, these are the parameters:
+
+* `FwAddrLists`: a list of firewall address-lists to download and import
+* `FwAddrListTimeOut`: the timeout for expiration without renew
+
+> ℹ️ **Info**: Copy relevant configuration from
+> [`global-config`](../global-config.rsc) (the one without `-overlay`) to
+> your local `global-config-overlay` and modify it to your specific needs.
+
+Naming a certificate for a list makes the script verify the server
+certificate, so you should add that if possible. Some certificates are
+available in my repository and downloaded automatically. Import it manually
+(menu `/certificate/`) if missing.
+
+Create firewall rules to process the packets that are related to addresses
+from address-lists.
+
+### IPv4 rules
+
+This rejects the packets from and to IPv4 addresses listed in
+address-list `block`.
+
+ /ip/firewall/filter/add chain=input src-address-list=block action=reject reject-with=icmp-admin-prohibited;
+ /ip/firewall/filter/add chain=forward src-address-list=block action=reject reject-with=icmp-admin-prohibited;
+ /ip/firewall/filter/add chain=forward dst-address-list=block action=reject reject-with=icmp-admin-prohibited;
+ /ip/firewall/filter/add chain=output dst-address-list=block action=reject reject-with=icmp-admin-prohibited;
+
+You may want to have an address-list to allow specific addresses, as prepared
+with a list `allow`. In fact you can use any list name, just change the
+default ones or add your own - matching in configuration and firewall rules.
+
+ /ip/firewall/filter/add chain=input src-address-list=allow action=accept;
+ /ip/firewall/filter/add chain=forward src-address-list=allow action=accept;
+ /ip/firewall/filter/add chain=forward dst-address-list=allow action=accept;
+ /ip/firewall/filter/add chain=output dst-address-list=allow action=accept;
+
+Modify these for your needs, but **most important**: Move the rules up in
+chains and make sure they actually take effect as expected!
+
+Alternatively handle the packets in firewall's raw section if you prefer:
+
+ /ip/firewall/raw/add chain=prerouting src-address-list=block action=drop;
+ /ip/firewall/raw/add chain=prerouting dst-address-list=block action=drop;
+ /ip/firewall/raw/add chain=output dst-address-list=block action=drop;
+
+> ⚠️ **Warning**: Just again... The order of firewall rules is important. Make
+> sure they actually take effect as expected!
+
+### IPv6 rules
+
+These are the same rules, but for IPv6.
+
+Reject packets in address-list `block`:
+
+ /ipv6/firewall/filter/add chain=input src-address-list=block action=reject reject-with=icmp-admin-prohibited;
+ /ipv6/firewall/filter/add chain=forward src-address-list=block action=reject reject-with=icmp-admin-prohibited;
+ /ipv6/firewall/filter/add chain=forward dst-address-list=block action=reject reject-with=icmp-admin-prohibited;
+ /ipv6/firewall/filter/add chain=output dst-address-list=block action=reject reject-with=icmp-admin-prohibited;
+
+Allow packets in address-list `allow`:
+
+ /ipv6/firewall/filter/add chain=input src-address-list=allow action=accept;
+ /ipv6/firewall/filter/add chain=forward src-address-list=allow action=accept;
+ /ipv6/firewall/filter/add chain=forward dst-address-list=allow action=accept;
+ /ipv6/firewall/filter/add chain=output dst-address-list=allow action=accept;
+
+Drop packets in firewall's raw section:
+
+ /ipv6/firewall/raw/add chain=prerouting src-address-list=block action=drop;
+ /ipv6/firewall/raw/add chain=prerouting dst-address-list=block action=drop;
+ /ipv6/firewall/raw/add chain=output dst-address-list=block action=drop;
+
+> ⚠️ **Warning**: Just again... The order of firewall rules is important. Make
+> sure they actually take effect as expected!
+
+---
+[⬅️ Go back to main README](../README.md)
+[⬆️ Go back to top](#top)
diff --git a/doc/global-wait.md b/doc/global-wait.md
new file mode 100644
index 0000000..4b42717
--- /dev/null
+++ b/doc/global-wait.md
@@ -0,0 +1,47 @@
+Wait for global functions and modules
+=====================================
+
+[![GitHub stars](https://img.shields.io/github/stars/eworm-de/routeros-scripts?logo=GitHub&style=flat&color=red)](https://github.com/eworm-de/routeros-scripts/stargazers)
+[![GitHub forks](https://img.shields.io/github/forks/eworm-de/routeros-scripts?logo=GitHub&style=flat&color=green)](https://github.com/eworm-de/routeros-scripts/network)
+[![GitHub watchers](https://img.shields.io/github/watchers/eworm-de/routeros-scripts?logo=GitHub&style=flat&color=blue)](https://github.com/eworm-de/routeros-scripts/watchers)
+[![required RouterOS version](https://img.shields.io/badge/RouterOS-7.13-yellow?style=flat)](https://mikrotik.com/download/changelogs/)
+[![Telegram group @routeros_scripts](https://img.shields.io/badge/Telegram-%40routeros__scripts-%2326A5E4?logo=telegram&style=flat)](https://t.me/routeros_scripts)
+[![donate with PayPal](https://img.shields.io/badge/Like_it%3F-Donate!-orange?logo=githubsponsors&logoColor=orange&style=flat)](https://www.paypal.com/cgi-bin/webscr?cmd=_s-xclick&hosted_button_id=A4ZXBD6YS2W8J)
+
+[⬅️ Go back to main README](../README.md)
+
+> ℹ️ **Info**: This script can not be used on its own but requires the base
+> installation. See [main README](../README.md) for details.
+
+Description
+-----------
+
+The global functions from `global-functions` and modules are loaded by
+scheduler at system startup. Running these functions at system startup may
+result in race condition where configuration and/or function are not yet
+available. This script is supposed to wait for everything being prepared.
+
+Do **not** add this script `global-wait` to the `global-scripts` scheduler!
+It would inhibit the initialization of configuration and functions.
+
+Requirements and installation
+-----------------------------
+
+Just install the script:
+
+ $ScriptInstallUpdate global-wait;
+
+... and add it to your scheduler, for example in combination with the module
+to [manage VLANs on bridge ports](mod/bridge-port-vlan.md):
+
+ /system/scheduler/add name=bridge-port-vlan on-event="/system/script/run global-wait; :global BridgePortVlan; \$BridgePortVlan default;" start-time=startup;
+
+See also
+--------
+
+* [Manage ports in bridge](mod/bridge-port-to.md)
+* [Manage VLANs on bridge ports](mod/bridge-port-vlan.md)
+
+---
+[⬅️ Go back to main README](../README.md)
+[⬆️ Go back to top](#top)
diff --git a/doc/gps-track.md b/doc/gps-track.md
new file mode 100644
index 0000000..7006fb3
--- /dev/null
+++ b/doc/gps-track.md
@@ -0,0 +1,51 @@
+Send GPS position to server
+===========================
+
+[![GitHub stars](https://img.shields.io/github/stars/eworm-de/routeros-scripts?logo=GitHub&style=flat&color=red)](https://github.com/eworm-de/routeros-scripts/stargazers)
+[![GitHub forks](https://img.shields.io/github/forks/eworm-de/routeros-scripts?logo=GitHub&style=flat&color=green)](https://github.com/eworm-de/routeros-scripts/network)
+[![GitHub watchers](https://img.shields.io/github/watchers/eworm-de/routeros-scripts?logo=GitHub&style=flat&color=blue)](https://github.com/eworm-de/routeros-scripts/watchers)
+[![required RouterOS version](https://img.shields.io/badge/RouterOS-7.13-yellow?style=flat)](https://mikrotik.com/download/changelogs/)
+[![Telegram group @routeros_scripts](https://img.shields.io/badge/Telegram-%40routeros__scripts-%2326A5E4?logo=telegram&style=flat)](https://t.me/routeros_scripts)
+[![donate with PayPal](https://img.shields.io/badge/Like_it%3F-Donate!-orange?logo=githubsponsors&logoColor=orange&style=flat)](https://www.paypal.com/cgi-bin/webscr?cmd=_s-xclick&hosted_button_id=A4ZXBD6YS2W8J)
+
+[⬅️ Go back to main README](../README.md)
+
+> ℹ️ **Info**: This script can not be used on its own but requires the base
+> installation. See [main README](../README.md) for details.
+
+Description
+-----------
+
+This script is supposed to run periodically from scheduler and send GPS
+position data to a server for tracking.
+
+A hardware GPS antenna is required.
+
+Requirements and installation
+-----------------------------
+
+Just install the script:
+
+ $ScriptInstallUpdate gps-track;
+
+... and create a scheduler:
+
+ /system/scheduler/add interval=1m name=gps-track on-event="/system/script/run gps-track;" start-time=startup;
+
+Configuration
+-------------
+
+The configuration goes to `global-config-overlay`, the only parameter is:
+
+* `GpsTrackUrl`: the url to send json data to
+
+> ℹ️ **Info**: Copy relevant configuration from
+> [`global-config`](../global-config.rsc) (the one without `-overlay`) to
+> your local `global-config-overlay` and modify it to your specific needs.
+
+The configured coordinate format (see `/system/gps`) defines the format
+sent to the server.
+
+---
+[⬅️ Go back to main README](../README.md)
+[⬆️ Go back to top](#top)
diff --git a/doc/hotspot-to-wpa.md b/doc/hotspot-to-wpa.md
new file mode 100644
index 0000000..275fe4d
--- /dev/null
+++ b/doc/hotspot-to-wpa.md
@@ -0,0 +1,124 @@
+Use WPA network with hotspot credentials
+========================================
+
+[![GitHub stars](https://img.shields.io/github/stars/eworm-de/routeros-scripts?logo=GitHub&style=flat&color=red)](https://github.com/eworm-de/routeros-scripts/stargazers)
+[![GitHub forks](https://img.shields.io/github/forks/eworm-de/routeros-scripts?logo=GitHub&style=flat&color=green)](https://github.com/eworm-de/routeros-scripts/network)
+[![GitHub watchers](https://img.shields.io/github/watchers/eworm-de/routeros-scripts?logo=GitHub&style=flat&color=blue)](https://github.com/eworm-de/routeros-scripts/watchers)
+[![required RouterOS version](https://img.shields.io/badge/RouterOS-7.13-yellow?style=flat)](https://mikrotik.com/download/changelogs/)
+[![Telegram group @routeros_scripts](https://img.shields.io/badge/Telegram-%40routeros__scripts-%2326A5E4?logo=telegram&style=flat)](https://t.me/routeros_scripts)
+[![donate with PayPal](https://img.shields.io/badge/Like_it%3F-Donate!-orange?logo=githubsponsors&logoColor=orange&style=flat)](https://www.paypal.com/cgi-bin/webscr?cmd=_s-xclick&hosted_button_id=A4ZXBD6YS2W8J)
+
+[⬅️ Go back to main README](../README.md)
+
+> ℹ️ **Info**: This script can not be used on its own but requires the base
+> installation. See [main README](../README.md) for details.
+
+Description
+-----------
+
+RouterOS supports an unlimited number of MAC address specific passphrases
+for WPA encrypted wifi networks via access list. The idea of this script
+is to transfer hotspot credentials to MAC address specific WPA passphrase.
+
+Requirements and installation
+-----------------------------
+
+You need a properly configured hotspot on one (open) SSID and a WPA enabled
+SSID with suffix "`-wpa`".
+
+Then install the script.
+Depending on whether you use `wifi` package (`/interface/wifi`)or legacy
+wifi with CAPsMAN (`/caps-man`) you need to install a different script and
+set it as `on-login` script in hotspot.
+
+For `wifi`:
+
+ $ScriptInstallUpdate hotspot-to-wpa.wifi;
+ /ip/hotspot/user/profile/set on-login="hotspot-to-wpa.wifi" [ find ];
+
+For legacy CAPsMAN:
+
+ $ScriptInstallUpdate hotspot-to-wpa.capsman;
+ /ip/hotspot/user/profile/set on-login="hotspot-to-wpa.capsman" [ find ];
+
+### Automatic cleanup
+
+With just `hotspot-to-wpa` installed the mac addresses will last in the
+access list forever. Install the optional script for automatic cleanup
+and add a scheduler.
+
+For `wifi`:
+
+ $ScriptInstallUpdate hotspot-to-wpa-cleanup.wifi,lease-script;
+ /system/scheduler/add interval=1d name=hotspot-to-wpa-cleanup on-event="/system/script/run hotspot-to-wpa-cleanup.wifi;" start-time=startup;
+
+For legacy CAPsMAN:
+
+ $ScriptInstallUpdate hotspot-to-wpa-cleanup.capsman,lease-script;
+ /system/scheduler/add interval=1d name=hotspot-to-wpa-cleanup on-event="/system/script/run hotspot-to-wpa-cleanup.capsman;" start-time=startup;
+
+And add the lease script and matcher comment to your wpa interfaces' dhcp
+server. You can add more information to the comment, separated by comma. In
+this example the server is called `hotspot-to-wpa`.
+
+ /ip/dhcp-server/set lease-script=lease-script comment="hotspot-to-wpa=wpa" hotspot-to-wpa;
+
+You can specify the timeout after which a device is removed from leases and
+access-list. The default is four weeks.
+
+ /ip/dhcp-server/set lease-script=lease-script comment="hotspot-to-wpa=wpa, timeout=2w" hotspot-to-wpa;
+
+Configuration
+-------------
+
+On first run a disabled access list entry acting as marker (with comment
+"`--- hotspot-to-wpa above ---`") is added. Move this entry to define where new
+entries are to be added.
+
+Create hotspot login credentials:
+
+ /ip/hotspot/user/add comment="Test User 1" name=user1 password=v3ry;
+ /ip/hotspot/user/add comment="Test User 2" name=user2 password=s3cr3t;
+
+This also works with authentication via radius, but is limited then:
+Additional information is not available, including the password.
+
+Additionally templates can be created to give more options for access list:
+
+* `action`: set to `reject` to ignore logins on that hotspot
+* `passphrase` or `private-passphrase`: do **not** use passphrase from
+ hotspot's user credentials, but given one - or unset (use default
+ passphrase) with special word `ignore`
+* `ssid-regexp`: set a different SSID regular expression to match
+* `vlan-id`: connect device to specific VLAN
+* `vlan-mode`: set the VLAN mode for device
+
+For a hotspot called `example` the template could look like this.
+For `wifi`:
+
+ /interface/wifi/access-list/add comment="hotspot-to-wpa template example" disabled=yes passphrase="ignore" ssid-regexp="^example\$" vlan-id=10;
+
+For legacy CAPsMAN:
+
+ /caps-man/access-list/add comment="hotspot-to-wpa template example" disabled=yes private-passphrase="ignore" ssid-regexp="^example\$" vlan-id=10 vlan-mode=use-tag;
+
+The same settings are available in hotspot user's comment and take precedence
+over the template settings:
+
+ /ip/hotspot/user/add comment="private-passphrase=ignore, ssid-regexp=^example\\\$, vlan-id=10, vlan-mode=use-tag" name=user password=v3ry-s3cr3t;
+
+Usage and invocation
+--------------------
+
+Now let the users connect and login to the hotspot. After that the devices
+(identified by MAC address) can connect to the WPA network, using the
+passphrase from hotspot credentials.
+
+See also
+--------
+
+* [Run other scripts on DHCP lease](lease-script.md)
+
+---
+[⬅️ Go back to main README](../README.md)
+[⬆️ Go back to top](#top)
diff --git a/doc/ip-addr-bridge.md b/doc/ip-addr-bridge.md
new file mode 100644
index 0000000..941a8ae
--- /dev/null
+++ b/doc/ip-addr-bridge.md
@@ -0,0 +1,39 @@
+Manage IP addresses with bridge status
+======================================
+
+[![GitHub stars](https://img.shields.io/github/stars/eworm-de/routeros-scripts?logo=GitHub&style=flat&color=red)](https://github.com/eworm-de/routeros-scripts/stargazers)
+[![GitHub forks](https://img.shields.io/github/forks/eworm-de/routeros-scripts?logo=GitHub&style=flat&color=green)](https://github.com/eworm-de/routeros-scripts/network)
+[![GitHub watchers](https://img.shields.io/github/watchers/eworm-de/routeros-scripts?logo=GitHub&style=flat&color=blue)](https://github.com/eworm-de/routeros-scripts/watchers)
+[![required RouterOS version](https://img.shields.io/badge/RouterOS-7.13-yellow?style=flat)](https://mikrotik.com/download/changelogs/)
+[![Telegram group @routeros_scripts](https://img.shields.io/badge/Telegram-%40routeros__scripts-%2326A5E4?logo=telegram&style=flat)](https://t.me/routeros_scripts)
+[![donate with PayPal](https://img.shields.io/badge/Like_it%3F-Donate!-orange?logo=githubsponsors&logoColor=orange&style=flat)](https://www.paypal.com/cgi-bin/webscr?cmd=_s-xclick&hosted_button_id=A4ZXBD6YS2W8J)
+
+[⬅️ Go back to main README](../README.md)
+
+Description
+-----------
+
+With RouterOS an IP address is always active, even if an interface is down.
+Other venders handle this differently - and sometimes this behavior is
+expected. This script mimics this behavior.
+
+Requirements and installation
+-----------------------------
+
+Just install the script:
+
+ $ScriptInstallUpdate ip-addr-bridge;
+
+... and make it run from scheduler:
+
+ /system/scheduler/add name=ip-addr-bridge on-event="/system/script/run ip-addr-bridge;" start-time=startup;
+
+This will disable IP addresses on bridges without at least one running port.
+The IP address is enabled if at least one port is running.
+
+Note that IP addresses on bridges without a single port (acting as loopback
+interface) are ignored.
+
+---
+[⬅️ Go back to main README](../README.md)
+[⬆️ Go back to top](#top)
diff --git a/doc/ipsec-to-dns.md b/doc/ipsec-to-dns.md
new file mode 100644
index 0000000..0a91960
--- /dev/null
+++ b/doc/ipsec-to-dns.md
@@ -0,0 +1,57 @@
+Create DNS records for IPSec peers
+==================================
+
+[![GitHub stars](https://img.shields.io/github/stars/eworm-de/routeros-scripts?logo=GitHub&style=flat&color=red)](https://github.com/eworm-de/routeros-scripts/stargazers)
+[![GitHub forks](https://img.shields.io/github/forks/eworm-de/routeros-scripts?logo=GitHub&style=flat&color=green)](https://github.com/eworm-de/routeros-scripts/network)
+[![GitHub watchers](https://img.shields.io/github/watchers/eworm-de/routeros-scripts?logo=GitHub&style=flat&color=blue)](https://github.com/eworm-de/routeros-scripts/watchers)
+[![required RouterOS version](https://img.shields.io/badge/RouterOS-7.13-yellow?style=flat)](https://mikrotik.com/download/changelogs/)
+[![Telegram group @routeros_scripts](https://img.shields.io/badge/Telegram-%40routeros__scripts-%2326A5E4?logo=telegram&style=flat)](https://t.me/routeros_scripts)
+[![donate with PayPal](https://img.shields.io/badge/Like_it%3F-Donate!-orange?logo=githubsponsors&logoColor=orange&style=flat)](https://www.paypal.com/cgi-bin/webscr?cmd=_s-xclick&hosted_button_id=A4ZXBD6YS2W8J)
+
+[⬅️ Go back to main README](../README.md)
+
+> ℹ️ **Info**: This script can not be used on its own but requires the base
+> installation. See [main README](../README.md) for details.
+
+Description
+-----------
+
+This script adds (and removes) dns records based on IPSec peers and their
+dynamic addresses from mode-config.
+
+Requirements and installation
+-----------------------------
+
+Just install the script:
+
+ $ScriptInstallUpdate ipsec-to-dns;
+
+This script is run from scheduler:
+
+ /system/scheduler/add interval=1m name=ipsec-to-dns on-event="/system/script/run ipsec-to-dns;" start-time=startup;
+
+Configuration
+-------------
+
+On first run a disabled static dns record acting as marker (with comment
+"`--- ipsec-to-dns above ---`") is added. Move this entry to define where new
+entries are to be added.
+
+The configuration goes to `global-config-overlay`, these are the parameters:
+
+* `Domain`: the domain used for dns records
+* `HostNameInZone`: whether or not to add the ipsec/dns server's hostname
+* `PrefixInZone`: whether or not to add prefix `ipsec`
+
+> ℹ️ **Info**: Copy relevant configuration from
+> [`global-config`](../global-config.rsc) (the one without `-overlay`) to
+> your local `global-config-overlay` and modify it to your specific needs.
+
+See also
+--------
+
+* [Create DNS records for DHCP leases](dns-to-dhcp.md)
+
+---
+[⬅️ Go back to main README](../README.md)
+[⬆️ Go back to top](#top)
diff --git a/doc/ipv6-update.md b/doc/ipv6-update.md
new file mode 100644
index 0000000..20265fe
--- /dev/null
+++ b/doc/ipv6-update.md
@@ -0,0 +1,80 @@
+Update configuration on IPv6 prefix change
+==========================================
+
+[![GitHub stars](https://img.shields.io/github/stars/eworm-de/routeros-scripts?logo=GitHub&style=flat&color=red)](https://github.com/eworm-de/routeros-scripts/stargazers)
+[![GitHub forks](https://img.shields.io/github/forks/eworm-de/routeros-scripts?logo=GitHub&style=flat&color=green)](https://github.com/eworm-de/routeros-scripts/network)
+[![GitHub watchers](https://img.shields.io/github/watchers/eworm-de/routeros-scripts?logo=GitHub&style=flat&color=blue)](https://github.com/eworm-de/routeros-scripts/watchers)
+[![required RouterOS version](https://img.shields.io/badge/RouterOS-7.13-yellow?style=flat)](https://mikrotik.com/download/changelogs/)
+[![Telegram group @routeros_scripts](https://img.shields.io/badge/Telegram-%40routeros__scripts-%2326A5E4?logo=telegram&style=flat)](https://t.me/routeros_scripts)
+[![donate with PayPal](https://img.shields.io/badge/Like_it%3F-Donate!-orange?logo=githubsponsors&logoColor=orange&style=flat)](https://www.paypal.com/cgi-bin/webscr?cmd=_s-xclick&hosted_button_id=A4ZXBD6YS2W8J)
+
+[⬅️ Go back to main README](../README.md)
+
+> ℹ️ **Info**: This script can not be used on its own but requires the base
+> installation. See [main README](../README.md) for details.
+
+Description
+-----------
+
+With changing IPv6 prefix from ISP this script handles to update...
+
+* ipv6 firewall address-list (prefixes (`/64`) and host addresses (`/128`))
+* dns records
+
+Requirements and installation
+-----------------------------
+
+Just install the script:
+
+ $ScriptInstallUpdate ipv6-update;
+
+Your ISP needs to provide an IPv6 prefix, your device receives it via dhcp:
+
+ /ipv6/dhcp-client/add add-default-route=yes interface=ppp-isp pool-name=isp request=prefix script=ipv6-update;
+
+Note this already adds this script as `script`. The pool name (here: "`isp`")
+is important, we need it later.
+
+Also this expects there is an address assigned from pool to an interface:
+
+ /ipv6/address/add from-pool=isp interface=br-local;
+
+Sometimes dhcp client is stuck on reconnect and needs to be released.
+Installing [ppp-on-up](ppp-on-up.md) may solve this.
+
+Configuration
+-------------
+
+An address list entry is updated with current prefix and can be used in
+firewall rules, comment has to be "`ipv6-pool-`" and actual pool name:
+
+ /ipv6/firewall/address-list/add address=2003:cf:2f0f:de00::/56 comment=ipv6-pool-isp list=extern;
+
+As this entry is mandatory it is created automatically if it does not exist,
+with the comment also set for list.
+
+Address list entries for specific interfaces can be updated as well. The
+interface needs to get its address from pool `isp` and the address list entry
+has to be associated to an interface in comment:
+
+ /ipv6/firewall/address-list/add address=2003:cf:2f0f:de01::/64 comment="ipv6-pool-isp, interface=br-local" list=local;
+
+Updating address list entries with host addresses works as well, the new
+prefix is combinded with given suffix then:
+
+ /ipv6/firewall/address-list/add address=2003:cf:2f0f:de01:e3e0:f8fa:8cd6:dbe1/128 comment="ipv6-pool-isp, interface=br-local" list=hosts;
+
+Static DNS records need a special comment to be updated. Again it has to
+start with "`ipv6-pool-`" and actual pool name, followed by a comma,
+"`interface=`" and the name of interface this address is connected to:
+
+ /ip/dns/static/add address=2003:cf:2f0f:de00:1122:3344:5566:7788 comment="ipv6-pool-isp, interface=br-local" name=test.example.com ttl=15m;
+
+See also
+--------
+
+* [Run scripts on ppp connection](ppp-on-up.md)
+
+---
+[⬅️ Go back to main README](../README.md)
+[⬆️ Go back to top](#top)
diff --git a/doc/lease-script.md b/doc/lease-script.md
new file mode 100644
index 0000000..4d2f3bc
--- /dev/null
+++ b/doc/lease-script.md
@@ -0,0 +1,54 @@
+Run other scripts on DHCP lease
+===============================
+
+[![GitHub stars](https://img.shields.io/github/stars/eworm-de/routeros-scripts?logo=GitHub&style=flat&color=red)](https://github.com/eworm-de/routeros-scripts/stargazers)
+[![GitHub forks](https://img.shields.io/github/forks/eworm-de/routeros-scripts?logo=GitHub&style=flat&color=green)](https://github.com/eworm-de/routeros-scripts/network)
+[![GitHub watchers](https://img.shields.io/github/watchers/eworm-de/routeros-scripts?logo=GitHub&style=flat&color=blue)](https://github.com/eworm-de/routeros-scripts/watchers)
+[![required RouterOS version](https://img.shields.io/badge/RouterOS-7.13-yellow?style=flat)](https://mikrotik.com/download/changelogs/)
+[![Telegram group @routeros_scripts](https://img.shields.io/badge/Telegram-%40routeros__scripts-%2326A5E4?logo=telegram&style=flat)](https://t.me/routeros_scripts)
+[![donate with PayPal](https://img.shields.io/badge/Like_it%3F-Donate!-orange?logo=githubsponsors&logoColor=orange&style=flat)](https://www.paypal.com/cgi-bin/webscr?cmd=_s-xclick&hosted_button_id=A4ZXBD6YS2W8J)
+
+[⬅️ Go back to main README](../README.md)
+
+> ℹ️ **Info**: This script can not be used on its own but requires the base
+> installation. See [main README](../README.md) for details.
+
+Description
+-----------
+
+This script is supposed to run from dhcp server as lease script. On a dhcp
+lease it runs each script containing the following line, where `##` is a
+decimal number for ordering:
+
+ # provides: lease-script, order=##
+
+Currently it runs if available, in order:
+
+* [dhcp-to-dns](dhcp-to-dns.md)
+* [collect-wireless-mac](collect-wireless-mac.md)
+* [dhcp-lease-comment](dhcp-lease-comment.md)
+* `hotspot-to-wpa-cleanup`, which is an optional cleanup script
+ of [hotspot-to-wpa](hotspot-to-wpa.md)
+
+Requirements and installation
+-----------------------------
+
+Just install the script:
+
+ $ScriptInstallUpdate lease-script;
+
+... and add it as `lease-script` to your dhcp server:
+
+ /ip/dhcp-server/set lease-script=lease-script [ find ];
+
+See also
+--------
+
+* [Collect MAC addresses in wireless access list](collect-wireless-mac.md)
+* [Comment DHCP leases with info from access list](dhcp-lease-comment.md)
+* [Create DNS records for DHCP leases](dhcp-to-dns.md)
+* [Use WPA network with hotspot credentials](hotspot-to-wpa.md)
+
+---
+[⬅️ Go back to main README](../README.md)
+[⬆️ Go back to top](#top)
diff --git a/doc/leds-mode.md b/doc/leds-mode.md
new file mode 100644
index 0000000..90ea418
--- /dev/null
+++ b/doc/leds-mode.md
@@ -0,0 +1,57 @@
+Manage LEDs dark mode
+=====================
+
+[![GitHub stars](https://img.shields.io/github/stars/eworm-de/routeros-scripts?logo=GitHub&style=flat&color=red)](https://github.com/eworm-de/routeros-scripts/stargazers)
+[![GitHub forks](https://img.shields.io/github/forks/eworm-de/routeros-scripts?logo=GitHub&style=flat&color=green)](https://github.com/eworm-de/routeros-scripts/network)
+[![GitHub watchers](https://img.shields.io/github/watchers/eworm-de/routeros-scripts?logo=GitHub&style=flat&color=blue)](https://github.com/eworm-de/routeros-scripts/watchers)
+[![required RouterOS version](https://img.shields.io/badge/RouterOS-7.13-yellow?style=flat)](https://mikrotik.com/download/changelogs/)
+[![Telegram group @routeros_scripts](https://img.shields.io/badge/Telegram-%40routeros__scripts-%2326A5E4?logo=telegram&style=flat)](https://t.me/routeros_scripts)
+[![donate with PayPal](https://img.shields.io/badge/Like_it%3F-Donate!-orange?logo=githubsponsors&logoColor=orange&style=flat)](https://www.paypal.com/cgi-bin/webscr?cmd=_s-xclick&hosted_button_id=A4ZXBD6YS2W8J)
+
+[⬅️ Go back to main README](../README.md)
+
+Description
+-----------
+
+These scripts control LEDs mode and allow to run your device
+completely dark. Hardware support for dark mode is required.
+
+Requirements and installation
+-----------------------------
+
+Just install the scripts:
+
+ $ScriptInstallUpdate leds-day-mode,leds-night-mode,leds-toggle-mode;
+
+Usage and invocation
+--------------------
+
+To switch the device to dark mode:
+
+ /system/script/run leds-night-mode;
+
+... and back to normal mode:
+
+ /system/script/run leds-day-mode;
+
+To toggle between the two modes:
+
+ /system/script/run leds-toggle-mode;
+
+Add these schedulers to switch to dark mode in the evening and back to
+normal mode in the morning:
+
+ /system/scheduler/add interval=1d name=leds-day-mode on-event="/system/script/run leds-day-mode;" start-time=07:00:00;
+ /system/scheduler/add interval=1d name=leds-night-mode on-event="/system/script/run leds-night-mode;" start-time=21:00:00;
+
+The script `leds-toggle-mode` can be used from [mode button](mode-button.md)
+to toggle mode.
+
+See also
+--------
+
+* [Mode button with multiple presses](mode-button.md)
+
+---
+[⬅️ Go back to main README](../README.md)
+[⬆️ Go back to top](#top)
diff --git a/doc/log-forward.d/notification.avif b/doc/log-forward.d/notification.avif
new file mode 100644
index 0000000..a0f9ab3
--- /dev/null
+++ b/doc/log-forward.d/notification.avif
Binary files differ
diff --git a/doc/log-forward.md b/doc/log-forward.md
new file mode 100644
index 0000000..44409dc
--- /dev/null
+++ b/doc/log-forward.md
@@ -0,0 +1,95 @@
+Forward log messages via notification
+=====================================
+
+[![GitHub stars](https://img.shields.io/github/stars/eworm-de/routeros-scripts?logo=GitHub&style=flat&color=red)](https://github.com/eworm-de/routeros-scripts/stargazers)
+[![GitHub forks](https://img.shields.io/github/forks/eworm-de/routeros-scripts?logo=GitHub&style=flat&color=green)](https://github.com/eworm-de/routeros-scripts/network)
+[![GitHub watchers](https://img.shields.io/github/watchers/eworm-de/routeros-scripts?logo=GitHub&style=flat&color=blue)](https://github.com/eworm-de/routeros-scripts/watchers)
+[![required RouterOS version](https://img.shields.io/badge/RouterOS-7.13-yellow?style=flat)](https://mikrotik.com/download/changelogs/)
+[![Telegram group @routeros_scripts](https://img.shields.io/badge/Telegram-%40routeros__scripts-%2326A5E4?logo=telegram&style=flat)](https://t.me/routeros_scripts)
+[![donate with PayPal](https://img.shields.io/badge/Like_it%3F-Donate!-orange?logo=githubsponsors&logoColor=orange&style=flat)](https://www.paypal.com/cgi-bin/webscr?cmd=_s-xclick&hosted_button_id=A4ZXBD6YS2W8J)
+
+[⬅️ Go back to main README](../README.md)
+
+> ℹ️ **Info**: This script can not be used on its own but requires the base
+> installation. See [main README](../README.md) for details.
+
+Description
+-----------
+
+RouterOS itself supports sending log messages via e-mail or to a syslog
+server (see `/system/logging`). This has some limitation, however:
+
+* does not work early after boot if network connectivity is not
+ yet established, or breaks intermittently
+* lots of messages generate a flood of mails
+* Matrix and Telegram are not supported
+
+The script works around the limitations, for example it does:
+
+* read from `/log`, including messages from early boot
+* skip multi-repeated messages
+* rate-limit itself to mitigate flooding
+* forward via notification (which includes *e-mail*, *Matrix* and *Telegram*
+ when installed and configured, see below)
+
+It is intended to be run periodically from scheduler, then collects new
+log messages and forwards them via notification.
+
+### Sample notification
+
+![log-forward notification](log-forward.d/notification.avif)
+
+Requirements and installation
+-----------------------------
+
+Just install the script:
+
+ $ScriptInstallUpdate log-forward;
+
+... and add a scheduler:
+
+ /system/scheduler/add interval=1m name=log-forward on-event="/system/script/run log-forward;" start-time=startup;
+
+Configuration
+-------------
+
+The configuration goes to `global-config-overlay`, these are the parameters:
+
+* `LogForwardFilter`: define topics *not* to be forwarded
+* `LogForwardFilterMessage`: define message text *not* to be forwarded
+* `LogForwardInclude`: define topics to be forwarded (even if filter matches)
+* `LogForwardIncludeMessage`: define message text to be forwarded (even if
+ filter matches)
+
+> ℹ️ **Info**: Copy relevant configuration from
+> [`global-config`](../global-config.rsc) (the one without `-overlay`) to
+> your local `global-config-overlay` and modify it to your specific needs.
+
+These patterns are matched as
+[regular expressions](https://wiki.mikrotik.com/wiki/Manual:Regular_Expressions).
+To forward **all** (ignoring severity) log messages with topics `account`
+(which includes user logins) and `dhcp` you need something like:
+
+ :global LogForwardInclude "(account|dhcp)";
+
+Also notification settings are required for
+[e-mail](mod/notification-email.md),
+[matrix](mod/notification-matrix.md),
+[ntfy](mod/notification-ntfy.md) and/or
+[telegram](mod/notification-telegram.md).
+
+Tips & Tricks
+-------------
+
+### Notification on reboot
+
+You want to receive a notification on every device (re-)boot? Quite easy,
+just add:
+
+ :global LogForwardIncludeMessage "(^router rebooted)";
+
+This will match on every log message beginning with `router rebooted`.
+
+---
+[⬅️ Go back to main README](../README.md)
+[⬆️ Go back to top](#top)
diff --git a/doc/mod/bridge-port-to.md b/doc/mod/bridge-port-to.md
new file mode 100644
index 0000000..5c8bebc
--- /dev/null
+++ b/doc/mod/bridge-port-to.md
@@ -0,0 +1,88 @@
+Manage ports in bridge
+======================
+
+[![GitHub stars](https://img.shields.io/github/stars/eworm-de/routeros-scripts?logo=GitHub&style=flat&color=red)](https://github.com/eworm-de/routeros-scripts/stargazers)
+[![GitHub forks](https://img.shields.io/github/forks/eworm-de/routeros-scripts?logo=GitHub&style=flat&color=green)](https://github.com/eworm-de/routeros-scripts/network)
+[![GitHub watchers](https://img.shields.io/github/watchers/eworm-de/routeros-scripts?logo=GitHub&style=flat&color=blue)](https://github.com/eworm-de/routeros-scripts/watchers)
+[![required RouterOS version](https://img.shields.io/badge/RouterOS-7.13-yellow?style=flat)](https://mikrotik.com/download/changelogs/)
+[![Telegram group @routeros_scripts](https://img.shields.io/badge/Telegram-%40routeros__scripts-%2326A5E4?logo=telegram&style=flat)](https://t.me/routeros_scripts)
+[![donate with PayPal](https://img.shields.io/badge/Like_it%3F-Donate!-orange?logo=githubsponsors&logoColor=orange&style=flat)](https://www.paypal.com/cgi-bin/webscr?cmd=_s-xclick&hosted_button_id=A4ZXBD6YS2W8J)
+
+[⬅️ Go back to main README](../../README.md)
+
+> ℹ️️ **Info**: This module can not be used on its own but requires the base
+> installation. See [main README](../../README.md) for details.
+
+Description
+-----------
+
+This module and its functio are are supposed to handle interfaces and
+switching them from one bridge to another.
+
+Requirements and installation
+-----------------------------
+
+Just install the module:
+
+ $ScriptInstallUpdate mod/bridge-port-to;
+
+Configuration
+-------------
+
+The configuration goes to ports' comments (`/interface/bridge/port`).
+
+ /interface/bridge/port/add bridge=br-guest comment="default=dhcp-client, alt=br-guest" disabled=yes interface=en1;
+ /interface/bridge/port/add bridge=br-intern comment="default=br-intern, alt=br-guest" interface=en2;
+ /interface/bridge/port/add bridge=br-guest comment="default=br-guest, extra=br-extra" interface=en3;
+
+Also dhcp client can be handled:
+
+ /ip/dhcp-client/add comment="toggle with bridge port" disabled=no interface=en1;
+
+Add a scheduler to start with default setup on system startup:
+
+ $ScriptInstallUpdate global-wait;
+ /system/scheduler/add name=bridge-port-to on-event="/system/script/run global-wait; :global BridgePortTo; \$BridgePortTo default;" start-time=startup;
+
+Usage and invocation
+--------------------
+
+The usage examples show what happens with the configuration from above.
+
+Running the function `$BridgePortTo` with parameter `default` applies all
+configuration given with `default=`:
+
+ $BridgePortTo default;
+
+For the three interfaces we get this configuration:
+
+* The special value `dhcp-client` enables the dhcp client for interface `en1`. The bridge port entry is disabled.
+* Interface `en2` is put in bridge `br-intern`.
+* Interface `en3` is put in bridge `br-guest`.
+
+Running the function `$BridgePortTo` with parameter `alt` applies all
+configuration given with `alt=`:
+
+ $BridgePortTo alt;
+
+* Interface `en1` is put in bridge `br-guest`, dhcp client for the interface is disabled.
+* Interface `en2` is put in bridge `br-guest`.
+* Interface `en3` is unchanged, stays in bridge `br-guest`.
+
+Running the function `$BridgePortTo` with parameter `extra` applies another
+configuration:
+
+ $BridgePortTo extra;
+
+* Interfaces `en1` and `en2` are unchanged.
+* Interface `en3` is put in bridge `br-intern`.
+
+See also
+--------
+
+* [Wait for global functions und modules](../global-wait.md)
+* [Manage VLANs on bridge ports](bridge-port-vlan.md)
+
+---
+[⬅️ Go back to main README](../../README.md)
+[⬆️ Go back to top](#top)
diff --git a/doc/mod/bridge-port-vlan.md b/doc/mod/bridge-port-vlan.md
new file mode 100644
index 0000000..d23d5b5
--- /dev/null
+++ b/doc/mod/bridge-port-vlan.md
@@ -0,0 +1,92 @@
+Manage VLANs on bridge ports
+============================
+
+[![GitHub stars](https://img.shields.io/github/stars/eworm-de/routeros-scripts?logo=GitHub&style=flat&color=red)](https://github.com/eworm-de/routeros-scripts/stargazers)
+[![GitHub forks](https://img.shields.io/github/forks/eworm-de/routeros-scripts?logo=GitHub&style=flat&color=green)](https://github.com/eworm-de/routeros-scripts/network)
+[![GitHub watchers](https://img.shields.io/github/watchers/eworm-de/routeros-scripts?logo=GitHub&style=flat&color=blue)](https://github.com/eworm-de/routeros-scripts/watchers)
+[![required RouterOS version](https://img.shields.io/badge/RouterOS-7.13-yellow?style=flat)](https://mikrotik.com/download/changelogs/)
+[![Telegram group @routeros_scripts](https://img.shields.io/badge/Telegram-%40routeros__scripts-%2326A5E4?logo=telegram&style=flat)](https://t.me/routeros_scripts)
+[![donate with PayPal](https://img.shields.io/badge/Like_it%3F-Donate!-orange?logo=githubsponsors&logoColor=orange&style=flat)](https://www.paypal.com/cgi-bin/webscr?cmd=_s-xclick&hosted_button_id=A4ZXBD6YS2W8J)
+
+[⬅️ Go back to main README](../../README.md)
+
+> ℹ️️ **Info**: This module can not be used on its own but requires the base
+> installation. See [main README](../../README.md) for details.
+
+Description
+-----------
+
+This module and its function are supposed to handle VLANs on bridge ports.
+
+Requirements and installation
+-----------------------------
+
+Just install the module:
+
+ $ScriptInstallUpdate mod/bridge-port-vlan;
+
+Configuration
+-------------
+
+Using named VLANs you have to add comments in bridge vlan menu:
+
+ /interface/bridge/vlan/add bridge=bridge comment=intern tagged=br-local vlan-ids=10;
+ /interface/bridge/vlan/add bridge=bridge comment=geust tagged=br-local vlan-ids=20;
+ /interface/bridge/vlan/add bridge=bridge comment=extra tagged=br-local vlan-ids=30;
+
+The configuration goes to ports' comments (`/interface/bridge/port`).
+
+ /interface/bridge/port/add bridge=bridge comment="default=dhcp-client, alt=guest" disabled=yes interface=en1;
+ /interface/bridge/port/add bridge=bridge comment="default=intern, alt=guest, extra=30" interface=en2;
+ /interface/bridge/port/add bridge=bridge comment="default=guest, extra=extra" interface=en3;
+
+Also dhcp client can be handled:
+
+ /ip/dhcp-client/add comment="toggle with bridge port" disabled=no interface=en1;
+
+Add a scheduler to start with default setup on system startup:
+
+ $ScriptInstallUpdate global-wait;
+ /system/scheduler/add name=bridge-port-vlan on-event="/system/script/run global-wait; :global BridgePortVlan; \$BridgePortVlan default;" start-time=startup;
+
+Usage and invocation
+--------------------
+
+The usage examples show what happens with the configuration from above.
+
+Running the function `$BridgePortVlan` with parameter `default` applies all
+configuration given with `default=`:
+
+ $BridgePortVlan default;
+
+For the three interfaces we get this configuration:
+
+* The special value `dhcp-client` enables the dhcp client for interface `en1`. The bridge port entry is disabled.
+* Primary VLAN `intern` (ID `10`) is configured on `en2`.
+* Primary VLAN `guest` (ID `20`) is configured on `en3`.
+
+Running the function `$BridgePortVlan` with parameter `alt` applies all
+configuration given with `alt=`:
+
+ $BridgePortVlan alt;
+
+* Primary VLAN `guest` (ID `20`) is configured on `en1`, dhcp client for the interface is disabled.
+* Primary VLAN `guest` (ID `20`) is configured on `en2`.
+* Interface `en3` is unchanged, primary VLAN `guest` (ID `20`) is unchanged.
+
+Running the function `$BridgePortVlan` with parameter `extra` applies another
+configuration:
+
+* Interface `en1` is unchanged.
+* Primary VLAN `extra` (via its ID `30`) is configured on `en2`.
+* Primary VLAN `extra` (ID `30`) is configured on `en3`.
+
+See also
+--------
+
+* [Wait for global functions und modules](../global-wait.md)
+* [Manage ports in bridge](bridge-port-to.md)
+
+---
+[⬅️ Go back to main README](../../README.md)
+[⬆️ Go back to top](#top)
diff --git a/doc/mod/inspectvar.d/inspectvar.avif b/doc/mod/inspectvar.d/inspectvar.avif
new file mode 100644
index 0000000..f1da1d4
--- /dev/null
+++ b/doc/mod/inspectvar.d/inspectvar.avif
Binary files differ
diff --git a/doc/mod/inspectvar.md b/doc/mod/inspectvar.md
new file mode 100644
index 0000000..d4e59b3
--- /dev/null
+++ b/doc/mod/inspectvar.md
@@ -0,0 +1,40 @@
+Inspect variables
+=================
+
+[![GitHub stars](https://img.shields.io/github/stars/eworm-de/routeros-scripts?logo=GitHub&style=flat&color=red)](https://github.com/eworm-de/routeros-scripts/stargazers)
+[![GitHub forks](https://img.shields.io/github/forks/eworm-de/routeros-scripts?logo=GitHub&style=flat&color=green)](https://github.com/eworm-de/routeros-scripts/network)
+[![GitHub watchers](https://img.shields.io/github/watchers/eworm-de/routeros-scripts?logo=GitHub&style=flat&color=blue)](https://github.com/eworm-de/routeros-scripts/watchers)
+[![required RouterOS version](https://img.shields.io/badge/RouterOS-7.13-yellow?style=flat)](https://mikrotik.com/download/changelogs/)
+[![Telegram group @routeros_scripts](https://img.shields.io/badge/Telegram-%40routeros__scripts-%2326A5E4?logo=telegram&style=flat)](https://t.me/routeros_scripts)
+[![donate with PayPal](https://img.shields.io/badge/Like_it%3F-Donate!-orange?logo=githubsponsors&logoColor=orange&style=flat)](https://www.paypal.com/cgi-bin/webscr?cmd=_s-xclick&hosted_button_id=A4ZXBD6YS2W8J)
+
+[⬅️ Go back to main README](../../README.md)
+
+> ℹ️️ **Info**: This module can not be used on its own but requires the base
+> installation. See [main README](../../README.md) for details.
+
+Description
+-----------
+
+RouterOS handles not just scalar variables, but also arrays - even nested.
+This module adds a function to inspect variables.
+
+Requirements and installation
+-----------------------------
+
+Just install the module:
+
+ $ScriptInstallUpdate mod/inspectvar;
+
+Usage and invocation
+--------------------
+
+Call the function `$InspectVar` with a variable as parameter:
+
+ $InspectVar $ModeButton;
+
+![InspectVar](inspectvar.d/inspectvar.avif)
+
+---
+[⬅️ Go back to main README](../../README.md)
+[⬆️ Go back to top](#top)
diff --git a/doc/mod/ipcalc.d/ipcalc.avif b/doc/mod/ipcalc.d/ipcalc.avif
new file mode 100644
index 0000000..fe726e8
--- /dev/null
+++ b/doc/mod/ipcalc.d/ipcalc.avif
Binary files differ
diff --git a/doc/mod/ipcalc.d/ipcalcreturn.avif b/doc/mod/ipcalc.d/ipcalcreturn.avif
new file mode 100644
index 0000000..5e4dd57
--- /dev/null
+++ b/doc/mod/ipcalc.d/ipcalcreturn.avif
Binary files differ
diff --git a/doc/mod/ipcalc.md b/doc/mod/ipcalc.md
new file mode 100644
index 0000000..cb655bc
--- /dev/null
+++ b/doc/mod/ipcalc.md
@@ -0,0 +1,60 @@
+IP address calculation
+======================
+
+[![GitHub stars](https://img.shields.io/github/stars/eworm-de/routeros-scripts?logo=GitHub&style=flat&color=red)](https://github.com/eworm-de/routeros-scripts/stargazers)
+[![GitHub forks](https://img.shields.io/github/forks/eworm-de/routeros-scripts?logo=GitHub&style=flat&color=green)](https://github.com/eworm-de/routeros-scripts/network)
+[![GitHub watchers](https://img.shields.io/github/watchers/eworm-de/routeros-scripts?logo=GitHub&style=flat&color=blue)](https://github.com/eworm-de/routeros-scripts/watchers)
+[![required RouterOS version](https://img.shields.io/badge/RouterOS-7.13-yellow?style=flat)](https://mikrotik.com/download/changelogs/)
+[![Telegram group @routeros_scripts](https://img.shields.io/badge/Telegram-%40routeros__scripts-%2326A5E4?logo=telegram&style=flat)](https://t.me/routeros_scripts)
+[![donate with PayPal](https://img.shields.io/badge/Like_it%3F-Donate!-orange?logo=githubsponsors&logoColor=orange&style=flat)](https://www.paypal.com/cgi-bin/webscr?cmd=_s-xclick&hosted_button_id=A4ZXBD6YS2W8J)
+
+[⬅️ Go back to main README](../../README.md)
+
+> ℹ️️ **Info**: This module can not be used on its own but requires the base
+> installation. See [main README](../../README.md) for details.
+
+Description
+-----------
+
+This module adds functions for IP address calculation.
+
+Requirements and installation
+-----------------------------
+
+Just install the module:
+
+ $ScriptInstallUpdate mod/ipcalc;
+
+Usage and invocation
+--------------------
+
+### IPCalc
+
+The function `$IPCalc` prints information to terminal, including:
+
+* address
+* netmask
+* network in CIDR notation
+* minimum host address
+* maximum host address
+* broadcast address
+
+It expects an IP address in CIDR notation as argument.
+
+ $IPCalc 192.168.88.1/24;
+
+![IPCalc](ipcalc.d/ipcalc.avif)
+
+### IPCalcReturn
+
+The function `$IPCalcReturn` expects an IP address in CIDR notation as
+argument as well. But it does not print to terminal, instead it returns
+the information in a named array.
+
+ :put ([ $IPCalcReturn 192.168.88.1/24 ]->"broadcast");
+
+![IPCalcReturn](ipcalc.d/ipcalcreturn.avif)
+
+---
+[⬅️ Go back to main README](../../README.md)
+[⬆️ Go back to top](#top)
diff --git a/doc/mod/notification-email.md b/doc/mod/notification-email.md
new file mode 100644
index 0000000..2138e31
--- /dev/null
+++ b/doc/mod/notification-email.md
@@ -0,0 +1,88 @@
+Send notifications via e-mail
+=============================
+
+[![GitHub stars](https://img.shields.io/github/stars/eworm-de/routeros-scripts?logo=GitHub&style=flat&color=red)](https://github.com/eworm-de/routeros-scripts/stargazers)
+[![GitHub forks](https://img.shields.io/github/forks/eworm-de/routeros-scripts?logo=GitHub&style=flat&color=green)](https://github.com/eworm-de/routeros-scripts/network)
+[![GitHub watchers](https://img.shields.io/github/watchers/eworm-de/routeros-scripts?logo=GitHub&style=flat&color=blue)](https://github.com/eworm-de/routeros-scripts/watchers)
+[![required RouterOS version](https://img.shields.io/badge/RouterOS-7.13-yellow?style=flat)](https://mikrotik.com/download/changelogs/)
+[![Telegram group @routeros_scripts](https://img.shields.io/badge/Telegram-%40routeros__scripts-%2326A5E4?logo=telegram&style=flat)](https://t.me/routeros_scripts)
+[![donate with PayPal](https://img.shields.io/badge/Like_it%3F-Donate!-orange?logo=githubsponsors&logoColor=orange&style=flat)](https://www.paypal.com/cgi-bin/webscr?cmd=_s-xclick&hosted_button_id=A4ZXBD6YS2W8J)
+
+[⬅️ Go back to main README](../../README.md)
+
+> ℹ️️ **Info**: This module can not be used on its own but requires the base
+> installation. See [main README](../../README.md) for details.
+
+Description
+-----------
+
+This module adds support for sending notifications via e-mail. A queue is
+used to make sure notifications are not lost on failure but sent later.
+
+Requirements and installation
+-----------------------------
+
+Just install the module:
+
+ $ScriptInstallUpdate mod/notification-email;
+
+Also you need a valid e-mail account with smtp login credentials.
+
+Configuration
+-------------
+
+Set up your device's
+[e-mail settings](https://wiki.mikrotik.com/wiki/Manual:Tools/email).
+Also make sure the device has correct time configured, best is to set up
+the ntp client.
+
+Then edit `global-config-overlay`, add `EmailGeneralTo` with a valid
+recipient address. Finally reload the configuration.
+
+> ℹ️ **Info**: Copy relevant configuration from
+> [`global-config`](../../global-config.rsc) (the one without `-overlay`) to
+> your local `global-config-overlay` and modify it to your specific needs.
+
+### Sending to several recipients
+
+Sending notifications to several recipients is possible as well. Add
+`EmailGeneralCc` on top, which can have a single mail address or a comma
+separated list.
+
+Usage and invocation
+--------------------
+
+There's nothing special to do. Every script or function sending a notification
+will now send it to your e-mail account.
+
+But of course you can use the function to send notifications directly. Give
+it a try:
+
+ $SendEMail "Subject..." "Body...";
+
+Alternatively this sends a notification with all available and configured
+methods:
+
+ $SendNotification "Subject..." "Body...";
+
+To use the functions in your own scripts you have to declare them first.
+Place this before you call them:
+
+ :global SendEMail;
+ :global SendNotification;
+
+In case there is a situation when the queue needs to be purged there is a
+function available:
+
+ $PurgeEMailQueue;
+
+See also
+--------
+
+* [Send notifications via Matrix](notification-matrix.md)
+* [Send notifications via Ntfy](notification-ntfy.md)
+* [Send notifications via Telegram](notification-telegram.md)
+
+---
+[⬅️ Go back to main README](../../README.md)
+[⬆️ Go back to top](#top)
diff --git a/doc/mod/notification-matrix.d/01-authenticate.avif b/doc/mod/notification-matrix.d/01-authenticate.avif
new file mode 100644
index 0000000..b897943
--- /dev/null
+++ b/doc/mod/notification-matrix.d/01-authenticate.avif
Binary files differ
diff --git a/doc/mod/notification-matrix.d/02-join-room.avif b/doc/mod/notification-matrix.d/02-join-room.avif
new file mode 100644
index 0000000..ad99ffd
--- /dev/null
+++ b/doc/mod/notification-matrix.d/02-join-room.avif
Binary files differ
diff --git a/doc/mod/notification-matrix.md b/doc/mod/notification-matrix.md
new file mode 100644
index 0000000..92383be
--- /dev/null
+++ b/doc/mod/notification-matrix.md
@@ -0,0 +1,132 @@
+Send notifications via Matrix
+=============================
+
+[![GitHub stars](https://img.shields.io/github/stars/eworm-de/routeros-scripts?logo=GitHub&style=flat&color=red)](https://github.com/eworm-de/routeros-scripts/stargazers)
+[![GitHub forks](https://img.shields.io/github/forks/eworm-de/routeros-scripts?logo=GitHub&style=flat&color=green)](https://github.com/eworm-de/routeros-scripts/network)
+[![GitHub watchers](https://img.shields.io/github/watchers/eworm-de/routeros-scripts?logo=GitHub&style=flat&color=blue)](https://github.com/eworm-de/routeros-scripts/watchers)
+[![required RouterOS version](https://img.shields.io/badge/RouterOS-7.13-yellow?style=flat)](https://mikrotik.com/download/changelogs/)
+[![Telegram group @routeros_scripts](https://img.shields.io/badge/Telegram-%40routeros__scripts-%2326A5E4?logo=telegram&style=flat)](https://t.me/routeros_scripts)
+[![donate with PayPal](https://img.shields.io/badge/Like_it%3F-Donate!-orange?logo=githubsponsors&logoColor=orange&style=flat)](https://www.paypal.com/cgi-bin/webscr?cmd=_s-xclick&hosted_button_id=A4ZXBD6YS2W8J)
+
+[⬅️ Go back to main README](../../README.md)
+
+> ℹ️️ **Info**: This module can not be used on its own but requires the base
+> installation. See [main README](../../README.md) for details.
+
+Description
+-----------
+
+This module adds support for sending notifications via
+[Matrix](https://matrix.org/) via client server api. A queue is used to
+make sure notifications are not lost on failure but sent later.
+
+Requirements and installation
+-----------------------------
+
+Just install the module:
+
+ $ScriptInstallUpdate mod/notification-matrix;
+
+Also install a Matrix client on at least one of your mobile and/or desktop
+devices. Create and setup an account there, we will reference that as
+"*general account*" later.
+
+Configuration
+-------------
+
+Edit `global-config-overlay`, add `MatrixHomeServer`, `MatrixAccessToken` and
+`MatrixRoom` - see below on hints how to retrieve this information. Then
+reload the configuration.
+
+> ℹ️ **Info**: Copy relevant configuration from
+> [`global-config`](../../global-config.rsc) (the one without `-overlay`) to
+> your local `global-config-overlay` and modify it to your specific needs.
+
+The Matrix server is connected via encrypted https, and certificate
+verification is applied. So make sure you have the certificate chain for
+your server in device's certificate store.
+
+> ℹ️ **Info**: The *matrix.org* server uses a Cloudflare certificate. You can
+> install that with: `$CertificateAvailable "Cloudflare Inc ECC CA-3"`
+
+### From other device
+
+If you have setup your Matrix *notification account* before just reuse that.
+Copy the relevant configuration to the device to be configured.
+
+### Setup new account
+
+As there is no privilege separation you should create a dedicated account
+for use with these scripts, in addition to your *general account*.
+We will reference that as "*notification account*" in the following steps.
+
+#### Authenticate
+
+Matrix user accounts are identified by a unique user id in the form of
+`@localpart:domain`. Use that and your password to generate an access token
+and write first part of the configuration:
+
+ $SetupMatrixAuthenticate "@example:matrix.org" "v3ry-s3cr3t";
+
+![authenticate](notification-matrix.d/01-authenticate.avif)
+
+The configuration is written to a new configuration snippet
+`global-config-overlay.d/mod/notification-matrix`.
+
+#### Join Room
+
+Every Matix chat is a room, so we have to create one. Do that with your
+*general account*, this makes sure your *general account* is the room owner.
+Then join the room and invite the *notification account* by its user id
+"*@example:matrix.org*".
+Look up the *room id* within the Matrix client, it should read like
+"*!WUcxpSjKyxSGelouhA:matrix.org*" (starting with an exclamation mark and
+ending with the domain).
+
+Finally make the *notification account* join into the room by accepting
+the invite.
+
+ $SetupMatrixJoinRoom "!WUcxpSjKyxSGelouhA:matrix.org";
+
+![join room](notification-matrix.d/02-join-room.avif)
+
+The configuration is appended to the configuration snippet
+`global-config-overlay.d/mod/notification-matrix`.
+
+Usage and invocation
+--------------------
+
+There's nothing special to do. Every script or function sending a notification
+will now send it to your Matrix account.
+
+But of course you can use the function to send notifications directly. Give
+it a try:
+
+ $SendMatrix "Subject..." "Body...";
+
+Alternatively this sends a notification with all available and configured
+methods:
+
+ $SendNotification "Subject..." "Body...";
+
+To use the functions in your own scripts you have to declare them first.
+Place this before you call them:
+
+ :global SendMatrix;
+ :global SendNotification;
+
+In case there is a situation when the queue needs to be purged there is a
+function available:
+
+ $PurgeMatrixQueue;
+
+See also
+--------
+
+* [Send notifications via e-mail](notification-email.md)
+* [Send notifications via Ntfy](notification-ntfy.md)
+* [Send notifications via Telegram](notification-telegram.md)
+
+---
+[⬅️ Go back to main README](../../README.md)
+[⬆️ Go back to top](#top)
diff --git a/doc/mod/notification-ntfy.md b/doc/mod/notification-ntfy.md
new file mode 100644
index 0000000..b2330a5
--- /dev/null
+++ b/doc/mod/notification-ntfy.md
@@ -0,0 +1,91 @@
+Send notifications via Ntfy
+===========================
+
+[![GitHub stars](https://img.shields.io/github/stars/eworm-de/routeros-scripts?logo=GitHub&style=flat&color=red)](https://github.com/eworm-de/routeros-scripts/stargazers)
+[![GitHub forks](https://img.shields.io/github/forks/eworm-de/routeros-scripts?logo=GitHub&style=flat&color=green)](https://github.com/eworm-de/routeros-scripts/network)
+[![GitHub watchers](https://img.shields.io/github/watchers/eworm-de/routeros-scripts?logo=GitHub&style=flat&color=blue)](https://github.com/eworm-de/routeros-scripts/watchers)
+[![required RouterOS version](https://img.shields.io/badge/RouterOS-7.13-yellow?style=flat)](https://mikrotik.com/download/changelogs/)
+[![Telegram group @routeros_scripts](https://img.shields.io/badge/Telegram-%40routeros__scripts-%2326A5E4?logo=telegram&style=flat)](https://t.me/routeros_scripts)
+[![donate with PayPal](https://img.shields.io/badge/Like_it%3F-Donate!-orange?logo=githubsponsors&logoColor=orange&style=flat)](https://www.paypal.com/cgi-bin/webscr?cmd=_s-xclick&hosted_button_id=A4ZXBD6YS2W8J)
+
+[⬅️ Go back to main README](../../README.md)
+
+> ℹ️️ **Info**: This module can not be used on its own but requires the base
+> installation. See [main README](../../README.md) for details.
+
+Description
+-----------
+
+This module adds support for sending notifications via
+[Ntfy](https://ntfy.sh/). A queue is used to make sure
+notifications are not lost on failure but sent later.
+
+Requirements and installation
+-----------------------------
+
+Just install the module:
+
+ $ScriptInstallUpdate mod/notification-ntfy;
+
+Also install the Ntfy app on your mobile device or use the
+[web app](https://ntfy.sh/app) in a browser of your choice.
+
+Configuration
+-------------
+
+Creating an account is not required. Just choose a topic and you are good
+to go.
+
+> ⚠️ **Warning**: If you use ntfy without sign-up, the topic is essentially
+> a password, so pick something that's not easily guessable.
+
+Edit `global-config-overlay`, add `NtfyServer` (leave it unchanged, unless
+you are self-hosting the service) and `NtfyTopic` with your choosen topic.
+Then reload the configuration.
+
+> ℹ️ **Info**: Copy relevant configuration from
+> [`global-config`](../../global-config.rsc) (the one without `-overlay`) to
+> your local `global-config-overlay` and modify it to your specific needs.
+
+Using a paid account or running a server on-premises allows to add additional
+basic authentication. Configure `NtfyServerUser` and `NtfyServerPass` for this.
+Even authentication via access token is possible, adding it as password with
+a blank username.
+
+Usage and invocation
+--------------------
+
+There's nothing special to do. Every script or function sending a notification
+will now send it to your Ntfy topic.
+
+But of course you can use the function to send notifications directly. Give
+it a try:
+
+ $SendNtfy "Subject..." "Body...";
+
+Alternatively this sends a notification with all available and configured
+methods:
+
+ $SendNotification "Subject..." "Body...";
+
+To use the functions in your own scripts you have to declare them first.
+Place this before you call them:
+
+ :global SendNtfy;
+ :global SendNotification;
+
+In case there is a situation when the queue needs to be purged there is a
+function available:
+
+ $PurgeNtfyQueue;
+
+See also
+--------
+
+* [Send notifications via e-mail](notification-email.md)
+* [Send notifications via Matrix](notification-matrix.md)
+* [Send notifications via Telegram](notification-telegram.md)
+
+---
+[⬅️ Go back to main README](../../README.md)
+[⬆️ Go back to top](#top)
diff --git a/doc/mod/notification-telegram.d/newbot.avif b/doc/mod/notification-telegram.d/newbot.avif
new file mode 100644
index 0000000..1fc7355
--- /dev/null
+++ b/doc/mod/notification-telegram.d/newbot.avif
Binary files differ
diff --git a/doc/mod/notification-telegram.d/setuserpic.avif b/doc/mod/notification-telegram.d/setuserpic.avif
new file mode 100644
index 0000000..2017d20
--- /dev/null
+++ b/doc/mod/notification-telegram.d/setuserpic.avif
Binary files differ
diff --git a/doc/mod/notification-telegram.md b/doc/mod/notification-telegram.md
new file mode 100644
index 0000000..159fda9
--- /dev/null
+++ b/doc/mod/notification-telegram.md
@@ -0,0 +1,112 @@
+Send notifications via Telegram
+===============================
+
+[![GitHub stars](https://img.shields.io/github/stars/eworm-de/routeros-scripts?logo=GitHub&style=flat&color=red)](https://github.com/eworm-de/routeros-scripts/stargazers)
+[![GitHub forks](https://img.shields.io/github/forks/eworm-de/routeros-scripts?logo=GitHub&style=flat&color=green)](https://github.com/eworm-de/routeros-scripts/network)
+[![GitHub watchers](https://img.shields.io/github/watchers/eworm-de/routeros-scripts?logo=GitHub&style=flat&color=blue)](https://github.com/eworm-de/routeros-scripts/watchers)
+[![required RouterOS version](https://img.shields.io/badge/RouterOS-7.13-yellow?style=flat)](https://mikrotik.com/download/changelogs/)
+[![Telegram group @routeros_scripts](https://img.shields.io/badge/Telegram-%40routeros__scripts-%2326A5E4?logo=telegram&style=flat)](https://t.me/routeros_scripts)
+[![donate with PayPal](https://img.shields.io/badge/Like_it%3F-Donate!-orange?logo=githubsponsors&logoColor=orange&style=flat)](https://www.paypal.com/cgi-bin/webscr?cmd=_s-xclick&hosted_button_id=A4ZXBD6YS2W8J)
+
+[⬅️ Go back to main README](../../README.md)
+
+> ℹ️️ **Info**: This module can not be used on its own but requires the base
+> installation. See [main README](../../README.md) for details.
+
+Description
+-----------
+
+This module adds support for sending notifications via
+[Telegram](https://telegram.org/) via bot api. A queue is used to make sure
+notifications are not lost on failure but sent later.
+
+Requirements and installation
+-----------------------------
+
+Just install the module:
+
+ $ScriptInstallUpdate mod/notification-telegram;
+
+Also install Telegram on at least one of your mobile and/or desktop devices
+and create an account.
+
+Configuration
+-------------
+
+Open Telegram, then start a chat with [BotFather](https://t.me/BotFather) and
+create your own bot:
+
+![create new bot](notification-telegram.d/newbot.avif)
+
+Now open a chat with your bot and start it by clicking the `START` button.
+
+Open just another chat with [GetIDs Bot](https://t.me/getidsbot), again start
+with the `START` button. It will send you some information, including the
+`id`, just below `You`.
+
+Finally edit `global-config-overlay`, add `TelegramTokenId` with the token
+from *BotFather* and `TelegramChatId` with your id from *GetIDs Bot*. Then
+reload the configuration.
+
+> ℹ️ **Info**: Copy relevant configuration from
+> [`global-config`](../../global-config.rsc) (the one without `-overlay`) to
+> your local `global-config-overlay` and modify it to your specific needs.
+
+### Notifications to a group
+
+Sending notifications to a group is possible as well. Add your bot and the
+*GetIDs Bot* to a group, then use the group's id (which starts with a dash)
+for `TelegramChatId`. Then remove *GetIDs Bot* from group.
+
+Usage and invocation
+--------------------
+
+There's nothing special to do. Every script or function sending a notification
+will now send it to your Telegram account.
+
+But of course you can use the function to send notifications directly. Give
+it a try:
+
+ $SendTelegram "Subject..." "Body...";
+
+Alternatively this sends a notification with all available and configured
+methods:
+
+ $SendNotification "Subject..." "Body...";
+
+To use the functions in your own scripts you have to declare them first.
+Place this before you call them:
+
+ :global SendTelegram;
+ :global SendNotification;
+
+In case there is a situation when the queue needs to be purged there is a
+function available:
+
+ $PurgeTelegramQueue;
+
+Tips & Tricks
+-------------
+
+### Set a profile photo
+
+You can use a profile photo for your bot to make it recognizable. Open the
+chat with [BotFather](https://t.me/BotFather) and set it there.
+
+![set profile photo](notification-telegram.d/setuserpic.avif)
+
+Have a look at my
+[RouterOS-Scripts Logo Color Changer](https://git.eworm.de/cgit/routeros-scripts/plain/contrib/logo-color.html)
+to create a colored version of this scripts' logo.
+
+See also
+--------
+
+* [Chat with your router and send commands via Telegram bot](../telegram-chat.md)
+* [Send notifications via e-mail](notification-email.md)
+* [Send notifications via Matrix](notification-matrix.md)
+* [Send notifications via Ntfy](notification-ntfy.md)
+
+---
+[⬅️ Go back to main README](../../README.md)
+[⬆️ Go back to top](#top)
diff --git a/doc/mod/scriptrunonce.d/hello-world.rsc b/doc/mod/scriptrunonce.d/hello-world.rsc
new file mode 100644
index 0000000..6404781
--- /dev/null
+++ b/doc/mod/scriptrunonce.d/hello-world.rsc
@@ -0,0 +1,3 @@
+#!rsc by RouterOS
+
+:put ("Hello World from " . [ /system/identity/get name ] . "!");
diff --git a/doc/mod/scriptrunonce.d/scriptrunonce.avif b/doc/mod/scriptrunonce.d/scriptrunonce.avif
new file mode 100644
index 0000000..27ccd41
--- /dev/null
+++ b/doc/mod/scriptrunonce.d/scriptrunonce.avif
Binary files differ
diff --git a/doc/mod/scriptrunonce.md b/doc/mod/scriptrunonce.md
new file mode 100644
index 0000000..c5fa891
--- /dev/null
+++ b/doc/mod/scriptrunonce.md
@@ -0,0 +1,59 @@
+Download script and run it once
+===============================
+
+[![GitHub stars](https://img.shields.io/github/stars/eworm-de/routeros-scripts?logo=GitHub&style=flat&color=red)](https://github.com/eworm-de/routeros-scripts/stargazers)
+[![GitHub forks](https://img.shields.io/github/forks/eworm-de/routeros-scripts?logo=GitHub&style=flat&color=green)](https://github.com/eworm-de/routeros-scripts/network)
+[![GitHub watchers](https://img.shields.io/github/watchers/eworm-de/routeros-scripts?logo=GitHub&style=flat&color=blue)](https://github.com/eworm-de/routeros-scripts/watchers)
+[![required RouterOS version](https://img.shields.io/badge/RouterOS-7.13-yellow?style=flat)](https://mikrotik.com/download/changelogs/)
+[![Telegram group @routeros_scripts](https://img.shields.io/badge/Telegram-%40routeros__scripts-%2326A5E4?logo=telegram&style=flat)](https://t.me/routeros_scripts)
+[![donate with PayPal](https://img.shields.io/badge/Like_it%3F-Donate!-orange?logo=githubsponsors&logoColor=orange&style=flat)](https://www.paypal.com/cgi-bin/webscr?cmd=_s-xclick&hosted_button_id=A4ZXBD6YS2W8J)
+
+[⬅️ Go back to main README](../../README.md)
+
+> ℹ️️ **Info**: This module can not be used on its own but requires the base
+> installation. See [main README](../../README.md) for details.
+
+Description
+-----------
+
+This module adds a function that downloads a script, checks for syntax
+validity and runs it once.
+
+Requirements and installation
+-----------------------------
+
+Just install the module:
+
+ $ScriptInstallUpdate mod/scriptrunonce;
+
+Configuration
+-------------
+
+The optional configuration goes to `global-config-overlay`.
+
+* `ScriptRunOnceBaseUrl`: base url, prepended to parameter
+* `ScriptRunOnceUrlSuffix`: url suffix, appended to parameter
+
+> ℹ️ **Info**: Copy relevant configuration from
+> [`global-config`](../../global-config.rsc) (the one without `-overlay`) to
+> your local `global-config-overlay` and modify it to your specific needs.
+
+If the parameter passed to the function is not a complete URL (starting
+with protocol `ftp://`, `http://`, `https://` or `sftp://`) the base-url is
+prepended, and file extension `.rsc` and url-suffix are appended.
+
+Usage and invocation
+--------------------
+
+The function `$ScriptRunOnce` expects an URL (or name if
+`ScriptRunOnceBaseUrl` is given) pointing to a script as parameter.
+
+ $ScriptRunOnce https://git.eworm.de/cgit/routeros-scripts/plain/doc/mod/scriptrunonce.d/hello-world.rsc;
+
+![ScriptRunOnce](scriptrunonce.d/scriptrunonce.avif)
+
+Giving multiple scripts is possible, separated by comma.
+
+---
+[⬅️ Go back to main README](../../README.md)
+[⬆️ Go back to top](#top)
diff --git a/doc/mod/ssh-keys-import.md b/doc/mod/ssh-keys-import.md
new file mode 100644
index 0000000..db8e322
--- /dev/null
+++ b/doc/mod/ssh-keys-import.md
@@ -0,0 +1,73 @@
+Import ssh keys for public key authentication
+=============================================
+
+[![GitHub stars](https://img.shields.io/github/stars/eworm-de/routeros-scripts?logo=GitHub&style=flat&color=red)](https://github.com/eworm-de/routeros-scripts/stargazers)
+[![GitHub forks](https://img.shields.io/github/forks/eworm-de/routeros-scripts?logo=GitHub&style=flat&color=green)](https://github.com/eworm-de/routeros-scripts/network)
+[![GitHub watchers](https://img.shields.io/github/watchers/eworm-de/routeros-scripts?logo=GitHub&style=flat&color=blue)](https://github.com/eworm-de/routeros-scripts/watchers)
+[![required RouterOS version](https://img.shields.io/badge/RouterOS-7.13-yellow?style=flat)](https://mikrotik.com/download/changelogs/)
+[![Telegram group @routeros_scripts](https://img.shields.io/badge/Telegram-%40routeros__scripts-%2326A5E4?logo=telegram&style=flat)](https://t.me/routeros_scripts)
+[![donate with PayPal](https://img.shields.io/badge/Like_it%3F-Donate!-orange?logo=githubsponsors&logoColor=orange&style=flat)](https://www.paypal.com/cgi-bin/webscr?cmd=_s-xclick&hosted_button_id=A4ZXBD6YS2W8J)
+
+[⬅️ Go back to main README](../../README.md)
+
+> ℹ️️ **Info**: This module can not be used on its own but requires the base
+> installation. See [main README](../../README.md) for details.
+
+Description
+-----------
+
+RouterOS supports ssh login with public key authentication. The functions
+in this module help importing the keys.
+
+Requirements and installation
+-----------------------------
+
+Just install the module:
+
+ $ScriptInstallUpdate mod/ssh-keys-import;
+
+Usage and invocation
+--------------------
+
+### Import single key from terminal
+
+Call the function `$SSHKeysImport` with key and user as parameter to
+import that key:
+
+ $SSHKeysImport "ssh-rsa AAAAB3Nza...QYZk8= user" admin;
+
+Starting with RouterOS *7.12beta1* support for keys of type `ed25519` has
+been added:
+
+ $SSHKeysImport "ssh-ed25519 AAAAC3Nza...ZVugJT user" admin;
+
+The third part of the key (`user` in this example) is inherited as
+`key-owner` in RouterOS. Also the `MD5` fingerprint is recorded, this helps
+to audit and verify the available keys.
+
+> ℹ️️ **Info**: Use `ssh-keygen` to show a fingerprint of an existing public
+> key file: `ssh-keygen -l -E md5 -f ~/.ssh/id_ed25519.pub`
+
+### Import several keys from file
+
+The functions `$SSHKeysImportFile` can read an `authorized_keys`-style file
+and import all the keys. The user given to the function can be overwritting
+from comments in the file. Create a file `keys.pub` with this content:
+
+```
+ssh-ed25519 AAAAC3Nza...3OcN8A user@client
+ssh-rsa AAAAB3Nza...ozyts= worker@station
+# user=example
+ssh-rsa AAAAB3Nza...GXQVk= person@host
+```
+
+Then import it with:
+
+ $SSHKeysImportFile keys.pub admin;
+
+This will import the first two keys for user `admin` (as given to function)
+and the third one for user `example` (as defined in comment).
+
+---
+[⬅️ Go back to main README](../../README.md)
+[⬆️ Go back to top](#top)
diff --git a/doc/mode-button.md b/doc/mode-button.md
new file mode 100644
index 0000000..8734352
--- /dev/null
+++ b/doc/mode-button.md
@@ -0,0 +1,75 @@
+Mode button with multiple presses
+=================================
+
+[![GitHub stars](https://img.shields.io/github/stars/eworm-de/routeros-scripts?logo=GitHub&style=flat&color=red)](https://github.com/eworm-de/routeros-scripts/stargazers)
+[![GitHub forks](https://img.shields.io/github/forks/eworm-de/routeros-scripts?logo=GitHub&style=flat&color=green)](https://github.com/eworm-de/routeros-scripts/network)
+[![GitHub watchers](https://img.shields.io/github/watchers/eworm-de/routeros-scripts?logo=GitHub&style=flat&color=blue)](https://github.com/eworm-de/routeros-scripts/watchers)
+[![required RouterOS version](https://img.shields.io/badge/RouterOS-7.13-yellow?style=flat)](https://mikrotik.com/download/changelogs/)
+[![Telegram group @routeros_scripts](https://img.shields.io/badge/Telegram-%40routeros__scripts-%2326A5E4?logo=telegram&style=flat)](https://t.me/routeros_scripts)
+[![donate with PayPal](https://img.shields.io/badge/Like_it%3F-Donate!-orange?logo=githubsponsors&logoColor=orange&style=flat)](https://www.paypal.com/cgi-bin/webscr?cmd=_s-xclick&hosted_button_id=A4ZXBD6YS2W8J)
+
+[⬅️ Go back to main README](../README.md)
+
+> ℹ️ **Info**: This script can not be used on its own but requires the base
+> installation. See [main README](../README.md) for details.
+
+Description
+-----------
+
+This script extend the functionality of mode button. Instead of just one
+you can trigger several actions by pressing the mode button several times.
+
+The hardware needs to have a mode button, see
+`/system/routerboard/mode-button`. Starting with RouterOS 6.47beta60 you
+can configure the reset button to act the same, see
+`/system/routerboard/reset-button`.
+
+Copy this code to terminal to check:
+
+```
+:if ([ :len [ /system/routerboard/mode-button/print as-value ] ] > 0) do={
+ :put "Mode button is supported.";
+} else={
+ :if ([ :len [ /system/routerboard/reset-button/print as-value ] ] > 0) do={
+ :put "Mode button is not supported, but reset button is.";
+ } else={
+ :put "Neither mode button nor reset button is supported.";
+ }
+}
+```
+
+Requirements and installation
+-----------------------------
+
+Just install the script:
+
+ $ScriptInstallUpdate mode-button;
+
+Then configure the mode button to run `mode-button`:
+
+ /system/routerboard/mode-button/set enabled=yes on-event="/system/script/run mode-button;";
+
+To use the reset button instead:
+
+ /system/routerboard/reset-button/set enabled=yes on-event="/system/script/run mode-button;";
+
+Configuration
+-------------
+
+The configuration goes to `global-config-overlay`, these are the parameters:
+
+* `ModeButton`: an array with defined actions
+* `ModeButtonLED`: led to give visual feedback, `type` must be `on` or `off`
+
+> ℹ️ **Info**: Copy relevant configuration from
+> [`global-config`](../global-config.rsc) (the one without `-overlay`) to
+> your local `global-config-overlay` and modify it to your specific needs.
+
+Usage and invocation
+--------------------
+
+Press the mode button. 😜
+
+---
+[⬅️ Go back to main README](../README.md)
+[⬆️ Go back to top](#top)
diff --git a/doc/netwatch-dns.md b/doc/netwatch-dns.md
new file mode 100644
index 0000000..443106f
--- /dev/null
+++ b/doc/netwatch-dns.md
@@ -0,0 +1,94 @@
+Manage DNS and DoH servers from netwatch
+========================================
+
+[![GitHub stars](https://img.shields.io/github/stars/eworm-de/routeros-scripts?logo=GitHub&style=flat&color=red)](https://github.com/eworm-de/routeros-scripts/stargazers)
+[![GitHub forks](https://img.shields.io/github/forks/eworm-de/routeros-scripts?logo=GitHub&style=flat&color=green)](https://github.com/eworm-de/routeros-scripts/network)
+[![GitHub watchers](https://img.shields.io/github/watchers/eworm-de/routeros-scripts?logo=GitHub&style=flat&color=blue)](https://github.com/eworm-de/routeros-scripts/watchers)
+[![required RouterOS version](https://img.shields.io/badge/RouterOS-7.13-yellow?style=flat)](https://mikrotik.com/download/changelogs/)
+[![Telegram group @routeros_scripts](https://img.shields.io/badge/Telegram-%40routeros__scripts-%2326A5E4?logo=telegram&style=flat)](https://t.me/routeros_scripts)
+[![donate with PayPal](https://img.shields.io/badge/Like_it%3F-Donate!-orange?logo=githubsponsors&logoColor=orange&style=flat)](https://www.paypal.com/cgi-bin/webscr?cmd=_s-xclick&hosted_button_id=A4ZXBD6YS2W8J)
+
+[⬅️ Go back to main README](../README.md)
+
+> ℹ️ **Info**: This script can not be used on its own but requires the base
+> installation. See [main README](../README.md) for details.
+
+Description
+-----------
+
+This script reads server state from netwatch and manages used DNS and
+DoH (DNS over HTTPS) servers.
+
+Requirements and installation
+-----------------------------
+
+Just install the script:
+
+ $ScriptInstallUpdate netwatch-dns;
+
+Then add a scheduler to run it periodically:
+
+ /system/scheduler/add interval=1m name=netwatch-dns on-event="/system/script/run netwatch-dns;" start-time=startup;
+
+Configuration
+-------------
+
+The DNS and DoH servers to be checked have to be added to netwatch with
+specific comment:
+
+ /tool/netwatch/add comment="doh" host=1.1.1.1;
+ /tool/netwatch/add comment="dns" host=8.8.8.8;
+ /tool/netwatch/add comment="doh, dns" host=9.9.9.9;
+
+This will configure *cloudflare-dns* for DoH (`https://1.1.1.1/dnsquery`), and
+*google-dns* and *quad-nine* for regular DNS (`8.8.8.8,9.9.9.9`) if up.
+If *cloudflare-dns* is down the script will fall back to *quad-nine* for DoH.
+
+Giving a specific query url for DoH is possible:
+
+ /tool/netwatch/add comment="doh, doh-url=https://dns.nextdns.io/dns-query" host=199.247.16.158;
+
+Note that using a name in DoH url may introduce a chicken-and-egg issue!
+
+Adding a static DNS record has the same result for the url, but always
+resolves to the same address.
+
+ /ip/dns/static/add name="dns.nextdns.io" address=199.247.16.158;
+ /tool/netwatch/add comment="doh" host=199.247.16.158;
+
+Be aware that you have to keep the ip address in sync with real world
+manually!
+
+Importing a certificate automatically is possible, at least if available in
+the repository (see `certs` sub directory).
+
+ /tool/netwatch/add comment="doh, doh-cert=DigiCert Global G2 TLS RSA SHA256 2020 CA1" host=1.1.1.1;
+ /tool/netwatch/add comment="doh, doh-cert=DigiCert TLS Hybrid ECC SHA384 2020 CA1" host=9.9.9.9;
+ /tool/netwatch/add comment="doh, doh-cert=GTS CA 1C3" host=8.8.8.8;
+
+Sometimes using just one specific (possibly internal) DNS server may be
+desired, with fallback in case it fails. This is possible as well:
+
+ /tool/netwatch/add comment="dns" host=10.0.0.10;
+ /tool/netwatch/add comment="dns-fallback" host=1.1.1.1;
+
+Tips & Tricks
+-------------
+
+### Use in combination with notifications
+
+Netwatch entries can be created to work with both - this script and
+[netwatch-notify](netwatch-notify.md). Just give options for both:
+
+ /tool/netwatch/add comment="doh, notify, name=cloudflare-dns" host=1.1.1.1;
+
+Also this allows to update host address, see option `resolve`.
+
+See also
+--------
+
+* [Notify on host up and down](netwatch-notify.md)
+
+---
+[⬅️ Go back to main README](../README.md)
+[⬆️ Go back to top](#top)
diff --git a/doc/netwatch-notify.d/notification-01-down.avif b/doc/netwatch-notify.d/notification-01-down.avif
new file mode 100644
index 0000000..894fb23
--- /dev/null
+++ b/doc/netwatch-notify.d/notification-01-down.avif
Binary files differ
diff --git a/doc/netwatch-notify.d/notification-02-up.avif b/doc/netwatch-notify.d/notification-02-up.avif
new file mode 100644
index 0000000..9021a93
--- /dev/null
+++ b/doc/netwatch-notify.d/notification-02-up.avif
Binary files differ
diff --git a/doc/netwatch-notify.md b/doc/netwatch-notify.md
new file mode 100644
index 0000000..2db32bb
--- /dev/null
+++ b/doc/netwatch-notify.md
@@ -0,0 +1,189 @@
+Notify on host up and down
+==========================
+
+[![GitHub stars](https://img.shields.io/github/stars/eworm-de/routeros-scripts?logo=GitHub&style=flat&color=red)](https://github.com/eworm-de/routeros-scripts/stargazers)
+[![GitHub forks](https://img.shields.io/github/forks/eworm-de/routeros-scripts?logo=GitHub&style=flat&color=green)](https://github.com/eworm-de/routeros-scripts/network)
+[![GitHub watchers](https://img.shields.io/github/watchers/eworm-de/routeros-scripts?logo=GitHub&style=flat&color=blue)](https://github.com/eworm-de/routeros-scripts/watchers)
+[![required RouterOS version](https://img.shields.io/badge/RouterOS-7.13-yellow?style=flat)](https://mikrotik.com/download/changelogs/)
+[![Telegram group @routeros_scripts](https://img.shields.io/badge/Telegram-%40routeros__scripts-%2326A5E4?logo=telegram&style=flat)](https://t.me/routeros_scripts)
+[![donate with PayPal](https://img.shields.io/badge/Like_it%3F-Donate!-orange?logo=githubsponsors&logoColor=orange&style=flat)](https://www.paypal.com/cgi-bin/webscr?cmd=_s-xclick&hosted_button_id=A4ZXBD6YS2W8J)
+
+[⬅️ Go back to main README](../README.md)
+
+> ℹ️ **Info**: This script can not be used on its own but requires the base
+> installation. See [main README](../README.md) for details.
+
+Description
+-----------
+
+This script sends notifications about host UP and DOWN events. In comparison
+to just netwatch (`/tool/netwatch`) and its `up-script` and `down-script`
+this script implements a simple state machine and dependency model. Host
+down events are triggered only if the host is down for several checks and
+optional parent host is not down to avoid false alerts.
+
+### Sample notifications
+
+![netwatch-notify notification down](netwatch-notify.d/notification-01-down.avif)
+![netwatch-notify notification up](netwatch-notify.d/notification-02-up.avif)
+
+Requirements and installation
+-----------------------------
+
+Just install the script:
+
+ $ScriptInstallUpdate netwatch-notify;
+
+Then add a scheduler to run it periodically:
+
+ /system/scheduler/add interval=1m name=netwatch-notify on-event="/system/script/run netwatch-notify;" start-time=startup;
+
+Configuration
+-------------
+
+The hosts to be checked have to be added to netwatch with specific comment:
+
+ /tool/netwatch/add comment="notify, name=example.com" host=[ :resolve "example.com" ];
+
+Also notification settings are required for
+[e-mail](mod/notification-email.md),
+[matrix](mod/notification-matrix.md),
+[ntfy](mod/notification-ntfy.md) and/or
+[telegram](mod/notification-telegram.md).
+
+### Hooks
+
+It is possible to run an up hook command (`up-hook`) or down hook command
+(`down-hook`) when a notification is triggered. This has to be added in
+comment, note that some characters need extra escaping:
+
+ /tool/netwatch/add comment=("notify, name=device, down-hook=/interface/ethernet \\{ disable \\\"en2\\\"; enable \\\"en2\\\"; \\}") host=10.0.0.20;
+
+Also there is a `pre-down-hook` that fires at two thirds of failed checks
+required for the notification. The idea is to fix the issue before a
+notification is sent.
+
+Getting the escaping right may be troublesome. Please consider adding a
+script in `/system/script`, then running that from hook.
+
+### Count threshould
+
+The count threshould (default is 5 checks) is configurable as well:
+
+ /tool/netwatch/add comment="notify, name=example.com, count=10" host=104.18.144.11;
+
+### Parents & dependencies
+
+If the host is behind another checked host add a dependency, this will
+suppress notification if the parent host is down:
+
+ /tool/netwatch/add comment="notify, name=gateway" host=93.184.216.1;
+ /tool/netwatch/add comment="notify, name=example.com, parent=gateway" host=93.184.216.34;
+
+Note that every configured parent in a chain increases the check count
+threshould by one.
+
+### Update from DNS
+
+The host address can be updated dynamically. Give extra parameter `resolve`
+with a resolvable name:
+
+ /tool/netwatch/add comment="notify, name=example.com, resolve=example.com";
+
+This supports multiple A or AAAA records for a name just fine, even a CNAME
+to those. An update happens only if no more record with the configured host
+address is found.
+
+### No notification on host down
+
+Also suppressing the notification on host down is possible with parameter
+`no-down-notification`. This may be desired for devices that are usually
+powered off, but accessibility is of interest.
+
+ /tool/netwatch/add comment="notify, name=printer, no-down-notification" host=10.0.0.30;
+
+Go and get your coffee ☕️ before sending the print job.
+
+### No log on failed resolve
+
+A message is writting to log after three failed attemts to resolve a host.
+However this can cause some noise for hosts that are expected to have
+failures, for example when the name is dynamically added by
+[`dhcp-to-dns`](dhcp-to-dns.md). This can be suppressed:
+
+ /tool/netwatch/add comment="notify, name=client, resolve=client.dhcp.example.com, no-resolve-fail" host=10.0.0.0;
+
+### Add a note in notification
+
+For some extra information it is possible to add a text note. This is
+included verbatim into the notification.
+
+ /tool/netwatch/add comment="notify, name=example, note=Do not touch!" host=10.0.0.31;
+
+### Add a link in notification
+
+It is possible to add a link in notification, that is added below the
+formatted notification text.
+
+ /tool/netwatch/add comment="notify, name=example.com, resolve=example.com, link=https://example.com/";
+
+Tips & Tricks
+-------------
+
+### One of several hosts
+
+Sometimes it is sufficient if one of a number of hosts is available. You can
+make `netwatch-notify` check for that by adding several items with same
+`name`. Note that `count` has to be multiplied to keep the actual time.
+
+ /tool/netwatch/add comment="notify, name=service, count=10" host=10.0.0.10;
+ /tool/netwatch/add comment="notify, name=service, count=10" host=10.0.0.20;
+
+### Checking internet connectivity
+
+Sometimes you can not check your gateway for internet connectivity, for
+example when it does not respond to pings or has a dynamic address. You could
+check `1.1.1.1` (Cloudflare DNS), `9.9.9.9` (Quad-nine DNS), `8.8.8.8`
+(Google DNS) or any other reliable address that indicates internet
+connectivity.
+
+ /tool/netwatch/add comment="notify, name=internet" host=1.1.1.1;
+
+A target like this suits well to be parent for other checks.
+
+ /tool/netwatch/add comment="notify, name=example.com, parent=internet" host=93.184.216.34;
+
+### Checking specific ISP
+
+Having several ISPs for redundancy a failed link may go unnoticed without
+proper monitoring. You can use routing-mark to monitor specific connections.
+Create a route and firewall mangle rule.
+
+ /routing/table/add fib name=via-isp1;
+ /ip/route/add distance=1 gateway=isp1 routing-table=via-isp1;
+ /ip/firewall/mangle/add action=mark-routing chain=output new-routing-mark=via-isp1 dst-address=1.0.0.1 passthrough=yes;
+
+Finally monitor the address with `netwatch-notify`.
+
+ /tool/netwatch/add comment="notify, name=quad-one via isp1" host=1.0.0.1;
+
+Note that *all* traffic to the given address is routed that way. In case of
+link failure this address is not available, so use something reliable but
+non-essential. In this example the address `1.0.0.1` is used, the same service
+(Cloudflare DNS) is available at `1.1.1.1`.
+
+### Use in combination with DNS and DoH management
+
+Netwatch entries can be created to work with both - this script and
+[netwatch-dns](netwatch-dns.md). Just give options for both:
+
+ /tool/netwatch/add comment="doh, notify, name=cloudflare-dns" host=1.1.1.1;
+
+See also
+--------
+
+* [Manage DNS and DoH servers from netwatch](netwatch-dns.md)
+
+---
+[⬅️ Go back to main README](../README.md)
+[⬆️ Go back to top](#top)
diff --git a/doc/netwatch-syslog.md b/doc/netwatch-syslog.md
new file mode 100644
index 0000000..6a337d4
--- /dev/null
+++ b/doc/netwatch-syslog.md
@@ -0,0 +1,5 @@
+This script has been dropped. Filtering in firewall is advised, which should
+look something like this:
+
+ /ip/firewall/filter/add action=reject chain=output out-interface-list=WAN port=514 protocol=udp reject-with=icmp-admin-prohibited;
+ /ip/firewall/filter/add action=reject chain=forward out-interface-list=WAN port=514 protocol=udp reject-with=icmp-admin-prohibited;
diff --git a/doc/ospf-to-leds.md b/doc/ospf-to-leds.md
new file mode 100644
index 0000000..121f77b
--- /dev/null
+++ b/doc/ospf-to-leds.md
@@ -0,0 +1,44 @@
+Visualize OSPF state via LEDs
+=============================
+
+[![GitHub stars](https://img.shields.io/github/stars/eworm-de/routeros-scripts?logo=GitHub&style=flat&color=red)](https://github.com/eworm-de/routeros-scripts/stargazers)
+[![GitHub forks](https://img.shields.io/github/forks/eworm-de/routeros-scripts?logo=GitHub&style=flat&color=green)](https://github.com/eworm-de/routeros-scripts/network)
+[![GitHub watchers](https://img.shields.io/github/watchers/eworm-de/routeros-scripts?logo=GitHub&style=flat&color=blue)](https://github.com/eworm-de/routeros-scripts/watchers)
+[![required RouterOS version](https://img.shields.io/badge/RouterOS-7.13-yellow?style=flat)](https://mikrotik.com/download/changelogs/)
+[![Telegram group @routeros_scripts](https://img.shields.io/badge/Telegram-%40routeros__scripts-%2326A5E4?logo=telegram&style=flat)](https://t.me/routeros_scripts)
+[![donate with PayPal](https://img.shields.io/badge/Like_it%3F-Donate!-orange?logo=githubsponsors&logoColor=orange&style=flat)](https://www.paypal.com/cgi-bin/webscr?cmd=_s-xclick&hosted_button_id=A4ZXBD6YS2W8J)
+
+[⬅️ Go back to main README](../README.md)
+
+> ℹ️ **Info**: This script can not be used on its own but requires the base
+> installation. See [main README](../README.md) for details.
+
+Description
+-----------
+
+Physical interfaces have their state LEDs, software-defined connectivity
+does not. This script helps to visualize whether or not an OSPF instance
+is running.
+
+Requirements and installation
+-----------------------------
+
+Just install the script:
+
+ $ScriptInstallUpdate ospf-to-leds;
+
+... and add a scheduler to run the script periodically:
+
+ /system/scheduler/add interval=20s name=ospf-to-leds on-event="/system/script/run ospf-to-leds;" start-time=startup;
+
+Configuration
+-------------
+
+The configuration goes to OSPF instance's comment. To visualize state for
+instance `default` via LED `user-led` set this:
+
+ /routing/ospf/instance/set default comment="ospf-to-leds, leds=user-led";
+
+---
+[⬅️ Go back to main README](../README.md)
+[⬆️ Go back to top](#top)
diff --git a/doc/packages-update.md b/doc/packages-update.md
new file mode 100644
index 0000000..fae3896
--- /dev/null
+++ b/doc/packages-update.md
@@ -0,0 +1,78 @@
+Manage system update
+====================
+
+[![GitHub stars](https://img.shields.io/github/stars/eworm-de/routeros-scripts?logo=GitHub&style=flat&color=red)](https://github.com/eworm-de/routeros-scripts/stargazers)
+[![GitHub forks](https://img.shields.io/github/forks/eworm-de/routeros-scripts?logo=GitHub&style=flat&color=green)](https://github.com/eworm-de/routeros-scripts/network)
+[![GitHub watchers](https://img.shields.io/github/watchers/eworm-de/routeros-scripts?logo=GitHub&style=flat&color=blue)](https://github.com/eworm-de/routeros-scripts/watchers)
+[![required RouterOS version](https://img.shields.io/badge/RouterOS-7.13-yellow?style=flat)](https://mikrotik.com/download/changelogs/)
+[![Telegram group @routeros_scripts](https://img.shields.io/badge/Telegram-%40routeros__scripts-%2326A5E4?logo=telegram&style=flat)](https://t.me/routeros_scripts)
+[![donate with PayPal](https://img.shields.io/badge/Like_it%3F-Donate!-orange?logo=githubsponsors&logoColor=orange&style=flat)](https://www.paypal.com/cgi-bin/webscr?cmd=_s-xclick&hosted_button_id=A4ZXBD6YS2W8J)
+
+[⬅️ Go back to main README](../README.md)
+
+> ℹ️ **Info**: This script can not be used on its own but requires the base
+> installation. See [main README](../README.md) for details.
+
+Description
+-----------
+
+In rare cases RouterOS fails to properly downlaod package on update
+(`/system/package/update/install`), resulting in borked system with missing
+packages. This script tries to avoid this situation by doing some basic
+verification.
+
+But it provides some extra functionality:
+
+* upload backup to Mikrotik cloud if [backup-cloud](backup-cloud.md) is
+ installed
+* send backup via e-mail if [backup-email](backup-email.md) is installed
+* save configuration to fallback partition if
+ [backup-partition](backup-partition.md) is installed
+* upload backup to server if [backup-upload](backup-upload.md) is installed
+* schedule reboot at night
+
+Requirements and installation
+-----------------------------
+
+Just install the script:
+
+ $ScriptInstallUpdate packages-update;
+
+It is automatically run by [check-routeros-update](check-routeros-update.md)
+if available.
+
+Configuration
+-------------
+
+The configuration goes to `global-config-overlay`, this is the only parameter:
+
+* `PackagesUpdateDeferReboot`: defer the reboot for night (between 3 AM
+ and 5 AM)
+
+By modifying the scheduler's `start-time` you can force the reboot at
+different time.
+
+> ℹ️ **Info**: Copy relevant configuration from
+> [`global-config`](../global-config.rsc) (the one without `-overlay`) to
+> your local `global-config-overlay` and modify it to your specific needs.
+
+Usage and invocation
+--------------------
+
+Alternatively run it manually:
+
+ /system/script/run packages-update;
+
+See also
+--------
+
+* [Upload backup to Mikrotik cloud](backup-cloud.md)
+* [Send backup via e-mail](backup-email.md)
+* [Save configuration to fallback partition](backup-partition.md)
+* [Upload backup to server](backup-upload.md)
+* [Notify on RouterOS update](check-routeros-update.md)
+* [Automatically upgrade firmware and reboot](firmware-upgrade-reboot.md)
+
+---
+[⬅️ Go back to main README](../README.md)
+[⬆️ Go back to top](#top)
diff --git a/doc/ppp-on-up.md b/doc/ppp-on-up.md
new file mode 100644
index 0000000..21847c7
--- /dev/null
+++ b/doc/ppp-on-up.md
@@ -0,0 +1,44 @@
+Run scripts on ppp connection
+=============================
+
+[![GitHub stars](https://img.shields.io/github/stars/eworm-de/routeros-scripts?logo=GitHub&style=flat&color=red)](https://github.com/eworm-de/routeros-scripts/stargazers)
+[![GitHub forks](https://img.shields.io/github/forks/eworm-de/routeros-scripts?logo=GitHub&style=flat&color=green)](https://github.com/eworm-de/routeros-scripts/network)
+[![GitHub watchers](https://img.shields.io/github/watchers/eworm-de/routeros-scripts?logo=GitHub&style=flat&color=blue)](https://github.com/eworm-de/routeros-scripts/watchers)
+[![required RouterOS version](https://img.shields.io/badge/RouterOS-7.13-yellow?style=flat)](https://mikrotik.com/download/changelogs/)
+[![Telegram group @routeros_scripts](https://img.shields.io/badge/Telegram-%40routeros__scripts-%2326A5E4?logo=telegram&style=flat)](https://t.me/routeros_scripts)
+[![donate with PayPal](https://img.shields.io/badge/Like_it%3F-Donate!-orange?logo=githubsponsors&logoColor=orange&style=flat)](https://www.paypal.com/cgi-bin/webscr?cmd=_s-xclick&hosted_button_id=A4ZXBD6YS2W8J)
+
+[⬅️ Go back to main README](../README.md)
+
+> ℹ️ **Info**: This script can not be used on its own but requires the base
+> installation. See [main README](../README.md) for details.
+
+Description
+-----------
+
+This script is supposed to run on established ppp connection. Currently
+it does:
+
+* release IPv6 dhcp leases (and thus force a renew)
+* run [update-tunnelbroker](update-tunnelbroker.md)
+
+Requirements and installation
+-----------------------------
+
+Just install the script:
+
+ $ScriptInstallUpdate ppp-on-up;
+
+... and make it the `on-up` script for ppp profile:
+
+ /ppp/profile/set on-up=ppp-on-up [ find ];
+
+See also
+--------
+
+* [Update configuration on IPv6 prefix change](ipv6-update.md)
+* [Update tunnelbroker configuration](update-tunnelbroker.md)
+
+---
+[⬅️ Go back to main README](../README.md)
+[⬆️ Go back to top](#top)
diff --git a/doc/rotate-ntp.md b/doc/rotate-ntp.md
new file mode 100644
index 0000000..9a016a3
--- /dev/null
+++ b/doc/rotate-ntp.md
@@ -0,0 +1,3 @@
+This script has been dropped as the limitation does no longer exist with
+RouterOS 7.x, where you can enable a ntp server and use a name for the client
+at the same time.
diff --git a/doc/sms-action.md b/doc/sms-action.md
new file mode 100644
index 0000000..b4678af
--- /dev/null
+++ b/doc/sms-action.md
@@ -0,0 +1,63 @@
+Act on received SMS
+===================
+
+[![GitHub stars](https://img.shields.io/github/stars/eworm-de/routeros-scripts?logo=GitHub&style=flat&color=red)](https://github.com/eworm-de/routeros-scripts/stargazers)
+[![GitHub forks](https://img.shields.io/github/forks/eworm-de/routeros-scripts?logo=GitHub&style=flat&color=green)](https://github.com/eworm-de/routeros-scripts/network)
+[![GitHub watchers](https://img.shields.io/github/watchers/eworm-de/routeros-scripts?logo=GitHub&style=flat&color=blue)](https://github.com/eworm-de/routeros-scripts/watchers)
+[![required RouterOS version](https://img.shields.io/badge/RouterOS-7.13-yellow?style=flat)](https://mikrotik.com/download/changelogs/)
+[![Telegram group @routeros_scripts](https://img.shields.io/badge/Telegram-%40routeros__scripts-%2326A5E4?logo=telegram&style=flat)](https://t.me/routeros_scripts)
+[![donate with PayPal](https://img.shields.io/badge/Like_it%3F-Donate!-orange?logo=githubsponsors&logoColor=orange&style=flat)](https://www.paypal.com/cgi-bin/webscr?cmd=_s-xclick&hosted_button_id=A4ZXBD6YS2W8J)
+
+[⬅️ Go back to main README](../README.md)
+
+> ℹ️ **Info**: This script can not be used on its own but requires the base
+> installation. See [main README](../README.md) for details.
+
+Description
+-----------
+
+RouterOS can act on received SMS. Reboot the device from remote or do
+whatever is required.
+
+A broadband interface with SMS support is required.
+
+Requirements and installation
+-----------------------------
+
+Just install the script:
+
+ $ScriptInstallUpdate sms-action;
+
+Configuration
+-------------
+
+The configuration goes to `global-config-overlay`, this is the only parameter:
+
+* `SmsAction`: an array with pre-defined actions
+
+> ℹ️ **Info**: Copy relevant configuration from
+> [`global-config`](../global-config.rsc) (the one without `-overlay`) to
+> your local `global-config-overlay` and modify it to your specific needs.
+
+Then enable SMS actions:
+
+ /tool/sms/set allowed-number=+491234567890 receive-enabled=yes secret=s3cr3t;
+
+Usage and invocation
+--------------------
+
+Send a SMS from allowed number to your device's phone number:
+
+ :cmd s3cr3t script sms-action action=reboot;
+
+The value given by "`action=`" is one of the pre-defined actions from
+`SmsAction`.
+
+See also
+--------
+
+* [Forward received SMS](sms-forward.md)
+
+---
+[⬅️ Go back to main README](../README.md)
+[⬆️ Go back to top](#top)
diff --git a/doc/sms-forward.d/notification.avif b/doc/sms-forward.d/notification.avif
new file mode 100644
index 0000000..01eb7ba
--- /dev/null
+++ b/doc/sms-forward.d/notification.avif
Binary files differ
diff --git a/doc/sms-forward.md b/doc/sms-forward.md
new file mode 100644
index 0000000..597410b
--- /dev/null
+++ b/doc/sms-forward.md
@@ -0,0 +1,98 @@
+Forward received SMS
+====================
+
+[![GitHub stars](https://img.shields.io/github/stars/eworm-de/routeros-scripts?logo=GitHub&style=flat&color=red)](https://github.com/eworm-de/routeros-scripts/stargazers)
+[![GitHub forks](https://img.shields.io/github/forks/eworm-de/routeros-scripts?logo=GitHub&style=flat&color=green)](https://github.com/eworm-de/routeros-scripts/network)
+[![GitHub watchers](https://img.shields.io/github/watchers/eworm-de/routeros-scripts?logo=GitHub&style=flat&color=blue)](https://github.com/eworm-de/routeros-scripts/watchers)
+[![required RouterOS version](https://img.shields.io/badge/RouterOS-7.13-yellow?style=flat)](https://mikrotik.com/download/changelogs/)
+[![Telegram group @routeros_scripts](https://img.shields.io/badge/Telegram-%40routeros__scripts-%2326A5E4?logo=telegram&style=flat)](https://t.me/routeros_scripts)
+[![donate with PayPal](https://img.shields.io/badge/Like_it%3F-Donate!-orange?logo=githubsponsors&logoColor=orange&style=flat)](https://www.paypal.com/cgi-bin/webscr?cmd=_s-xclick&hosted_button_id=A4ZXBD6YS2W8J)
+
+[⬅️ Go back to main README](../README.md)
+
+> ℹ️ **Info**: This script can not be used on its own but requires the base
+> installation. See [main README](../README.md) for details.
+
+Description
+-----------
+
+RouterOS can receive SMS. This script forwards SMS as notification.
+
+A broadband interface with SMS support is required.
+
+### Sample notification
+
+![sms-forward notification](sms-forward.d/notification.avif)
+
+Requirements and installation
+-----------------------------
+
+Just install the script:
+
+ $ScriptInstallUpdate sms-forward;
+
+... and add a scheduler to run it periodically:
+
+ /system/scheduler/add interval=2m name=sms-forward on-event="/system/script/run sms-forward;" start-time=startup;
+
+Configuration
+-------------
+
+You have to enable receiving of SMS:
+
+ /tool/sms/set receive-enabled=yes;
+
+The configuration goes to `global-config-overlay`, this is the only parameter:
+
+* `SmsForwardHooks`: an array with pre-defined hooks, where each hook consists
+ of `match` (which is matched against the received message), `allowed-number`
+ (which is matched against the sending phone number or name) and `command`.
+ For `match` and `allowed-number` regular expressions are supported. Actual
+ phone number (`$Phone`) and message (`$Message`) are available for the hook.
+
+> ℹ️ **Info**: Copy relevant configuration from
+> [`global-config`](../global-config.rsc) (the one without `-overlay`) to
+> your local `global-config-overlay` and modify it to your specific needs.
+
+Notification settings are required for
+[e-mail](mod/notification-email.md),
+[matrix](mod/notification-matrix.md),
+[ntfy](mod/notification-ntfy.md) and/or
+[telegram](mod/notification-telegram.md).
+
+Tips & Tricks
+-------------
+
+### Take care of harmful commands!
+
+It is easy to fake the sending phone number! So make sure you do not rely on
+that number for potentially harmful commands. Add a shared secret to match
+into the text instead, for example: `reboot-53cr3t-5tr1n9` instead of just
+`reboot`.
+
+### Order new volume
+
+Most broadband providers include a volume limit for their data plans. The
+hook functionality can be used to order new volume automatically.
+
+Let's assume an imaginary provider **ABC** sends a message when the available
+volume is about to deplete. The message is sent from `ABC` and the text
+contains the string `80%`. New volume can be ordered by sending a SMS back to
+the phone number `1234` with the text `data-plan`.
+
+ :global SmsForwardHooks {
+ { match="80%";
+ allowed-number="ABC";
+ command="/tool/sms/send lte1 phone-number=1234 message=\"data-plan\";" };
+ };
+
+Adjust the values to your own needs.
+
+See also
+--------
+
+* [Act on received SMS](sms-action.md)
+
+---
+[⬅️ Go back to main README](../README.md)
+[⬆️ Go back to top](#top)
diff --git a/doc/ssh-keys-import.md b/doc/ssh-keys-import.md
new file mode 100644
index 0000000..d1325aa
--- /dev/null
+++ b/doc/ssh-keys-import.md
@@ -0,0 +1,2 @@
+This script has been replaced by a module. Please see
+[Import ssh keys for public key authentication](mod/ssh-keys-import.md).
diff --git a/doc/super-mario-theme.md b/doc/super-mario-theme.md
new file mode 100644
index 0000000..e4bae2e
--- /dev/null
+++ b/doc/super-mario-theme.md
@@ -0,0 +1,38 @@
+Play Super Mario theme
+======================
+
+[![GitHub stars](https://img.shields.io/github/stars/eworm-de/routeros-scripts?logo=GitHub&style=flat&color=red)](https://github.com/eworm-de/routeros-scripts/stargazers)
+[![GitHub forks](https://img.shields.io/github/forks/eworm-de/routeros-scripts?logo=GitHub&style=flat&color=green)](https://github.com/eworm-de/routeros-scripts/network)
+[![GitHub watchers](https://img.shields.io/github/watchers/eworm-de/routeros-scripts?logo=GitHub&style=flat&color=blue)](https://github.com/eworm-de/routeros-scripts/watchers)
+[![required RouterOS version](https://img.shields.io/badge/RouterOS-7.13-yellow?style=flat)](https://mikrotik.com/download/changelogs/)
+[![Telegram group @routeros_scripts](https://img.shields.io/badge/Telegram-%40routeros__scripts-%2326A5E4?logo=telegram&style=flat)](https://t.me/routeros_scripts)
+[![donate with PayPal](https://img.shields.io/badge/Like_it%3F-Donate!-orange?logo=githubsponsors&logoColor=orange&style=flat)](https://www.paypal.com/cgi-bin/webscr?cmd=_s-xclick&hosted_button_id=A4ZXBD6YS2W8J)
+
+[⬅️ Go back to main README](../README.md)
+
+Description
+-----------
+
+This script plays Super Mario theme.
+
+The hardware needs a beeper.
+
+Requirements and installation
+-----------------------------
+
+Just install the script:
+
+ $ScriptInstallUpdate super-mario-theme;
+
+Usage and invocation
+--------------------
+
+Just run the script to play:
+
+ /system/script/run super-mario-theme;
+
+For extra fun use it for dhcp lease script. :)
+
+---
+[⬅️ Go back to main README](../README.md)
+[⬆️ Go back to top](#top)
diff --git a/doc/telegram-chat.d/01-chat-specific.avif b/doc/telegram-chat.d/01-chat-specific.avif
new file mode 100644
index 0000000..ab75f78
--- /dev/null
+++ b/doc/telegram-chat.d/01-chat-specific.avif
Binary files differ
diff --git a/doc/telegram-chat.d/02-chat-all.avif b/doc/telegram-chat.d/02-chat-all.avif
new file mode 100644
index 0000000..ed1a389
--- /dev/null
+++ b/doc/telegram-chat.d/02-chat-all.avif
Binary files differ
diff --git a/doc/telegram-chat.d/03-reply.avif b/doc/telegram-chat.d/03-reply.avif
new file mode 100644
index 0000000..515853e
--- /dev/null
+++ b/doc/telegram-chat.d/03-reply.avif
Binary files differ
diff --git a/doc/telegram-chat.md b/doc/telegram-chat.md
new file mode 100644
index 0000000..eb4acf5
--- /dev/null
+++ b/doc/telegram-chat.md
@@ -0,0 +1,152 @@
+Chat with your router and send commands via Telegram bot
+========================================================
+
+[![GitHub stars](https://img.shields.io/github/stars/eworm-de/routeros-scripts?logo=GitHub&style=flat&color=red)](https://github.com/eworm-de/routeros-scripts/stargazers)
+[![GitHub forks](https://img.shields.io/github/forks/eworm-de/routeros-scripts?logo=GitHub&style=flat&color=green)](https://github.com/eworm-de/routeros-scripts/network)
+[![GitHub watchers](https://img.shields.io/github/watchers/eworm-de/routeros-scripts?logo=GitHub&style=flat&color=blue)](https://github.com/eworm-de/routeros-scripts/watchers)
+[![required RouterOS version](https://img.shields.io/badge/RouterOS-7.13-yellow?style=flat)](https://mikrotik.com/download/changelogs/)
+[![Telegram group @routeros_scripts](https://img.shields.io/badge/Telegram-%40routeros__scripts-%2326A5E4?logo=telegram&style=flat)](https://t.me/routeros_scripts)
+[![donate with PayPal](https://img.shields.io/badge/Like_it%3F-Donate!-orange?logo=githubsponsors&logoColor=orange&style=flat)](https://www.paypal.com/cgi-bin/webscr?cmd=_s-xclick&hosted_button_id=A4ZXBD6YS2W8J)
+
+[⬅️ Go back to main README](../README.md)
+
+> ℹ️ **Info**: This script can not be used on its own but requires the base
+> installation. See [main README](../README.md) for details.
+
+Description
+-----------
+
+This script makes your device poll a Telegram bot for new messages. With
+these messages you can send commands to your device and make it run them.
+The resulting output is send back to you.
+
+Requirements and installation
+-----------------------------
+
+Just install the script and the module for notifications via Telegram:
+
+ $ScriptInstallUpdate telegram-chat,mod/notification-telegram;
+
+Then create a schedule that runs the script periodically:
+
+ /system/scheduler/add start-time=startup interval=30s name=telegram-chat on-event="/system/script/run telegram-chat;";
+
+> ⚠️ **Warning**: Make sure to keep the interval in sync when installing
+> on several devices. Differing polling intervals will result in missed
+> messages.
+
+Configuration
+-------------
+
+Make sure to configure
+[notifications via telegram](mod/notification-telegram.md) first. The
+additional configuration goes to `global-config-overlay`, these are the
+parameters:
+
+* `TelegramChatIdsTrusted`: an array with trusted chat ids or user names
+* `TelegramChatGroups`: define the groups a device should belong to
+
+> ℹ️ **Info**: Copy relevant configuration from
+> [`global-config`](../global-config.rsc) (the one without `-overlay`) to
+> your local `global-config-overlay` and modify it to your specific needs.
+
+Usage and invocation
+--------------------
+
+### Activating device(s)
+
+This script is capable of chatting with multiple devices. By default a
+device is passive and not acting on messages. To activate it send a message
+containing `! identity` (exclamation mark, optional space and system's
+identity). To query all dynamic ip addresses form a device named "*MikroTik*"
+send `! MikroTik`, followed by `/ip/address/print where dynamic;`.
+
+![chat to specific device](telegram-chat.d/01-chat-specific.avif)
+
+Devices can be grouped to chat with them simultaneously. The default group
+"*all*" can be activated by sending `! @all`, which will make all devices
+act on your commands.
+
+![chat to all devices](telegram-chat.d/02-chat-all.avif)
+
+Send a single exclamation mark or non-existent identity to make all
+devices passive again.
+
+### Reply to message
+
+Let's assume you received a message from a device before, and want to send
+a command to that device. No need to activate it, you can just reply to
+that message.
+
+![reply to message](telegram-chat.d/03-reply.avif)
+
+Associated messages are cleared on device reboot.
+
+### Ask for devices
+
+Send a message with a single question mark (`?`) to query for devices
+currenty online. The answer can be used for command via reply then.
+
+Known limitations
+-----------------
+
+### Do not use numeric ids!
+
+Numeric ids are valid within a session only. Usually you can use something
+like this to print all ip addresses and remove the first one:
+
+ /ip/address/print;
+ /ip/address/remove 0;
+
+This will fail when sent in separate messages. Instead you should use basic
+scripting capabilities. Try to print what you want to act on...
+
+ /ip/address/print where interface=eth;
+
+... verify and finally remove it.
+
+ /ip/address/remove [ find where interface=eth ];
+
+What does work is using the persistent ids:
+
+ /ip/address/print show-ids;
+
+The output contains an id starting with asterisk that can be used:
+
+ /ip/address/remove *E;
+
+### Mind command runtime
+
+The command is run in background while the script waits for it - about
+20 seconds at maximum. A command exceeding that time continues to run in
+background, but the output in the message is missing or truncated then.
+
+If you still want a response you can work around this by making your code
+send information on its own. Something like this should do the job:
+
+ :global SendTelegram;
+ :delay 30s;
+ $SendTelegram "Command finished" "Your command finished...";
+
+### Output size
+
+Telegram messages have a limit of 4096 characters. If output is too large it
+is truncated, and a warning is added to the message.
+
+### Sending commands to a group
+
+Adding a bot to a group allows it to send messages to that group. To allow
+it to receive messages you have to make it an admin of that group! It is
+fine to deny all permissions, though.
+
+Also adding an admin to a group can cause the group id to change, so check
+that if notifications break suddenly.
+
+See also
+--------
+
+* [Send notifications via Telegram](mod/notification-telegram.md)
+
+---
+[⬅️ Go back to main README](../README.md)
+[⬆️ Go back to top](#top)
diff --git a/doc/unattended-lte-firmware-upgrade.md b/doc/unattended-lte-firmware-upgrade.md
new file mode 100644
index 0000000..6680447
--- /dev/null
+++ b/doc/unattended-lte-firmware-upgrade.md
@@ -0,0 +1,54 @@
+Install LTE firmware upgrade
+============================
+
+[![GitHub stars](https://img.shields.io/github/stars/eworm-de/routeros-scripts?logo=GitHub&style=flat&color=red)](https://github.com/eworm-de/routeros-scripts/stargazers)
+[![GitHub forks](https://img.shields.io/github/forks/eworm-de/routeros-scripts?logo=GitHub&style=flat&color=green)](https://github.com/eworm-de/routeros-scripts/network)
+[![GitHub watchers](https://img.shields.io/github/watchers/eworm-de/routeros-scripts?logo=GitHub&style=flat&color=blue)](https://github.com/eworm-de/routeros-scripts/watchers)
+[![required RouterOS version](https://img.shields.io/badge/RouterOS-7.13-yellow?style=flat)](https://mikrotik.com/download/changelogs/)
+[![Telegram group @routeros_scripts](https://img.shields.io/badge/Telegram-%40routeros__scripts-%2326A5E4?logo=telegram&style=flat)](https://t.me/routeros_scripts)
+[![donate with PayPal](https://img.shields.io/badge/Like_it%3F-Donate!-orange?logo=githubsponsors&logoColor=orange&style=flat)](https://www.paypal.com/cgi-bin/webscr?cmd=_s-xclick&hosted_button_id=A4ZXBD6YS2W8J)
+
+[⬅️ Go back to main README](../README.md)
+
+Description
+-----------
+
+This script upgrades LTE firmware on compatible devices:
+
+* R11e-LTE
+* R11e-LTE-US
+* R11e-4G
+* R11e-LTE6
+* ... and more - probably what ever Mikrotik builds into their devices
+
+A temporary scheduler is created to be independent from terminal. Thus
+starting the upgrade process over the broadband connection is supported.
+
+Requirements and installation
+-----------------------------
+
+The firmware is downloaded over the air, so a working broadband connection
+on the lte interface to be updated is required! Having internet access from
+different gateway is not sufficient!
+
+Just install the script:
+
+ $ScriptInstallUpdate unattended-lte-firmware-upgrade;
+
+Usage and invocation
+--------------------
+
+Run the script if an upgrade for your LTE hardware is available:
+
+ /system/script/run unattended-lte-firmware-upgrade;
+
+Then be patient, go for a coffee and wait for the upgrade process to finish.
+
+See also
+--------
+
+* [Notify on LTE firmware upgrade](check-lte-firmware-upgrade.md)
+
+---
+[⬅️ Go back to main README](../README.md)
+[⬆️ Go back to top](#top)
diff --git a/doc/update-gre-address.md b/doc/update-gre-address.md
new file mode 100644
index 0000000..80902b9
--- /dev/null
+++ b/doc/update-gre-address.md
@@ -0,0 +1,48 @@
+Update GRE configuration with dynamic addresses
+===============================================
+
+[![GitHub stars](https://img.shields.io/github/stars/eworm-de/routeros-scripts?logo=GitHub&style=flat&color=red)](https://github.com/eworm-de/routeros-scripts/stargazers)
+[![GitHub forks](https://img.shields.io/github/forks/eworm-de/routeros-scripts?logo=GitHub&style=flat&color=green)](https://github.com/eworm-de/routeros-scripts/network)
+[![GitHub watchers](https://img.shields.io/github/watchers/eworm-de/routeros-scripts?logo=GitHub&style=flat&color=blue)](https://github.com/eworm-de/routeros-scripts/watchers)
+[![required RouterOS version](https://img.shields.io/badge/RouterOS-7.13-yellow?style=flat)](https://mikrotik.com/download/changelogs/)
+[![Telegram group @routeros_scripts](https://img.shields.io/badge/Telegram-%40routeros__scripts-%2326A5E4?logo=telegram&style=flat)](https://t.me/routeros_scripts)
+[![donate with PayPal](https://img.shields.io/badge/Like_it%3F-Donate!-orange?logo=githubsponsors&logoColor=orange&style=flat)](https://www.paypal.com/cgi-bin/webscr?cmd=_s-xclick&hosted_button_id=A4ZXBD6YS2W8J)
+
+[⬅️ Go back to main README](../README.md)
+
+> ℹ️ **Info**: This script can not be used on its own but requires the base
+> installation. See [main README](../README.md) for details.
+
+Description
+-----------
+
+Running a GRE tunnel over IPSec with IKEv2 is a common scenario. This is
+easy to configure on client, but has an issue on server side: client IP
+addresses are assigned dynamically via mode-config and have to be updated
+for GRE interface.
+
+This script handles the address updates and disables the interface if the
+client is disconnected.
+
+Requirements and installation
+-----------------------------
+
+Just install the script:
+
+ $ScriptInstallUpdate update-gre-address;
+
+... and add a scheduler to run the script periodically:
+
+ /system/scheduler/add interval=30s name=update-gre-address on-event="/system/script/run update-gre-address;" start-time=startup;
+
+Configuration
+-------------
+
+The configuration goes to interface's comment. Add the client's IKEv2
+certificate CN into the comment:
+
+ /interface/gre/set comment="ikev2-client1" gre-client1;
+
+---
+[⬅️ Go back to main README](../README.md)
+[⬆️ Go back to top](#top)
diff --git a/doc/update-tunnelbroker.md b/doc/update-tunnelbroker.md
new file mode 100644
index 0000000..2539e2f
--- /dev/null
+++ b/doc/update-tunnelbroker.md
@@ -0,0 +1,50 @@
+Update tunnelbroker configuration
+=================================
+
+[![GitHub stars](https://img.shields.io/github/stars/eworm-de/routeros-scripts?logo=GitHub&style=flat&color=red)](https://github.com/eworm-de/routeros-scripts/stargazers)
+[![GitHub forks](https://img.shields.io/github/forks/eworm-de/routeros-scripts?logo=GitHub&style=flat&color=green)](https://github.com/eworm-de/routeros-scripts/network)
+[![GitHub watchers](https://img.shields.io/github/watchers/eworm-de/routeros-scripts?logo=GitHub&style=flat&color=blue)](https://github.com/eworm-de/routeros-scripts/watchers)
+[![required RouterOS version](https://img.shields.io/badge/RouterOS-7.13-yellow?style=flat)](https://mikrotik.com/download/changelogs/)
+[![Telegram group @routeros_scripts](https://img.shields.io/badge/Telegram-%40routeros__scripts-%2326A5E4?logo=telegram&style=flat)](https://t.me/routeros_scripts)
+[![donate with PayPal](https://img.shields.io/badge/Like_it%3F-Donate!-orange?logo=githubsponsors&logoColor=orange&style=flat)](https://www.paypal.com/cgi-bin/webscr?cmd=_s-xclick&hosted_button_id=A4ZXBD6YS2W8J)
+
+[⬅️ Go back to main README](../README.md)
+
+> ℹ️ **Info**: This script can not be used on its own but requires the base
+> installation. See [main README](../README.md) for details.
+
+Description
+-----------
+
+Connecting to [tunnelbroker.net](//tunnelbroker.net) from dynamic public
+ip address requires the address to be sent to the remote, and to be set
+locally. This script does both.
+
+Requirements and installation
+-----------------------------
+
+Just install the script:
+
+ $ScriptInstallUpdate update-tunnelbroker;
+
+Installing [ppp-on-up](ppp-on-up.md) makes this script run when ever a ppp
+connection is established.
+
+Configuration
+-------------
+
+The configuration goes to interface's comment:
+
+ /interface/6to4/set comment="tunnelbroker, user=user, id=12345, pass=s3cr3t" tunnelbroker;
+
+You should know you user name from login. The `id` is the tunnel's numeric
+id, `pass` is the *update key* found on the tunnel's advanced tab.
+
+See also
+--------
+
+* [Run scripts on ppp connection](ppp-on-up.md)
+
+---
+[⬅️ Go back to main README](../README.md)
+[⬆️ Go back to top](#top)
diff --git a/doc/upload-backup.md b/doc/upload-backup.md
new file mode 100644
index 0000000..83c9991
--- /dev/null
+++ b/doc/upload-backup.md
@@ -0,0 +1 @@
+This script has been renamed. Please see [backup-upload](backup-upload.md).
diff --git a/email-backup b/email-backup
deleted file mode 100644
index d10acb2..0000000
--- a/email-backup
+++ /dev/null
@@ -1,53 +0,0 @@
-#!rsc
-# RouterOS script: email-backup
-# Copyright (c) 2013-2020 Christian Hesse <mail@eworm.de>
-#
-# create and email backup and config file
-
-:global Identity;
-:global Domain;
-:global EmailBackupTo;
-:global EmailBackupCc;
-:global BackupSendBinary;
-:global BackupSendExport;
-:global BackupPassword;
-
-:global CharacterReplace;
-:global DeviceInfo;
-
-:if ($BackupSendBinary != true && \
- $BackupSendExport != true) do={
- :log error ("Configured to send neither backup nor config export.");
- :error "Error: See log for details.";
-}
-
-# filename based on identity
-:local FileName [ $CharacterReplace ($Identity . "." . $Domain) "." "_" ];
-:local BackupFile "none";
-:local ConfigFile "none";
-:local Attach [ :toarray "" ];
-
-# binary backup
-:if ($BackupSendBinary = true) do={
- / system backup save encryption=aes-sha256 name=$FileName password=$BackupPassword;
- :set BackupFile ($FileName . ".backup");
- :set Attach ($Attach, $BackupFile);
-}
-
-# create configuration export
-:if ($BackupSendExport = true) do={
- / export terse file=$FileName;
- :set ConfigFile ($FileName . ".rsc");
- :set Attach ($Attach, $ConfigFile);
-}
-
-# send email with status and files
-/ tool e-mail send to=$EmailBackupTo cc=$EmailBackupCc \
- subject=("[" . $Identity . "] Backup & Config") \
- body=("See attached files for backup and config export for " . \
- $Identity . ".\n\n" . \
- [ $DeviceInfo ] . "\n\n" . \
- "Backup file: " . $BackupFile . "\n" . \
- "Config file: " . $ConfigFile) \
- file=$Attach;
-}
diff --git a/firmware-upgrade-reboot.rsc b/firmware-upgrade-reboot.rsc
new file mode 100644
index 0000000..169a2e0
--- /dev/null
+++ b/firmware-upgrade-reboot.rsc
@@ -0,0 +1,54 @@
+#!rsc by RouterOS
+# RouterOS script: firmware-upgrade-reboot
+# Copyright (c) 2022-2024 Christian Hesse <mail@eworm.de>
+# https://git.eworm.de/cgit/routeros-scripts/about/COPYING.md
+#
+# requires RouterOS, version=7.13
+#
+# install firmware upgrade, and reboot
+# https://git.eworm.de/cgit/routeros-scripts/about/doc/firmware-upgrade-reboot.md
+
+:global GlobalFunctionsReady;
+:while ($GlobalFunctionsReady != true) do={ :delay 500ms; }
+
+:do {
+ :local ScriptName [ :jobname ];
+
+ :global LogPrint;
+ :global ScriptLock;
+ :global VersionToNum;
+
+ :if ([ $ScriptLock $ScriptName ] = false) do={
+ :error false;
+ }
+
+ :local RouterBoard [ /system/routerboard/get ];
+ :if ($RouterBoard->"current-firmware" = $RouterBoard->"upgrade-firmware") do={
+ $LogPrint info $ScriptName ("Current and upgrade firmware match with version " . \
+ $RouterBoard->"current-firmware" . ".");
+ :error true;
+ }
+ :if ([ $VersionToNum ($RouterBoard->"current-firmware") ] > [ $VersionToNum ($RouterBoard->"upgrade-firmware") ]) do={
+ $LogPrint info $ScriptName ("Different firmware version is available, but it is a downgrade. Ignoring.");
+ :error true;
+ }
+
+ :if ([ /system/routerboard/settings/get auto-upgrade ] = false) do={
+ $LogPrint info $ScriptName ("Firmware version " . $RouterBoard->"upgrade-firmware" . \
+ " is available, upgrading.");
+ /system/routerboard/upgrade;
+ }
+
+ :while ([ :len [ /log/find where topics=({"system";"info";"critical"}) \
+ message="Firmware upgraded successfully, please reboot for changes to take effect!" ] ] = 0) do={
+ :delay 1s;
+ }
+
+ :local Uptime [ /system/resource/get uptime ];
+ :if ($Uptime < 1m) do={
+ :delay $Uptime;
+ }
+
+ $LogPrint info $ScriptName ("Firmware upgrade successful, rebooting.");
+ /system/reboot;
+} on-error={ }
diff --git a/fw-addr-lists.d/allow b/fw-addr-lists.d/allow
new file mode 100644
index 0000000..8b59ed7
--- /dev/null
+++ b/fw-addr-lists.d/allow
@@ -0,0 +1,3 @@
+# an ip address list for use with fw-addr-lists script
+# https://git.eworm.de/cgit/routeros-scripts/about/doc/fw-addr-lists.md
+git.eworm.de
diff --git a/fw-addr-lists.d/block b/fw-addr-lists.d/block
new file mode 100644
index 0000000..5e9fef2
--- /dev/null
+++ b/fw-addr-lists.d/block
@@ -0,0 +1,5 @@
+# an ip address list for use with fw-addr-lists script
+# https://git.eworm.de/cgit/routeros-scripts/about/doc/fw-addr-lists.md
+
+# example.net
+93.184.216.34
diff --git a/fw-addr-lists.d/mikrotik b/fw-addr-lists.d/mikrotik
new file mode 100644
index 0000000..3b31a94
--- /dev/null
+++ b/fw-addr-lists.d/mikrotik
@@ -0,0 +1,5 @@
+# AS51894 Mikrotikls SIA
+# https://bgp.he.net/AS51894
+159.148.147.0/24
+159.148.172.0/24
+2a02:610:7501::/48
diff --git a/fw-addr-lists.rsc b/fw-addr-lists.rsc
new file mode 100644
index 0000000..007282c
--- /dev/null
+++ b/fw-addr-lists.rsc
@@ -0,0 +1,183 @@
+#!rsc by RouterOS
+# RouterOS script: fw-addr-lists
+# Copyright (c) 2023-2024 Christian Hesse <mail@eworm.de>
+# https://git.eworm.de/cgit/routeros-scripts/about/COPYING.md
+#
+# requires RouterOS, version=7.13
+#
+# download, import and update firewall address-lists
+# https://git.eworm.de/cgit/routeros-scripts/about/doc/fw-addr-lists.md
+
+:global GlobalFunctionsReady;
+:while ($GlobalFunctionsReady != true) do={ :delay 500ms; }
+
+:do {
+ :local ScriptName [ :jobname ];
+
+ :global FwAddrLists;
+ :global FwAddrListTimeOut;
+
+ :global CertificateAvailable;
+ :global EitherOr;
+ :global FetchHuge;
+ :global HumanReadableNum;
+ :global LogPrint;
+ :global LogPrintOnce;
+ :global ScriptLock;
+ :global WaitFullyConnected;
+
+ :local FindDelim do={
+ :local ValidChars "0123456789.:/ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz-";
+ :for I from=0 to=[ :len $1 ] do={
+ :if ([ :typeof [ :find $ValidChars [ :pick ($1 . " ") $I ] ] ] != "num") do={
+ :return $I;
+ }
+ }
+ }
+
+ :if ([ $ScriptLock $ScriptName ] = false) do={
+ :error false;
+ }
+ $WaitFullyConnected;
+
+ :local ListComment ("managed by " . $ScriptName);
+
+ :foreach FwListName,FwList in=$FwAddrLists do={
+ :local CntAdd 0;
+ :local CntRenew 0;
+ :local CntRemove 0;
+ :local IPv4Addresses ({});
+ :local IPv6Addresses ({});
+ :local Failure false;
+
+ :foreach List in=$FwList do={
+ :local CheckCertificate false;
+ :local Data false;
+ :local TimeOut [ $EitherOr [ :totime ($List->"timeout") ] $FwAddrListTimeOut ];
+
+ :if ([ :len ($List->"cert") ] > 0) do={
+ :set CheckCertificate true;
+ :if ([ $CertificateAvailable ($List->"cert") ] = false) do={
+ $LogPrint warning $ScriptName ("Downloading required certificate (" . $FwListName . \
+ " / " . $List->"url" . ") failed, trying anyway.");
+ }
+ }
+
+ :for I from=1 to=5 do={
+ :if ($Data = false) do={
+ :set Data [ $FetchHuge $ScriptName ($List->"url") $CheckCertificate ];
+ :if ($Data = false) do={
+ :if ($I < 5) do={
+ $LogPrint debug $ScriptName ("Failed downloading for list '" . $FwListName . \
+ "', " . $I . ". try from: " . $List->"url");
+ :delay (($I * $I) . "s");
+ }
+ }
+ }
+ }
+
+ :if ($Data = false) do={
+ :set Data "";
+ :set Failure true;
+ $LogPrint warning $ScriptName ("Failed downloading for list '" . $FwListName . \
+ "' from: " . $List->"url");
+ } else={
+ $LogPrint debug $ScriptName ("Downloaded " . [ $HumanReadableNum [ :len $Data ] 1024 ] . \
+ "B for list '" . $FwListName . "' from: " . $List->"url");
+ }
+
+ :while ([ :len $Data ] != 0) do={
+ :local Line [ :pick $Data 0 [ :find $Data "\n" ] ];
+ :local Address ([ :pick $Line 0 [ $FindDelim $Line ] ] . ($List->"cidr"));
+ :do {
+ :if ($Address ~ "^[0-9]{1,3}\\.[0-9]{1,3}\\.[0-9]{1,3}\\.[0-9]{1,3}(/[0-9]{1,2})?\$") do={
+ :set ($IPv4Addresses->$Address) $TimeOut;
+ :error true;
+ }
+ :if ($Address ~ "^[0-9a-zA-Z]*:[0-9a-zA-Z:\\.]+(/[0-9]{1,3})?\$") do={
+ :set ($IPv6Addresses->$Address) $TimeOut;
+ :error true;
+ }
+ :if ($Address ~ "^[\\.a-zA-Z0-9-]+\\.[a-zA-Z]{2,}\$") do={
+ :set ($IPv4Addresses->$Address) $TimeOut;
+ :set ($IPv6Addresses->$Address) $TimeOut;
+ :error true;
+ }
+ } on-error={ }
+ :set Data [ :pick $Data ([ :len $Line ] + 1) [ :len $Data ] ];
+ }
+ }
+
+ :foreach Entry in=[ /ip/firewall/address-list/find where \
+ list=$FwListName comment=$ListComment ] do={
+ :local Address [ /ip/firewall/address-list/get $Entry address ];
+ :if ([ :typeof ($IPv4Addresses->$Address) ] = "time") do={
+ $LogPrint debug $ScriptName ("Renewing IPv4 address in list '" . $FwListName . \
+ "' with " . ($IPv4Addresses->$Address) . ": " . $Address);
+ /ip/firewall/address-list/set $Entry timeout=($IPv4Addresses->$Address);
+ :set ($IPv4Addresses->$Address);
+ :set CntRenew ($CntRenew + 1);
+ } else={
+ :if ($Failure = false) do={
+ $LogPrint debug $ScriptName ("Removing IPv4 address from list '" . $FwListName . \
+ "': " . $Address);
+ /ip/firewall/address-list/remove $Entry;
+ :set CntRemove ($CntRemove + 1);
+ }
+ }
+ }
+
+ :foreach Entry in=[ /ipv6/firewall/address-list/find where \
+ list=$FwListName comment=$ListComment ] do={
+ :local Address [ /ipv6/firewall/address-list/get $Entry address ];
+ :if ([ :typeof ($IPv6Addresses->$Address) ] = "time") do={
+ $LogPrint debug $ScriptName ("Renewing IPv6 address in list '" . $FwListName . \
+ "' with " . ($IPv6Addresses->$Address) . ": " . $Address);
+ /ipv6/firewall/address-list/set $Entry timeout=($IPv6Addresses->$Address);
+ :set ($IPv6Addresses->$Address);
+ :set CntRenew ($CntRenew + 1);
+ } else={
+ :if ($Failure = false) do={
+ $LogPrint debug $ScriptName ("Removing IPv6 address from list '" . $FwListName . \
+ "': " . $Address);
+ /ipv6/firewall/address-list/remove $Entry;
+ :set CntRemove ($CntRemove + 1);
+ }
+ }
+ }
+
+ :foreach Address,Timeout in=$IPv4Addresses do={
+ $LogPrint debug $ScriptName ("Adding IPv4 address to list '" . $FwListName . \
+ "' with " . $Timeout . ": " . $Address);
+ :do {
+ /ip/firewall/address-list/add list=$FwListName comment=$ListComment \
+ address=$Address timeout=$Timeout;
+ :set ($IPv4Addresses->$Address);
+ :set CntAdd ($CntAdd + 1);
+ } on-error={
+ $LogPrint warning $ScriptName ("Failed to add IPv4 address to list '" . $FwListName . \
+ "': " . $Address);
+ }
+ }
+
+ :foreach Address,Timeout in=$IPv6Addresses do={
+ $LogPrint debug $ScriptName ("Adding IPv6 address to list '" . $FwListName . \
+ "' with " . $Timeout . ": " . $Address);
+ :do {
+ /ipv6/firewall/address-list/add list=$FwListName comment=$ListComment \
+ address=$Address timeout=$Timeout;
+ :set ($IPv6Addresses->$Address);
+ :set CntAdd ($CntAdd + 1);
+ } on-error={
+ $LogPrint warning $ScriptName ("Failed to add IPv6 address to list '" . $FwListName . \
+ "': " . $Address);
+ }
+ }
+
+ $LogPrint info $ScriptName ("list: " . $FwListName . \
+ " (" . [ $HumanReadableNum ($CntAdd + $CntRenew) 1000 ] . ")" . \
+ " -- added: " . [ $HumanReadableNum $CntAdd 1000 ] . \
+ " - renewed: " . [ $HumanReadableNum $CntRenew 1000 ] . \
+ " - removed: " . [ $HumanReadableNum $CntRemove 1000 ]);
+ }
+} on-error={ }
diff --git a/global-config b/global-config
deleted file mode 100644
index aa125f2..0000000
--- a/global-config
+++ /dev/null
@@ -1,127 +0,0 @@
-#!rsc
-# RouterOS script: global-config
-# Copyright (c) 2013-2020 Christian Hesse <mail@eworm.de>
-#
-# global configuration
-
-# Make sure all configuration properties are up to date and this
-# value is in sync with value in script 'global-functions'!
-:global GlobalConfigVersion 10;
-
-# This is used for DNS and backup file.
-:global Domain "example.com";
-:global HostNameInZone true;
-
-# These addresses are used to send e-mails to. The to-address needs
-# to be filled; cc-address can be empty, one address or a comma
-# separated list of addresses.
-:global EmailGeneralTo "mail@example.com";
-:global EmailGeneralCc "another@example.com";
-
-# You can send Telegram notifications. Register a bot
-# and add the token and chat ids here.
-:global TelegramTokenId "";
-:global TelegramChatId "";
-#:global TelegramTokenId "123456:ABCDEF-GHI";
-#:global TelegramChatId "12345678";
-
-# This defines what backups to generate and what password to use.
-:global BackupSendBinary false;
-:global BackupSendExport true;
-:global BackupPassword "v3ry-s3cr3t";
-# These addresses are used to send backup and config export files to.
-:global EmailBackupTo "mail@example.com";
-:global EmailBackupCc "another@example.com";
-# These credentials are used to upload backup and config export files.
-# SFTP authentication is tricky, you may have to limit authentication
-# methods for your SSH server.
-:global BackupUploadUrl "sftp://example.com/backup/";
-:global BackupUploadUser "mikrotik";
-:global BackupUploadPass "v3ry-s3cr3t";
-
-# Specify an address to enable auto update to version assumed safe.
-# The configured channel (bugfix, current, release-candidate) is appended.
-:global SafeUpdateUrl "";
-#:global SafeUpdateUrl "https://example.com/ros/safe-update/";
-
-# These thresholds control when to send health notification
-# on temperature and voltage.
-:global CheckHealthTemperature {
- temperature=50;
- cpu-temperature=70;
- board-temperature1=50;
- board-temperature2=50;
-}
-:global CheckHealthVoltagePercent 10;
-
-# This controls what configuration is activated by bridge-port-to-default.
-:global BridgePortTo "default";
-
-# Access-list entries matching this comment are updated
-# with daily pseudo-random PSK.
-:global DailyPskMatchComment "Daily PSK";
-:global DailyPskSecrets {
- { "Abusive"; "Aggressive"; "Bored"; "Chemical"; "Cold";
- "Cruel"; "Curved"; "Delightful"; "Discreet"; "Elite";
- "Evasive"; "Faded"; "Flat"; "Future"; "Grandiose";
- "Hanging"; "Humorous"; "Interesting"; "Magenta";
- "Magnificent"; "Numerous"; "Optimal"; "Pathetic";
- "Possessive"; "Remarkable"; "Rightful"; "Ruthless";
- "Stale"; "Unusual"; "Useless"; "Various" };
- { "Adhesive"; "Amusing"; "Astonishing"; "Frantic";
- "Kindhearted"; "Limping"; "Roasted"; "Robust";
- "Staking"; "Thundering"; "Ultra"; "Unreal" };
- { "Belief"; "Button"; "Curtain"; "Edge"; "Jewel";
- "String"; "Whistle" }
-}
-
-# Run different commands with multiple mode-button presses.
-:global ModeButton {
- 1="/ system script run leds-toggle-mode;";
- 2=":global SendNotification; :global Identity; \$SendNotification (\"Hello...\") (\"Hello world, \" . \$Identity . \" calling!\");";
- 3="/ system shutdown;";
- 4="/ system reboot;";
- 5="/ system script run bridge-port-toggle;";
-# add more here...
-};
-
-# Run commands on SMS action.
-:global SmsAction {
- bridge-port-toggle="/ system script run bridge-port-toggle;";
- reboot="/ system reboot;";
- shutdown="/ system shutdown;";
-# add more here...
-};
-
-# This address should resolve ntp servers and is used to update
-# ntp settings. A pool can rotate servers.
-:global NtpPool "pool.ntp.org";
-
-# This is the address used to send gps data to.
-:global GpsTrackUrl "https://example.com/index.php";
-
-# Enable this to fetch scripts from given url.
-:global ScriptUpdatesFetch true;
-:global ScriptUpdatesBaseUrl "https://git.eworm.de/cgit/routeros-scripts/plain/";
-#:global ScriptUpdatesBaseUrl "https://raw.githubusercontent.com/eworm-de/routeros-scripts/master/";
-#:global ScriptUpdatesBaseUrl "https://gitlab.com/eworm-de/routeros-scripts/raw/master/";
-:global ScriptUpdatesUrlSuffix "";
-:global ScriptUpdatesIgnore {
- "global-config"
-}
-# Enable this to silence all configuration warnings.
-:global ScriptUpdatesConfigChangesIgnore false;
-# 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:
-# https://git.eworm.de/cgit/routeros-scripts/about/#donate
-# Enable this to silence donation hint.
-:global IDonate false;
-
-# Use this for certificate auto-renew
-:global CertRenewUrl "";
-#:global CertRenewUrl "https://example.com/certificates/";
-:global CertRenewPass {
- "v3ry-s3cr3t";
- "4n0th3r-s3cr3t";
-}
diff --git a/global-config-overlay b/global-config-overlay
deleted file mode 100644
index 1b07f15..0000000
--- a/global-config-overlay
+++ /dev/null
@@ -1,18 +0,0 @@
-#!rsc
-# RouterOS script: global-config-overlay
-# Copyright (c) 2013-2020 Christian Hesse <mail@eworm.de>
-#
-# global configuration, custom overlay
-
-# Make sure all configuration properties are up to date and this
-# value is in sync with value in script 'global-functions'!
-:global GlobalConfigVersion 10;
-
-# The global-config script is updated by script-updates,
-# global-config-overlay becomes an overlay for your changes.
-:global ScriptUpdatesIgnore {
- "global-config-overlay"
-}
-
-# Copy configuration from global-config here and modify it.
-
diff --git a/global-config-overlay.rsc b/global-config-overlay.rsc
new file mode 100644
index 0000000..9ffd90c
--- /dev/null
+++ b/global-config-overlay.rsc
@@ -0,0 +1,12 @@
+# Overlay for global configuration by RouterOS Scripts
+# Copyright (c) 2013-2024 Christian Hesse <mail@eworm.de>
+# https://git.eworm.de/cgit/routeros-scripts/about/COPYING.md
+#
+# global configuration, custom overlay
+# https://git.eworm.de/cgit/routeros-scripts/about/#editing-configuration
+
+# Copy relevant configuration from global-config, paste and modify it here.
+# https://git.eworm.de/cgit/routeros-scripts/about/global-config.rsc
+
+
+# End of global-config-overlay
diff --git a/global-config.changes b/global-config.changes
deleted file mode 100644
index 04ab34f..0000000
--- a/global-config.changes
+++ /dev/null
@@ -1,16 +0,0 @@
-# RouterOS global-config changes
-# Copyright (c) 2019-2020 Christian Hesse <mail@eworm.de>
-
-# Changes for global-config to be added to notification on script-updates
-:global GlobalConfigChanges {
- 1="moved variables from 'global-config' to 'global-functions' for independence";
- 2="variable names became CamelCase to work around scripting issues";
- 3="variable for certificate renew passphrase became an array to support multiple passphrases";
- 4="added option to ignore global-config changes";
- 5="split off new script 'cloud-backup' from 'email-backup'";
- 6="introduced script 'upload-backup' with new configuration parameters";
- 7="introduced script 'check-health' with new configuration parameters";
- 8="added donation hint and option to silence it";
- 9="introduced configuration overlay 'global-config-overlay'";
- 10="make health threshold for voltage configurable";
-};
diff --git a/global-config.rsc b/global-config.rsc
new file mode 100644
index 0000000..6a37c0c
--- /dev/null
+++ b/global-config.rsc
@@ -0,0 +1,262 @@
+#!rsc by RouterOS
+# RouterOS script: global-config
+# Copyright (c) 2013-2024 Christian Hesse <mail@eworm.de>
+# https://git.eworm.de/cgit/routeros-scripts/about/COPYING.md
+#
+# global configuration
+# https://git.eworm.de/cgit/routeros-scripts/about/
+
+# Set this to 'true' to disable news and change notifications.
+:global NoNewsAndChangesNotification false;
+
+# Add extra text (or emojis) in notification tags.
+:global IdentityExtra "";
+
+# This is used in DNS scripts ('ipsec-to-dns' and fallback in 'dhcp-to-dns')
+# and backup scripts for file names.
+:global Domain "example.com";
+
+# You can send e-mail notifications. Configure the system's mail settings
+# (/tool/e-mail), then install the module:
+# $ScriptInstallUpdate mod/notification-email
+# The to-address needs to be filled; cc-address can be empty, one address
+# or a comma separated list of addresses.
+:global EmailGeneralTo "";
+:global EmailGeneralCc "";
+#:global EmailGeneralTo "mail@example.com";
+#:global EmailGeneralCc "another@example.com,third@example.com";
+
+# You can send Telegram notifications. Register a bot
+# and add the token and chat ids here, then install the module:
+# $ScriptInstallUpdate mod/notification-telegram
+:global TelegramTokenId "";
+:global TelegramChatId "";
+#:global TelegramTokenId "123456:ABCDEF-GHI";
+#:global TelegramChatId "12345678";
+# Using telegram-chat you have to define trusted chat ids (not group ids!)
+# or user names. Groups allow to chat with devices simultaneously.
+#:global TelegramChatIdsTrusted {
+# "12345678";
+# "example_user";
+#};
+:global TelegramChatGroups "(all)";
+#:global TelegramChatGroups "(all|home|office)";
+
+# You can send Matrix notifications. Configure these settings and
+# install the module:
+# $ScriptInstallUpdate mod/notification-matrix
+:global MatrixHomeServer "";
+:global MatrixAccessToken "";
+:global MatrixRoom "";
+#:global MatrixHomeServer "matrix.org";
+#:global MatrixAccessToken "123456ABCDEFGHI...";
+#:global MatrixRoom "!example:matrix.org";
+
+# You can send Ntfy notifications. Configure these settings and
+# install the module:
+# $ScriptInstallUpdate mod/notification-ntfy
+:global NtfyServer "ntfy.sh";
+:global NtfyServerUser [];
+:global NtfyServerPass [];
+:global NtfyTopic "";
+
+# It is possible to override e-mail, Telegram, Matrix and Ntfy setting
+# for every script. This is done in arrays, where 'Override' is appended
+# to the variable name, like this:
+#:global EmailGeneralToOverride {
+# "check-certificates"="override@example.com";
+# "backup-email"="backup@example.com";
+#}
+
+# Toggle this to disable symbols in notifications.
+:global NotificationsWithSymbols true;
+# Toggle this to disable color output in terminal/cli.
+:global TerminalColorOutput true;
+
+# This defines what backups to generate and what password to use.
+:global BackupSendBinary false;
+:global BackupSendExport true;
+:global BackupSendGlobalConfig true;
+:global BackupPassword "v3ry-s3cr3t";
+:global BackupRandomDelay 0;
+# These credentials are used to upload backup and config export files.
+# SFTP authentication is tricky, you may have to limit authentication
+# methods for your SSH server.
+:global BackupUploadUrl "sftp://example.com/backup/";
+:global BackupUploadUser "mikrotik";
+:global BackupUploadPass "v3ry-s3cr3t";
+
+# This defines the settings for firewall address-lists (fw-addr-lists).
+:global FwAddrLists {
+# "allow"={
+# { url="https://git.eworm.de/cgit/routeros-scripts/plain/fw-addr-lists.d/allow";
+# cert="E1"; timeout=1w };
+# };
+ "block"={
+# { url="https://git.eworm.de/cgit/routeros-scripts/plain/fw-addr-lists.d/block";
+# cert="E1" };
+ { url="https://feodotracker.abuse.ch/downloads/ipblocklist_recommended.txt";
+ cert="GlobalSign Atlas R3 DV TLS CA 2022 Q3" };
+ { url="https://sslbl.abuse.ch/blacklist/sslipblacklist.txt";
+ cert="GlobalSign Atlas R3 DV TLS CA 2022 Q3" };
+ { url="https://www.dshield.org/block.txt"; cidr="/24";
+ cert="R3" };
+# { url="https://www.spamhaus.org/drop/drop.txt";
+# cert="Cloudflare Inc ECC CA-3" };
+# { url="https://www.spamhaus.org/drop/edrop.txt";
+# cert="Cloudflare Inc ECC CA-3" };
+ };
+# "mikrotik"={
+# { url="https://git.eworm.de/cgit/routeros-scripts/plain/fw-addr-lists.d/mikrotik";
+# cert="E1"; timeout=1w };
+# };
+};
+:global FwAddrListTimeOut 1d;
+
+# This defines what log messages to filter or include by topic or message
+# text. Regular expressions are supported. Do *NOT* set an empty string,
+# that will filter or include everything!
+# These are filters, so excluding messages from forwarding.
+:global LogForwardFilter "(debug|info|packet|raw)";
+:global LogForwardFilterMessage [];
+#:global LogForwardFilterMessage "message text";
+#:global LogForwardFilterMessage "(message text|another text|...)";
+# ... and another setting with reverse logic. This includes messages even
+# if filtered above.
+:global LogForwardInclude [];
+:global LogForwardIncludeMessage [];
+#:global LogForwardInclude "account";
+#:global LogForwardIncludeMessage "message text";
+
+# Specify an address to enable auto update to version assumed safe.
+# The configured channel (bugfix, current, release-candidate) is appended.
+:global SafeUpdateUrl "";
+#:global SafeUpdateUrl "https://example.com/ros/safe-update/";
+# Allow to install patch updates automatically.
+:global SafeUpdatePatch false;
+# Allow to install updates automatically if seen in neighbor list.
+:global SafeUpdateNeighbor false;
+:global SafeUpdateNeighborIdentity "";
+# Install *ALL* updates automatically!
+# Set to all upper-case "Yes, please!" to enable.
+:global SafeUpdateAll "no";
+
+# Defer the reboot for night on automatic (non-interactive) update
+:global PackagesUpdateDeferReboot false;
+
+# These thresholds control when to send health notification
+# on temperature and voltage.
+:global CheckHealthTemperature {
+ temperature=50;
+ cpu-temperature=70;
+ board-temperature1=50;
+ board-temperature2=50;
+};
+# This is deviation on recovery threshold against notification flooding.
+:global CheckHealthTemperatureDeviation 3;
+:global CheckHealthVoltageLow 115;
+:global CheckHealthVoltagePercent 10;
+
+# Access-list entries matching this comment are updated
+# with daily pseudo-random PSK.
+:global DailyPskMatchComment "Daily PSK";
+:global DailyPskQrCodeUrl "https://www.eworm.de/cgi-bin/cqrlogo-wifi.cgi";
+:global DailyPskSecrets {
+ { "Abusive"; "Aggressive"; "Bored"; "Chemical"; "Cold";
+ "Cruel"; "Curved"; "Delightful"; "Discreet"; "Elite";
+ "Evasive"; "Faded"; "Flat"; "Future"; "Grandiose";
+ "Hanging"; "Humorous"; "Interesting"; "Magenta";
+ "Magnificent"; "Numerous"; "Optimal"; "Pathetic";
+ "Possessive"; "Remarkable"; "Rightful"; "Ruthless";
+ "Stale"; "Unusual"; "Useless"; "Various" };
+ { "Adhesive"; "Amusing"; "Astonishing"; "Frantic";
+ "Kindhearted"; "Limping"; "Roasted"; "Robust";
+ "Staking"; "Thundering"; "Ultra"; "Unreal" };
+ { "Belief"; "Button"; "Curtain"; "Edge"; "Jewel";
+ "String"; "Whistle" }
+};
+
+# Specify how to assemble DNS names in ipsec-to-dns.
+:global HostNameInZone true;
+:global PrefixInZone true;
+
+# Run different commands with multiple mode-button presses.
+:global ModeButton {
+ 1="/system/script/run leds-toggle-mode;";
+ 2=":global Identity; :global SendNotification; :global SymbolForNotification; \$SendNotification ([ \$SymbolForNotification \"earth\" ] . \"Hello...\") (\"Hello world, \" . \$Identity . \" calling!\");";
+ 3="/system/shutdown;";
+ 4="/system/reboot;";
+ 5=":global BridgePortVlan; \$BridgePortVlan alt;";
+# add more here...
+};
+# This led gives visual feedback if type is 'on' or 'off'.
+:global ModeButtonLED "user-led";
+
+# Run commands on SMS action.
+:global SmsAction {
+ bridge-port-vlan-alt=":global BridgePortVlan; \$BridgePortVlan alt;";
+ reboot="/system/reboot;";
+ shutdown="/system/shutdown;";
+# add more here...
+};
+
+# Run commands by hooking into SMS forward.
+:global SmsForwardHooks {
+ { match="magic string";
+ allowed-number="12345678";
+ command="/system/script/run ..." };
+# add more here...
+};
+
+# This is the address used to send gps data to.
+:global GpsTrackUrl "https://example.com/index.php";
+
+# This is the base url to fetch scripts from.
+:global ScriptUpdatesBaseUrl "https://git.eworm.de/cgit/routeros-scripts/plain/";
+# alternative urls - main: stable code - next: currently in development
+#:global ScriptUpdatesBaseUrl "https://raw.githubusercontent.com/eworm-de/routeros-scripts/main/";
+#:global ScriptUpdatesBaseUrl "https://raw.githubusercontent.com/eworm-de/routeros-scripts/next/";
+#:global ScriptUpdatesBaseUrl "https://gitlab.com/eworm-de/routeros-scripts/raw/main/";
+#:global ScriptUpdatesBaseUrl "https://gitlab.com/eworm-de/routeros-scripts/raw/next/";
+:global ScriptUpdatesUrlSuffix "";
+# use next branch with default url (git.eworm.de)
+#:global ScriptUpdatesUrlSuffix "?h=next";
+
+# Use this for defaults with $ScriptRunOnce
+# Install module with:
+# $ScriptInstallUpdate mod/scriptrunonce
+:global ScriptRunOnceBaseUrl "";
+:global ScriptRunOnceUrlSuffix "";
+
+# 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:
+# https://git.eworm.de/cgit/routeros-scripts/about/#donate
+# Enable this to silence donation hint.
+:global IDonate false;
+
+# Use this for certificate auto-renew
+:global CertRenewUrl "";
+#:global CertRenewUrl "https://example.com/certificates/";
+:global CertRenewTime 3w;
+:global CertRenewPass {
+ "v3ry-s3cr3t";
+ "4n0th3r-s3cr3t";
+};
+:global CertWarnTime 2w;
+:global CertIssuedExportPass {
+ "cert1-cn"="v3ry-s3cr3t";
+ "cert2-cn"="4n0th3r-s3cr3t";
+};
+
+# load custom settings from overlay and snippets
+# Warning: Do *NOT* copy this code to overlay!
+:foreach Script in=([ /system/script/find where name="global-config-overlay" ], \
+ [ /system/script/find where name~"^global-config-overlay.d/" ]) do={
+ :do {
+ /system/script/run $Script;
+ } on-error={
+ :log error ("Loading configuration from overlay or snippet " . \
+ [ /system/script/get $Script name ] . " failed!");
+ }
+}
diff --git a/global-functions b/global-functions
deleted file mode 100644
index f510218..0000000
--- a/global-functions
+++ /dev/null
@@ -1,396 +0,0 @@
-#!rsc
-# RouterOS script: global-functions
-# Copyright (c) 2013-2020 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 ];
-
-# global functions
-:global UrlEncode;
-:global CharacterReplace;
-:global CertificateDownload;
-:global CertificateAvailable;
-:global SendEMail;
-:global SendTelegram;
-:global SendNotification;
-:global GetMacVendor;
-:global CleanFilePath;
-:global DownloadPackage;
-:global ScriptLock;
-:global ScriptFromTerminal;
-:global WaitForFile;
-:global ParseKeyValueStore;
-:global GetRandom;
-:global RandomDelay;
-:global DeviceInfo;
-
-# url encoding
-:set 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"; "%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 ([ :len $Replace ] > 0) do={
- :set Char ($Subs->$Replace);
- }
- :set Return ($Return . $Char);
- }
- }
-
- :return $Return;
-}
-
-# character replace
-:set 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);
-}
-
-# download and import certificate
-:set CertificateDownload do={
- :local CommonName [ :tostr $1 ];
-
- :global ScriptUpdatesBaseUrl;
- :global ScriptUpdatesUrlSuffix;
-
- :global CharacterReplace;
- :global UrlEncode;
- :global WaitForFile;
-
- :log info ("Downloading and importing certificate with " . \
- "CommonName \"" . $CommonName . "\".");
- :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;
-
- :foreach Cert in=[ / certificate find where name~("^" . $LocalFileName . "_[0-9]+\$") ] do={
- / certificate set $Cert name=[ $CharacterReplace [ $CharacterReplace [ get $Cert common-name ] " " "-" ] "---" "-" ];
- }
- } on-error={
- :log warning "Failed imprting certificate!";
- }
-}
-
-# check and download required certificate
-:set CertificateAvailable do={
- :local CommonName [ :tostr $1 ];
-
- :global CertificateDownload;
-
- :if ([ / certificate print count-only where common-name=$CommonName ] = 0) do={
- :log info ("Certificate with CommonName \"" . $CommonName . "\" not available.");
- $CertificateDownload $CommonName;
- }
-}
-
-# send notification via e-mail
-:set 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
-:set 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!
-:set 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
-:set 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
-: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;
-}
-
-# 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 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";
-
- :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={
- # catch error and fall through
- }
-
- / file remove [ find where name=$PkgDest ];
- :set Retry ($Retry - 1);
- }
-
- :return false;
-}
-
-# lock script against multiple invocation
-:set 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."
- }
-}
-
-# check if script is run from terminal
-:set ScriptFromTerminal do={
- :local Script [ :tostr $1 ];
-
- :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={
- :log debug ("Script " . $Script . " started from terminal.");
- :return true;
- }
- }
-
- :return false;
-}
-
-# wait for file to be available
-:set 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
-:set 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!
-:set 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
-:set RandomDelay do={
- :global GetRandom;
-
- :delay ([ $GetRandom $1 ] . "s");
-}
-
-# get readable device info
-:set 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={
- :local Revision "";
- :if ([ :len ($RouterBoard->"revision") ] > 0) do={
- :set Revision (" " . $RouterBoard->"revision");
- }
- :set Info ($Info . "\n" . \
- "Model: " . $RouterBoard->"model" . $Revision . "\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;
-}
diff --git a/global-functions.rsc b/global-functions.rsc
new file mode 100644
index 0000000..e8163ed
--- /dev/null
+++ b/global-functions.rsc
@@ -0,0 +1,1576 @@
+#!rsc by RouterOS
+# RouterOS script: global-functions
+# Copyright (c) 2013-2024 Christian Hesse <mail@eworm.de>
+# Michael Gisbers <michael@gisbers.de>
+# https://git.eworm.de/cgit/routeros-scripts/about/COPYING.md
+#
+# requires RouterOS, version=7.13
+#
+# global functions
+# https://git.eworm.de/cgit/routeros-scripts/about/
+
+:local ScriptName [ :jobname ];
+
+# expected configuration version
+:global ExpectedConfigVersion 127;
+
+# 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 DeviceInfo;
+:global Dos2Unix;
+:global DownloadPackage;
+:global EitherOr;
+:global EscapeForRegEx;
+:global FetchHuge;
+:global FetchUserAgentStr;
+: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 MAX;
+:global MIN;
+:global MkDir;
+:global NotificationFunctions;
+:global ParseDate;
+:global ParseKeyValueStore;
+:global PrettyPrint;
+:global ProtocolStrip;
+:global RandomDelay;
+:global RequiredRouterOS;
+: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 ];
+
+ :global CertificateDownload;
+ :global LogPrint;
+ :global ParseKeyValueStore;
+
+ :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 [ /certificate/find where common-name=$CommonName ] ] = 0) do={
+ $LogPrint info $0 ("Certificate with CommonName '" . $CommonName . "' not available.");
+ :if ([ $CertificateDownload $CommonName ] = false) do={
+ :return false;
+ }
+ }
+
+ :local CertVal [ /certificate/get [ find where common-name=$CommonName ] ];
+ :while (($CertVal->"akid") != "" && ($CertVal->"akid") != ($CertVal->"skid")) do={
+ :if ([ :len [ /certificate/find where skid=($CertVal->"akid") ] ] = 0) do={
+ $LogPrint info $0 ("Certificate chain for '" . $CommonName . \
+ "' is incomplete, missing '" . ([ $ParseKeyValueStore ($CertVal->"issuer") ]->"CN") . "'.");
+ :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 CleanName;
+ :global FetchUserAgentStr;
+ :global LogPrint;
+ :global WaitForFile;
+
+ $LogPrint info $0 ("Downloading and importing certificate with " . \
+ "CommonName '" . $CommonName . "'.");
+ :do {
+ :local FileName ([ $CleanName $CommonName ] . ".pem");
+ /tool/fetch check-certificate=yes-without-crl http-header-field=({ [ $FetchUserAgentStr $0 ] }) \
+ ($ScriptUpdatesBaseUrl . "certs/" . $FileName . $ScriptUpdatesUrlSuffix) \
+ dst-path=$FileName as-value;
+ $WaitForFile $FileName;
+ /certificate/import file-name=$FileName passphrase="" as-value;
+ :delay 1s;
+ /file/remove [ find where name=$FileName ];
+
+ :foreach Cert in=[ /certificate/find where name~("^" . $FileName . "_[0-9]+\$") ] do={
+ $CertificateNameByCN [ /certificate/get $Cert common-name ];
+ }
+ } on-error={
+ $LogPrint warning $0 ("Failed importing certificate with CommonName '" . $CommonName . "'!");
+ :return false;
+ }
+ :return true;
+}
+
+# name a certificate by its common-name
+:set CertificateNameByCN do={
+ :local CommonName [ :tostr $1 ];
+
+ :global CleanName;
+
+ :local Cert [ /certificate/find where common-name=$CommonName ];
+ /certificate/set $Cert name=[ $CleanName $CommonName ];
+}
+
+# multiply given character(s)
+:set CharacterMultiply do={
+ :local Return "";
+ :for I from=1 to=$2 do={
+ :set Return ($Return . $1);
+ }
+ :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;
+}
+
+# get readable device info
+:set DeviceInfo do={
+ :global ExpectedConfigVersion;
+ :global Identity;
+
+ :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") ] . \
+ [ $FormatLine "Board name" ($Resource->"board-name") ] . "\n" . \
+ [ $FormatLine "Architecture" ($Resource->"architecture-name") ] . "\n" . \
+ [ $IfThenElse ($RouterBoard->"routerboard" = true) \
+ ([ $FormatLine "Model" ($RouterBoard->"model") ] . \
+ [ $IfThenElse ([ :len ($RouterBoard->"revision") ] > 0) \
+ (" " . $RouterBoard->"revision") ] . "\n" . \
+ [ $FormatLine "Serial number" ($RouterBoard->"serial-number") ] . "\n") ] . \
+ [ $IfThenElse ([ :len ($License->"level") ] > 0) \
+ ([ $FormatLine "License" ($License->"level") ] . "\n") ] . \
+ "RouterOS:\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 " Version" $ExpectedConfigVersion ]);
+}
+
+# convert line endings, DOS -> UNIX
+:set Dos2Unix do={
+ :local Input [ :tostr $1 ];
+
+ :global CharacterReplace;
+
+ :return [ $CharacterReplace $Input ("\r\n") ("\n") ];
+}
+
+# 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 LogPrint;
+ :global MkDir;
+ :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 ([ :len [ /file/find where name=$PkgDest type="package" ] ] > 0) do={
+ $LogPrint info $0 ("Package file " . $PkgName . " already exists.");
+ :return true;
+ }
+
+ :if ([ $CertificateAvailable "R3" ] = 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);
+ :local Retry 3;
+ :while ($Retry > 0) do={
+ :do {
+ /tool/fetch check-certificate=yes-without-crl $Url dst-path=$PkgDest;
+ $WaitForFile $PkgDest;
+
+ :if ([ /file/get [ find where name=$PkgDest ] type ] = "package") do={
+ :return true;
+ }
+ } on-error={
+ $LogPrint debug $0 ("Downloading package file failed.");
+ }
+
+ /file/remove [ find where name=$PkgDest ];
+ :set Retry ($Retry - 1);
+ }
+
+ $LogPrint warning $0 ("Downloading package file '" . $PkgName . "' failed.");
+ :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 ];
+ }
+ :if ([ :typeof $1 ] = "time") do={
+ :return [ $IfThenElse ($1 > 0s) $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;
+}
+
+# fetch huge data to file, read in chunks
+:set FetchHuge do={
+ :local ScriptName [ :tostr $1 ];
+ :local Url [ :tostr $2 ];
+ :local CheckCert [ :tobool $3 ];
+
+ :global CleanName;
+ :global FetchUserAgentStr;
+ :global GetRandom20CharAlNum;
+ :global IfThenElse;
+ :global LogPrint;
+ :global MkDir;
+ :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 ]);
+ :do {
+ /tool/fetch check-certificate=$CheckCert $Url dst-path=$FileName \
+ http-header-field=({ [ $FetchUserAgentStr $ScriptName ] }) as-value;
+ } on-error={
+ :if ([ $WaitForFile $FileName 500ms ] = true) do={
+ /file/remove $FileName;
+ }
+ $LogPrint debug $0 ("Failed downloading from: " . $Url);
+ /file/remove $DirName;
+ :return false;
+ }
+ $WaitForFile $FileName;
+
+ :local FileSize [ /file/get $FileName size ];
+ :local Return "";
+ :local VarSize 0;
+ :while ($VarSize < $FileSize) do={
+ :set Return ($Return . ([ /file/read offset=$VarSize chunk-size=32768 file=$FileName as-value ]->"data"));
+ :set VarSize [ :len $Return ];
+ }
+ /file/remove $DirName;
+ :return $Return;
+}
+
+# generate user agent string for fetch
+:set FetchUserAgentStr do={
+ :local Caller [ :tostr $1 ];
+
+ :local Resource [ /system/resource/get ];
+
+ :return ("User-Agent: Mikrotik/" . $Resource->"version" . " " . \
+ $Resource->"architecture-name" . " " . $Caller . "/Fetch (https://rsc.eworm.de/)");
+}
+
+# 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 CA 1P5" ] = 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={
+ :do {
+ /tool/fetch check-certificate=yes-without-crl ("https://api.macvendors.com/") \
+ output=none as-value;
+ $LogPrint debug $0 ("The mac vendor is not known in database.");
+ } on-error={
+ $LogPrint warning $0 ("Failed getting mac vendor.");
+ }
+ :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 {
+ :local Line [ :pick $Input 0 [ :find $Input "\n" ] ];
+ :if ([ :typeof [ :find $Line $Pattern ] ] = "num") do={
+ :return $Line;
+ }
+ :set Input [ :pick $Input ([ :find $Input "\n" ] + 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 LogPrint;
+
+ :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;
+ }
+
+ :if ([ :typeof $IsTimeSyncResetNtp ] = "nothing") do={
+ :set IsTimeSyncResetNtp 0s;
+ }
+ :local Uptime [ /system/resource/get uptime ];
+ :if ($Uptime - $IsTimeSyncResetNtp < 3m) do={
+ :return false;
+ }
+
+ :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={
+ $LogPrint 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;
+ }
+
+ $LogPrint 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 "<unknown>" ] . ": " . $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;
+}
+
+# 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 LogPrint;
+ :global WaitForFile;
+
+ :local MkTmpfs do={
+ :global LogPrint;
+ :global WaitForFile;
+
+ :if ([ :len [ /disk/find where slot=tmpfs type=tmpfs ] ] = 1) do={
+ :return true;
+ }
+
+ $LogPrint info $0 ("Creating disk of type tmpfs.");
+ /file/remove [ find where name="tmpfs" type="directory" ];
+ :do {
+ /disk/add slot=tmpfs type=tmpfs tmpfs-max-size=([ /system/resource/get total-memory ] / 3);
+ $WaitForFile "tmpfs";
+ } on-error={
+ $LogPrint warning $0 ("Creating disk of type tmpfs failed!");
+ :return false;
+ }
+ :return true;
+ }
+
+ :set Path [ $CleanFilePath $Path ];
+
+ :if ($Path = "") do={
+ :return true;
+ }
+
+ :if ([ :len [ /file/find where name=$Path type="directory" ] ] = 1) do={
+ :return true;
+ }
+
+ :if ([ :pick $Path 0 5 ] = "tmpfs") do={
+ :if ([ $MkTmpfs ] = false) do={
+ :return false;
+ }
+ }
+
+ :do {
+ :local File ($Path . "/file");
+ /file/add name=$File;
+ $WaitForFile $File;
+ /file/remove $File;
+ } on-error={
+ $LogPrint warning $0 ("Making directory '" . $Path . "' failed!");
+ :return false;
+ }
+
+ :return true;
+}
+
+# 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 ([ :typeof $Source ] != "array") do={
+ :set Source [ :tostr $1 ];
+ }
+ :local Result ({});
+ :foreach KeyValue in=[ :toarray $Source ] do={
+ :if ([ :find $KeyValue "=" ]) do={
+ :set ($Result->[ :pick $KeyValue 0 [ :find $KeyValue "=" ] ]) \
+ [ :pick $KeyValue ([ :find $KeyValue "=" ] + 1) [ :len $KeyValue ] ];
+ } else={
+ :set ($Result->$KeyValue) true;
+ }
+ }
+ :return $Result;
+}
+
+# print lines with trailing carriage return
+:set PrettyPrint do={
+ :local Input [ :tostr $1 ];
+
+ :global Unix2Dos;
+
+ :put [ $Unix2Dos $Input ];
+}
+
+# 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;
+}
+
+# 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={
+ :local Scripts [ :toarray $1 ];
+ :local NewComment [ :tostr $2 ];
+
+ :global ExpectedConfigVersion;
+ :global Identity;
+ :global IDonate;
+ :global NoNewsAndChangesNotification;
+ :global ScriptUpdatesBaseUrl;
+ :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 "E1" ] = 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 info $0 ("Adding new script: " . $Script);
+ /system/script/add name=$Script owner=$Script source="#!rsc by RouterOS\n" comment=$NewComment;
+ }
+ }
+
+ :local ExpectedConfigVersionBefore $ExpectedConfigVersion;
+ :local ReloadGlobalFunctions false;
+ :local ReloadGlobalConfig false;
+
+ :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" . "'!");
+ }
+ }
+
+ :if (!($ScriptInfo->"ignore" = true)) do={
+ :do {
+ :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 ($Result->"data");
+ }
+ } on-error={
+ :if ($ScriptVal->"source" = "#!rsc by RouterOS\n") do={
+ $LogPrint warning $0 ("Failed fetching script '" . $ScriptVal->"name" . \
+ "', removing dummy. Typo on installation?");
+ /system/script/remove $Script;
+ } else={
+ $LogPrint warning $0 ("Failed fetching script '" . $ScriptVal->"name" . "'!");
+ }
+ }
+ }
+
+ :if ([ :len $SourceNew ] > 0) do={
+ :if ($SourceNew != $ScriptVal->"source") do={
+ :if ([ :pick $SourceNew 0 18 ] = "#!rsc by RouterOS\n") do={
+ :local Required ([ $ParseKeyValueStore [ $Grep $SourceNew ("\23 requires RouterOS, ") ] ]->"version");
+ :if ([ $RequiredRouterOS $0 [ $EitherOr $Required "0.0" ] false ] = true) do={
+ :if ([ $ValidateSyntax $SourceNew ] = true) do={
+ $LogPrint info $0 ("Updating script: " . $ScriptVal->"name");
+ /system/script/set owner=($ScriptVal->"name") source=$SourceNew $Script;
+ :if ($ScriptVal->"name" = "global-config") do={
+ :set ReloadGlobalConfig true;
+ }
+ :if ($ScriptVal->"name" = "global-functions" || $ScriptVal->"name" ~ ("^mod/.")) do={
+ :set ReloadGlobalFunctions true;
+ }
+ } else={
+ $LogPrint warning $0 ("Syntax validation for script '" . $ScriptVal->"name" . \
+ "' failed! Ignoring!");
+ }
+ } else={
+ $LogPrintOnce warning $0 ("The script '" . $ScriptVal->"name" . "' requires RouterOS " . \
+ $Required . ", which is not met by your installation. Ignoring!");
+ }
+ } else={
+ $LogPrint warning $0 ("Looks like new script '" . $ScriptVal->"name" . \
+ "' is not valid (missing shebang). Ignoring!");
+ }
+ } else={
+ $LogPrint debug $0 ("Script '" . $ScriptVal->"name" . "' did not change.");
+ }
+ } else={
+ $LogPrint debug $0 ("No update for script '" . $ScriptVal->"name" . "'.");
+ }
+ }
+
+ :if ($ReloadGlobalFunctions = true) do={
+ $LogPrint info $0 ("Reloading global functions.");
+ :do {
+ /system/script/run global-functions;
+ } on-error={
+ $LogPrint error $0 ("Reloading global functions failed!");
+ }
+ }
+
+ :if ($ReloadGlobalConfig = true) do={
+ $LogPrint info $0 ("Reloading global configuration.");
+ :do {
+ /system/script/run global-config;
+ } on-error={
+ $LogPrint error $0 ("Reloading global configuration failed!" . \
+ " Syntax error or missing overlay?");
+ }
+ }
+
+ :if ($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;
+
+ :do {
+ :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");
+ }
+ } on-error={
+ $LogPrint warning $0 ("Failed fetching news, changes and migration!");
+ }
+
+ :if ([ :len $ChangeLogCode ] > 0) do={
+ :if ([ $ValidateSyntax $ChangeLogCode ] = true) do={
+ :do {
+ [ :parse $ChangeLogCode ];
+ } on-error={
+ $LogPrint warning $0 ("The changelog failed to run!");
+ }
+ } 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 ]);
+ :if ([ :typeof $Migration ] = "str") do={
+ :if ([ $ValidateSyntax $Migration ] = true) do={
+ $LogPrint info $0 ("Applying migration for change " . $I . ": " . $Migration);
+ :do {
+ [ :parse $Migration ];
+ } on-error={
+ $LogPrint warning $0 ("Migration code for change " . $I . " failed to run!");
+ }
+ } else={
+ $LogPrint warning $0 ("Migration code for change " . $I . " failed syntax validation!");
+ }
+ }
+ }
+ }
+
+ :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;
+ }
+}
+
+# lock script against multiple invocation
+:set ScriptLock do={
+ :local Script [ :tostr $1 ];
+ :local WaitMax ([ :tonum $3 ] * 10);
+
+ :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 ([ :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 WaitCount 0;
+ :while ($WaitMax > $WaitCount && \
+ ([ $IsFirstTicket $Script $MyTicket ] = false || \
+ [ $TicketCount $Script ] < [ $JobCount $Script ])) do={
+ :set WaitCount ($WaitCount + 1);
+ :delay 100ms;
+ }
+
+ :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 ($WaitCount > 0) " and timed out waiting for lock" "" ] . "...");
+ :return false;
+}
+
+# send notification via NotificationFunctions - expects at least two string arguments
+:set SendNotification do={
+ :global SendNotification2;
+
+ $SendNotification2 ({ origin=$0; 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;
+ }
+}
+
+# return UTF-8 symbol for unicode name
+:set SymbolByUnicodeName do={
+ :local Name [ :tostr $1 ];
+
+ :global LogPrintOnce;
+
+ :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";
+ "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"
+ }
+
+ :if ([ :len ($Symbols->$Name) ] = 0) do={
+ $LogPrintOnce warning $0 ("No symbol available for name '" . $Name . "'!");
+ :return "";
+ }
+
+ :return (($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={
+ :local Input [ :tostr $1 ];
+
+ :global CharacterReplace;
+
+ :return [ $CharacterReplace [ $CharacterReplace $Input \
+ ("\n") ("\r\n") ] ("\r\r\n") ("\r\n") ];
+}
+
+# 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 $Input "." "," ];
+ :foreach I in={ "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 = "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 I 1;
+ :local Delay ([ $MAX [ $EitherOr $WaitTime 2s ] 100ms ] / 10);
+
+ :while ([ :len [ /file/find where name=$FileName ] ] = 0) do={
+ :if ($I >= 10) do={
+ :return false;
+ }
+ :delay $Delay;
+ :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 IsTimeSync;
+
+ :while ([ $IsTimeSync ] = false) do={
+ :delay 1s;
+ }
+}
+
+# load modules
+:foreach Script in=[ /system/script/find where name ~ "^mod/." ] do={
+ :local ScriptVal [ /system/script/get $Script ];
+ :if ([ $ValidateSyntax ($ScriptVal->"source") ] = true) do={
+ :do {
+ /system/script/run $Script;
+ } on-error={
+ $LogPrint error $0 ("Module '" . $ScriptVal->"name" . "' failed to run.");
+ }
+ } else={
+ $LogPrint error $0 ("Module '" . $ScriptVal->"name" . "' failed syntax validation, skipping.");
+ }
+}
+
+# Log success
+:local Resource [ /system/resource/get ];
+$LogPrintOnce info $ScriptName ("Loaded on " . $Resource->"board-name" . \
+ " with RouterOS " . $Resource->"version" . ".");
+
+# signal we are ready
+:set GlobalFunctionsReady true;
diff --git a/global-wait.rsc b/global-wait.rsc
new file mode 100644
index 0000000..239f575
--- /dev/null
+++ b/global-wait.rsc
@@ -0,0 +1,12 @@
+#!rsc by RouterOS
+# RouterOS script: global-wait
+# Copyright (c) 2020-2024 Christian Hesse <mail@eworm.de>
+# https://git.eworm.de/cgit/routeros-scripts/about/COPYING.md
+#
+# requires RouterOS, version=7.13
+#
+# wait for global-functions to finish
+# https://git.eworm.de/cgit/routeros-scripts/about/doc/global-wait.md
+
+:global GlobalFunctionsReady;
+:while ($GlobalFunctionsReady != true) do={ :delay 500ms; }
diff --git a/gps-track b/gps-track
deleted file mode 100644
index 932a0dc..0000000
--- a/gps-track
+++ /dev/null
@@ -1,27 +0,0 @@
-#!rsc
-# RouterOS script: gps-track
-# Copyright (c) 2018-2020 Christian Hesse <mail@eworm.de>
-#
-# track gps data by sending json data to http server
-
-:global Identity;
-:global GpsTrackUrl;
-
-:local CoordinateFormat [ /system gps get coordinate-format ];
-:local Gps [ / system gps monitor once as-value ];
-
-if ($Gps->"valid" = true) do={
- :tool fetch check-certificate=yes-without-crl \
- $GpsTrackUrl keep-result=no \
- http-method=post http-header-field="Content-Type: application/json" \
- http-data=("{" . \
- "\"lat\":\"" . ($Gps->"latitude") . "\"," . \
- "\"lon\":\"" . ($Gps->"longitude") . "\"," . \
- "\"identity\":\"" . $Identity . "\"" . \
- "}");
- :log debug ("Sending GPS data in " . $CoordinateFormat . " format: " . \
- "lat: " . ($Gps->"latitude") . " " . \
- "lon: " . ($Gps->"longitude"));
-} else={
- :log debug ("GPS data not valid.");
-}
diff --git a/gps-track.rsc b/gps-track.rsc
new file mode 100644
index 0000000..e2a4e16
--- /dev/null
+++ b/gps-track.rsc
@@ -0,0 +1,48 @@
+#!rsc by RouterOS
+# RouterOS script: gps-track
+# Copyright (c) 2018-2024 Christian Hesse <mail@eworm.de>
+# https://git.eworm.de/cgit/routeros-scripts/about/COPYING.md
+#
+# requires RouterOS, version=7.13
+#
+# track gps data by sending json data to http server
+# https://git.eworm.de/cgit/routeros-scripts/about/doc/gps-track.md
+
+:global GlobalFunctionsReady;
+:while ($GlobalFunctionsReady != true) do={ :delay 500ms; }
+
+:do {
+ :local ScriptName [ :jobname ];
+
+ :global GpsTrackUrl;
+ :global Identity;
+
+ :global FetchUserAgentStr;
+ :global LogPrint;
+ :global ScriptLock;
+ :global WaitFullyConnected;
+
+ :if ([ $ScriptLock $ScriptName ] = false) do={
+ :error false;
+ }
+ $WaitFullyConnected;
+
+ :local CoordinateFormat [ /system/gps/get coordinate-format ];
+ :local Gps [ /system/gps/monitor once as-value ];
+
+ :if ($Gps->"valid" = true) do={
+ :do {
+ /tool/fetch check-certificate=yes-without-crl output=none http-method=post \
+ http-header-field=({ [ $FetchUserAgentStr $ScriptName ]; "Content-Type: application/json" }) \
+ http-data=[ :serialize to=json { "identity"=$Identity; \
+ "lat"=($Gps->"latitude"); "lon"=($Gps->"longitude") } ] $GpsTrackUrl as-value;
+ $LogPrint debug $ScriptName ("Sending GPS data in " . $CoordinateFormat . " format: " . \
+ "lat: " . ($Gps->"latitude") . " " . \
+ "lon: " . ($Gps->"longitude"));
+ } on-error={
+ $LogPrint warning $ScriptName ("Failed sending GPS data!");
+ }
+ } else={
+ $LogPrint debug $ScriptName ("GPS data not valid.");
+ }
+} on-error={ }
diff --git a/hotspot-to-wpa b/hotspot-to-wpa
deleted file mode 100644
index a072d86..0000000
--- a/hotspot-to-wpa
+++ /dev/null
@@ -1,29 +0,0 @@
-#!rsc
-# RouterOS script: hotspot-to-wpa
-# Copyright (c) 2019-2020 Christian Hesse <mail@eworm.de>
-#
-# add private WPA passphrase after hotspot login
-
-:local MacAddress $"mac-address";
-:local UserName $username;
-:local Date [ / system clock get date ];
-:local PassWord [ / ip hotspot user get [ find where name=$UserName ] password ];
-
-:local PlaceBefore [ / caps-man access-list find where comment="--- hotspot-to-wpa above ---" disabled ];
-:if ([ :len $PlaceBefore ] = 0) do={
- :log error "Missing disabled access-list entry with comment '--- hotspot-to-wpa above ---'";
- :error "Error: See log for details.";
-}
-
-/ caps-man access-list remove [ find where mac-address=$MacAddress comment~"^hotspot-to-wpa: " ];
-
-:local Limits [ / caps-man access-list get $PlaceBefore ];
-:if (($Limits->"ap-tx-limit") > 0 && ($Limits->"client-tx-limit") > 0) do={
- / caps-man access-list add comment=("hotspot-to-wpa: " . $UserName . ", " . $MacAddress . ", " . $Date) \
- mac-address=$MacAddress private-passphrase=$PassWord ssid-regexp="-wpa\$" \
- ap-tx-limit=($Limits->"ap-tx-limit") client-tx-limit=($Limits->"client-tx-limit") \
- place-before=$PlaceBefore;
-} else={
- / caps-man access-list add comment=("hotspot-to-wpa: " . $UserName . ", " . $MacAddress . ", " . $Date) \
- mac-address=$MacAddress private-passphrase=$PassWord ssid-regexp="-wpa\$" place-before=$PlaceBefore;
-}
diff --git a/hotspot-to-wpa-cleanup.capsman.rsc b/hotspot-to-wpa-cleanup.capsman.rsc
new file mode 100644
index 0000000..8f55d71
--- /dev/null
+++ b/hotspot-to-wpa-cleanup.capsman.rsc
@@ -0,0 +1,74 @@
+#!rsc by RouterOS
+# RouterOS script: hotspot-to-wpa-cleanup.capsman
+# Copyright (c) 2021-2024 Christian Hesse <mail@eworm.de>
+# https://git.eworm.de/cgit/routeros-scripts/about/COPYING.md
+#
+# provides: lease-script, order=80
+# requires RouterOS, version=7.13
+#
+# manage and clean up private WPA passphrase after hotspot login
+# https://git.eworm.de/cgit/routeros-scripts/about/doc/hotspot-to-wpa.md
+#
+# !! Do not edit this file, it is generated from template!
+
+:global GlobalFunctionsReady;
+:while ($GlobalFunctionsReady != true) do={ :delay 500ms; }
+
+:do {
+ :local ScriptName [ :jobname ];
+
+ :global EitherOr;
+ :global LogPrint;
+ :global ParseKeyValueStore;
+ :global ScriptLock;
+
+ :if ([ $ScriptLock $ScriptName 10 ] = false) do={
+ :error false;
+ }
+
+ :local DHCPServers ({});
+ :foreach Server in=[ /ip/dhcp-server/find where comment~"hotspot-to-wpa" ] do={
+ :local ServerVal [ /ip/dhcp-server/get $Server ]
+ :local ServerInfo [ $ParseKeyValueStore ($ServerVal->"comment") ];
+ :if (($ServerInfo->"hotspot-to-wpa") = "wpa") do={
+ :set ($DHCPServers->($ServerVal->"name")) \
+ [ :totime [ $EitherOr ($ServerInfo->"timeout") 4w ] ];
+ }
+ }
+
+ :foreach Client in=[ /caps-man/registration-table/find where comment~"^hotspot-to-wpa:" ] do={
+ :local ClientVal [ /caps-man/registration-table/get $Client ];
+ :foreach Lease in=[ /ip/dhcp-server/lease/find where dynamic \
+ mac-address=($ClientVal->"mac-address") ] do={
+ :if (($DHCPServers->[ /ip/dhcp-server/lease/get $Lease server ]) > 0s) do={
+ $LogPrint info $ScriptName ("Client with mac address " . ($ClientVal->"mac-address") . \
+ " connected to WPA, making lease static.");
+ /ip/dhcp-server/lease/make-static $Lease;
+ /ip/dhcp-server/lease/set comment=($ClientVal->"comment") $Lease;
+ }
+ }
+ }
+
+ :foreach Client in=[ /caps-man/access-list/find where comment~"^hotspot-to-wpa:" \
+ !(comment~[ /system/clock/get date ]) ] do={
+ :local ClientVal [ /caps-man/access-list/get $Client ];
+ :if ([ :len [ /ip/dhcp-server/lease/find where !dynamic comment~"^hotspot-to-wpa:" \
+ mac-address=($ClientVal->"mac-address") ] ] = 0) do={
+ $LogPrint info $ScriptName ("Client with mac address " . ($ClientVal->"mac-address") . \
+ " did not connect to WPA, removing from access list.");
+ /caps-man/access-list/remove $Client;
+ }
+ }
+
+ :foreach Server,Timeout in=$DHCPServers do={
+ :foreach Lease in=[ /ip/dhcp-server/lease/find where !dynamic status="waiting" \
+ server=$Server last-seen>$Timeout comment~"^hotspot-to-wpa:" ] do={
+ :local LeaseVal [ /ip/dhcp-server/lease/get $Lease ];
+ $LogPrint info $ScriptName ("Client with mac address " . ($LeaseVal->"mac-address") . \
+ " was not seen for " . ($LeaseVal->"last-seen") . ", removing.");
+ /caps-man/access-list/remove [ find where comment~"^hotspot-to-wpa:" \
+ mac-address=($LeaseVal->"mac-address") ];
+ /ip/dhcp-server/lease/remove $Lease;
+ }
+ }
+} on-error={ }
diff --git a/hotspot-to-wpa-cleanup.template.rsc b/hotspot-to-wpa-cleanup.template.rsc
new file mode 100644
index 0000000..7ac996c
--- /dev/null
+++ b/hotspot-to-wpa-cleanup.template.rsc
@@ -0,0 +1,81 @@
+#!rsc by RouterOS
+# RouterOS script: hotspot-to-wpa-cleanup%TEMPL%
+# Copyright (c) 2021-2024 Christian Hesse <mail@eworm.de>
+# https://git.eworm.de/cgit/routeros-scripts/about/COPYING.md
+#
+# provides: lease-script, order=80
+# requires RouterOS, version=7.13
+#
+# manage and clean up private WPA passphrase after hotspot login
+# https://git.eworm.de/cgit/routeros-scripts/about/doc/hotspot-to-wpa.md
+#
+# !! This is just a template to generate the real script!
+# !! Pattern '%TEMPL%' is replaced, paths are filtered.
+
+:global GlobalFunctionsReady;
+:while ($GlobalFunctionsReady != true) do={ :delay 500ms; }
+
+:do {
+ :local ScriptName [ :jobname ];
+
+ :global EitherOr;
+ :global LogPrint;
+ :global ParseKeyValueStore;
+ :global ScriptLock;
+
+ :if ([ $ScriptLock $ScriptName 10 ] = false) do={
+ :error false;
+ }
+
+ :local DHCPServers ({});
+ :foreach Server in=[ /ip/dhcp-server/find where comment~"hotspot-to-wpa" ] do={
+ :local ServerVal [ /ip/dhcp-server/get $Server ]
+ :local ServerInfo [ $ParseKeyValueStore ($ServerVal->"comment") ];
+ :if (($ServerInfo->"hotspot-to-wpa") = "wpa") do={
+ :set ($DHCPServers->($ServerVal->"name")) \
+ [ :totime [ $EitherOr ($ServerInfo->"timeout") 4w ] ];
+ }
+ }
+
+ :foreach Client in=[ /caps-man/registration-table/find where comment~"^hotspot-to-wpa:" ] do={
+ :foreach Client in=[ /interface/wifi/registration-table/find where comment~"^hotspot-to-wpa:" ] do={
+ :local ClientVal [ /caps-man/registration-table/get $Client ];
+ :local ClientVal [ /interface/wifi/registration-table/get $Client ];
+ :foreach Lease in=[ /ip/dhcp-server/lease/find where dynamic \
+ mac-address=($ClientVal->"mac-address") ] do={
+ :if (($DHCPServers->[ /ip/dhcp-server/lease/get $Lease server ]) > 0s) do={
+ $LogPrint info $ScriptName ("Client with mac address " . ($ClientVal->"mac-address") . \
+ " connected to WPA, making lease static.");
+ /ip/dhcp-server/lease/make-static $Lease;
+ /ip/dhcp-server/lease/set comment=($ClientVal->"comment") $Lease;
+ }
+ }
+ }
+
+ :foreach Client in=[ /caps-man/access-list/find where comment~"^hotspot-to-wpa:" \
+ :foreach Client in=[ /interface/wifi/access-list/find where comment~"^hotspot-to-wpa:" \
+ !(comment~[ /system/clock/get date ]) ] do={
+ :local ClientVal [ /caps-man/access-list/get $Client ];
+ :local ClientVal [ /interface/wifi/access-list/get $Client ];
+ :if ([ :len [ /ip/dhcp-server/lease/find where !dynamic comment~"^hotspot-to-wpa:" \
+ mac-address=($ClientVal->"mac-address") ] ] = 0) do={
+ $LogPrint info $ScriptName ("Client with mac address " . ($ClientVal->"mac-address") . \
+ " did not connect to WPA, removing from access list.");
+ /caps-man/access-list/remove $Client;
+ /interface/wifi/access-list/remove $Client;
+ }
+ }
+
+ :foreach Server,Timeout in=$DHCPServers do={
+ :foreach Lease in=[ /ip/dhcp-server/lease/find where !dynamic status="waiting" \
+ server=$Server last-seen>$Timeout comment~"^hotspot-to-wpa:" ] do={
+ :local LeaseVal [ /ip/dhcp-server/lease/get $Lease ];
+ $LogPrint info $ScriptName ("Client with mac address " . ($LeaseVal->"mac-address") . \
+ " was not seen for " . ($LeaseVal->"last-seen") . ", removing.");
+ /caps-man/access-list/remove [ find where comment~"^hotspot-to-wpa:" \
+ /interface/wifi/access-list/remove [ find where comment~"^hotspot-to-wpa:" \
+ mac-address=($LeaseVal->"mac-address") ];
+ /ip/dhcp-server/lease/remove $Lease;
+ }
+ }
+} on-error={ }
diff --git a/hotspot-to-wpa-cleanup.wifi.rsc b/hotspot-to-wpa-cleanup.wifi.rsc
new file mode 100644
index 0000000..39c9f25
--- /dev/null
+++ b/hotspot-to-wpa-cleanup.wifi.rsc
@@ -0,0 +1,74 @@
+#!rsc by RouterOS
+# RouterOS script: hotspot-to-wpa-cleanup.wifi
+# Copyright (c) 2021-2024 Christian Hesse <mail@eworm.de>
+# https://git.eworm.de/cgit/routeros-scripts/about/COPYING.md
+#
+# provides: lease-script, order=80
+# requires RouterOS, version=7.13
+#
+# manage and clean up private WPA passphrase after hotspot login
+# https://git.eworm.de/cgit/routeros-scripts/about/doc/hotspot-to-wpa.md
+#
+# !! Do not edit this file, it is generated from template!
+
+:global GlobalFunctionsReady;
+:while ($GlobalFunctionsReady != true) do={ :delay 500ms; }
+
+:do {
+ :local ScriptName [ :jobname ];
+
+ :global EitherOr;
+ :global LogPrint;
+ :global ParseKeyValueStore;
+ :global ScriptLock;
+
+ :if ([ $ScriptLock $ScriptName 10 ] = false) do={
+ :error false;
+ }
+
+ :local DHCPServers ({});
+ :foreach Server in=[ /ip/dhcp-server/find where comment~"hotspot-to-wpa" ] do={
+ :local ServerVal [ /ip/dhcp-server/get $Server ]
+ :local ServerInfo [ $ParseKeyValueStore ($ServerVal->"comment") ];
+ :if (($ServerInfo->"hotspot-to-wpa") = "wpa") do={
+ :set ($DHCPServers->($ServerVal->"name")) \
+ [ :totime [ $EitherOr ($ServerInfo->"timeout") 4w ] ];
+ }
+ }
+
+ :foreach Client in=[ /interface/wifi/registration-table/find where comment~"^hotspot-to-wpa:" ] do={
+ :local ClientVal [ /interface/wifi/registration-table/get $Client ];
+ :foreach Lease in=[ /ip/dhcp-server/lease/find where dynamic \
+ mac-address=($ClientVal->"mac-address") ] do={
+ :if (($DHCPServers->[ /ip/dhcp-server/lease/get $Lease server ]) > 0s) do={
+ $LogPrint info $ScriptName ("Client with mac address " . ($ClientVal->"mac-address") . \
+ " connected to WPA, making lease static.");
+ /ip/dhcp-server/lease/make-static $Lease;
+ /ip/dhcp-server/lease/set comment=($ClientVal->"comment") $Lease;
+ }
+ }
+ }
+
+ :foreach Client in=[ /interface/wifi/access-list/find where comment~"^hotspot-to-wpa:" \
+ !(comment~[ /system/clock/get date ]) ] do={
+ :local ClientVal [ /interface/wifi/access-list/get $Client ];
+ :if ([ :len [ /ip/dhcp-server/lease/find where !dynamic comment~"^hotspot-to-wpa:" \
+ mac-address=($ClientVal->"mac-address") ] ] = 0) do={
+ $LogPrint info $ScriptName ("Client with mac address " . ($ClientVal->"mac-address") . \
+ " did not connect to WPA, removing from access list.");
+ /interface/wifi/access-list/remove $Client;
+ }
+ }
+
+ :foreach Server,Timeout in=$DHCPServers do={
+ :foreach Lease in=[ /ip/dhcp-server/lease/find where !dynamic status="waiting" \
+ server=$Server last-seen>$Timeout comment~"^hotspot-to-wpa:" ] do={
+ :local LeaseVal [ /ip/dhcp-server/lease/get $Lease ];
+ $LogPrint info $ScriptName ("Client with mac address " . ($LeaseVal->"mac-address") . \
+ " was not seen for " . ($LeaseVal->"last-seen") . ", removing.");
+ /interface/wifi/access-list/remove [ find where comment~"^hotspot-to-wpa:" \
+ mac-address=($LeaseVal->"mac-address") ];
+ /ip/dhcp-server/lease/remove $Lease;
+ }
+ }
+} on-error={ }
diff --git a/hotspot-to-wpa.capsman.rsc b/hotspot-to-wpa.capsman.rsc
new file mode 100644
index 0000000..113c95d
--- /dev/null
+++ b/hotspot-to-wpa.capsman.rsc
@@ -0,0 +1,98 @@
+#!rsc by RouterOS
+# RouterOS script: hotspot-to-wpa.capsman
+# Copyright (c) 2019-2024 Christian Hesse <mail@eworm.de>
+# https://git.eworm.de/cgit/routeros-scripts/about/COPYING.md
+#
+# requires RouterOS, version=7.13
+#
+# add private WPA passphrase after hotspot login
+# https://git.eworm.de/cgit/routeros-scripts/about/doc/hotspot-to-wpa.md
+#
+# !! Do not edit this file, it is generated from template!
+
+:global GlobalFunctionsReady;
+:while ($GlobalFunctionsReady != true) do={ :delay 500ms; }
+
+:do {
+ :local ScriptName [ :jobname ];
+
+ :global EitherOr;
+ :global LogPrint;
+ :global ParseKeyValueStore;
+ :global ScriptLock;
+
+ :local MacAddress $"mac-address";
+ :local UserName $username;
+
+ :if ([ $ScriptLock $ScriptName ] = false) do={
+ :error false;
+ }
+
+ :if ([ :typeof $MacAddress ] = "nothing" || [ :typeof $UserName ] = "nothing") do={
+ $LogPrint error $ScriptName ("This script is supposed to run from hotspot on login.");
+ :error false;
+ }
+
+ :local Date [ /system/clock/get date ];
+ :local UserVal ({});
+ :if ([ :len [ /ip/hotspot/user/find where name=$UserName ] ] > 0) do={
+ :set UserVal [ /ip/hotspot/user/get [ find where name=$UserName ] ];
+ }
+ :local UserInfo [ $ParseKeyValueStore ($UserVal->"comment") ];
+ :local Hotspot [ /ip/hotspot/host/get [ find where mac-address=$MacAddress authorized ] server ];
+
+ :if ([ :len [ /caps-man/access-list/find where comment="--- hotspot-to-wpa above ---" disabled ] ] = 0) do={
+ /caps-man/access-list/add comment="--- hotspot-to-wpa above ---" disabled=yes;
+ $LogPrint warning $ScriptName ("Added disabled access-list entry with comment '--- hotspot-to-wpa above ---'.");
+ }
+ :local PlaceBefore ([ /caps-man/access-list/find where comment="--- hotspot-to-wpa above ---" disabled ]->0);
+
+ :if ([ :len [ /caps-man/access-list/find where \
+ comment=("hotspot-to-wpa template " . $Hotspot) disabled ] ] = 0) do={
+ /caps-man/access-list/add comment=("hotspot-to-wpa template " . $Hotspot) disabled=yes place-before=$PlaceBefore;
+ $LogPrint warning $ScriptName ("Added template in access-list for hotspot '" . $Hotspot . "'.");
+ }
+ :local Template [ /caps-man/access-list/get ([ find where \
+ comment=("hotspot-to-wpa template " . $Hotspot) disabled ]->0) ];
+
+ :if ($Template->"action" = "reject") do={
+ $LogPrint info $ScriptName ("Ignoring login for hotspot '" . $Hotspot . "'.");
+ :error true;
+ }
+
+ # allow login page to load
+ :delay 1s;
+
+ $LogPrint info $ScriptName ("Adding/updating access-list entry for mac address " . $MacAddress . \
+ " (user " . $UserName . ").");
+ /caps-man/access-list/remove [ find where mac-address=$MacAddress comment~"^hotspot-to-wpa: " ];
+ /caps-man/access-list/add private-passphrase=($UserVal->"password") ssid-regexp="-wpa\$" \
+ mac-address=$MacAddress comment=("hotspot-to-wpa: " . $UserName . ", " . $MacAddress . ", " . $Date) \
+ action=reject place-before=$PlaceBefore;
+
+ :local Entry [ /caps-man/access-list/find where mac-address=$MacAddress \
+ comment=("hotspot-to-wpa: " . $UserName . ", " . $MacAddress . ", " . $Date) ];
+ :local PrivatePassphrase [ $EitherOr ($UserInfo->"private-passphrase") ($Template->"private-passphrase") ];
+ :if ([ :len $PrivatePassphrase ] > 0) do={
+ :if ($PrivatePassphrase = "ignore") do={
+ /caps-man/access-list/set $Entry !private-passphrase;
+ } else={
+ /caps-man/access-list/set $Entry private-passphrase=$PrivatePassphrase;
+ }
+ }
+ :local SsidRegexp [ $EitherOr ($UserInfo->"ssid-regexp") ($Template->"ssid-regexp") ];
+ :if ([ :len $SsidRegexp ] > 0) do={
+ /caps-man/access-list/set $Entry ssid-regexp=$SsidRegexp;
+ }
+ :local VlanId [ $EitherOr ($UserInfo->"vlan-id") ($Template->"vlan-id") ];
+ :if ([ :len $VlanId ] > 0) do={
+ /caps-man/access-list/set $Entry vlan-id=$VlanId;
+ }
+ :local VlanMode [ $EitherOr ($UserInfo->"vlan-mode") ($Template->"vlan-mode") ];
+ :if ([ :len $VlanMode] > 0) do={
+ /caps-man/access-list/set $Entry vlan-mode=$VlanMode;
+ }
+
+ :delay 2s;
+ /caps-man/access-list/set $Entry action=accept;
+} on-error={ }
diff --git a/hotspot-to-wpa.template.rsc b/hotspot-to-wpa.template.rsc
new file mode 100644
index 0000000..10f0c7e
--- /dev/null
+++ b/hotspot-to-wpa.template.rsc
@@ -0,0 +1,118 @@
+#!rsc by RouterOS
+# RouterOS script: hotspot-to-wpa%TEMPL%
+# Copyright (c) 2019-2024 Christian Hesse <mail@eworm.de>
+# https://git.eworm.de/cgit/routeros-scripts/about/COPYING.md
+#
+# requires RouterOS, version=7.13
+#
+# add private WPA passphrase after hotspot login
+# https://git.eworm.de/cgit/routeros-scripts/about/doc/hotspot-to-wpa.md
+#
+# !! This is just a template to generate the real script!
+# !! Pattern '%TEMPL%' is replaced, paths are filtered.
+
+:global GlobalFunctionsReady;
+:while ($GlobalFunctionsReady != true) do={ :delay 500ms; }
+
+:do {
+ :local ScriptName [ :jobname ];
+
+ :global EitherOr;
+ :global LogPrint;
+ :global ParseKeyValueStore;
+ :global ScriptLock;
+
+ :local MacAddress $"mac-address";
+ :local UserName $username;
+
+ :if ([ $ScriptLock $ScriptName ] = false) do={
+ :error false;
+ }
+
+ :if ([ :typeof $MacAddress ] = "nothing" || [ :typeof $UserName ] = "nothing") do={
+ $LogPrint error $ScriptName ("This script is supposed to run from hotspot on login.");
+ :error false;
+ }
+
+ :local Date [ /system/clock/get date ];
+ :local UserVal ({});
+ :if ([ :len [ /ip/hotspot/user/find where name=$UserName ] ] > 0) do={
+ :set UserVal [ /ip/hotspot/user/get [ find where name=$UserName ] ];
+ }
+ :local UserInfo [ $ParseKeyValueStore ($UserVal->"comment") ];
+ :local Hotspot [ /ip/hotspot/host/get [ find where mac-address=$MacAddress authorized ] server ];
+
+ :if ([ :len [ /caps-man/access-list/find where comment="--- hotspot-to-wpa above ---" disabled ] ] = 0) do={
+ :if ([ :len [ /interface/wifi/access-list/find where comment="--- hotspot-to-wpa above ---" disabled ] ] = 0) do={
+ /caps-man/access-list/add comment="--- hotspot-to-wpa above ---" disabled=yes;
+ /interface/wifi/access-list/add comment="--- hotspot-to-wpa above ---" disabled=yes;
+ $LogPrint warning $ScriptName ("Added disabled access-list entry with comment '--- hotspot-to-wpa above ---'.");
+ }
+ :local PlaceBefore ([ /caps-man/access-list/find where comment="--- hotspot-to-wpa above ---" disabled ]->0);
+ :local PlaceBefore ([ /interface/wifi/access-list/find where comment="--- hotspot-to-wpa above ---" disabled ]->0);
+
+ :if ([ :len [ /caps-man/access-list/find where \
+ :if ([ :len [ /interface/wifi/access-list/find where \
+ comment=("hotspot-to-wpa template " . $Hotspot) disabled ] ] = 0) do={
+ /caps-man/access-list/add comment=("hotspot-to-wpa template " . $Hotspot) disabled=yes place-before=$PlaceBefore;
+ /interface/wifi/access-list/add comment=("hotspot-to-wpa template " . $Hotspot) disabled=yes place-before=$PlaceBefore;
+ $LogPrint warning $ScriptName ("Added template in access-list for hotspot '" . $Hotspot . "'.");
+ }
+ :local Template [ /caps-man/access-list/get ([ find where \
+ :local Template [ /interface/wifi/access-list/get ([ find where \
+ comment=("hotspot-to-wpa template " . $Hotspot) disabled ]->0) ];
+
+ :if ($Template->"action" = "reject") do={
+ $LogPrint info $ScriptName ("Ignoring login for hotspot '" . $Hotspot . "'.");
+ :error true;
+ }
+
+ # allow login page to load
+ :delay 1s;
+
+ $LogPrint info $ScriptName ("Adding/updating access-list entry for mac address " . $MacAddress . \
+ " (user " . $UserName . ").");
+ /caps-man/access-list/remove [ find where mac-address=$MacAddress comment~"^hotspot-to-wpa: " ];
+ /interface/wifi/access-list/remove [ find where mac-address=$MacAddress comment~"^hotspot-to-wpa: " ];
+ /caps-man/access-list/add private-passphrase=($UserVal->"password") ssid-regexp="-wpa\$" \
+ /interface/wifi/access-list/add passphrase=($UserVal->"password") ssid-regexp="-wpa\$" \
+ mac-address=$MacAddress comment=("hotspot-to-wpa: " . $UserName . ", " . $MacAddress . ", " . $Date) \
+ action=reject place-before=$PlaceBefore;
+
+ :local Entry [ /caps-man/access-list/find where mac-address=$MacAddress \
+ :local Entry [ /interface/wifi/access-list/find where mac-address=$MacAddress \
+ comment=("hotspot-to-wpa: " . $UserName . ", " . $MacAddress . ", " . $Date) ];
+# NOT /caps-man/ #
+ :set ($Template->"private-passphrase") ($Template->"passphrase");
+# NOT /caps-man/ #
+ :local PrivatePassphrase [ $EitherOr ($UserInfo->"private-passphrase") ($Template->"private-passphrase") ];
+ :if ([ :len $PrivatePassphrase ] > 0) do={
+ :if ($PrivatePassphrase = "ignore") do={
+ /caps-man/access-list/set $Entry !private-passphrase;
+ /interface/wifi/access-list/set $Entry !passphrase;
+ } else={
+ /caps-man/access-list/set $Entry private-passphrase=$PrivatePassphrase;
+ /interface/wifi/access-list/set $Entry passphrase=$PrivatePassphrase;
+ }
+ }
+ :local SsidRegexp [ $EitherOr ($UserInfo->"ssid-regexp") ($Template->"ssid-regexp") ];
+ :if ([ :len $SsidRegexp ] > 0) do={
+ /caps-man/access-list/set $Entry ssid-regexp=$SsidRegexp;
+ /interface/wifi/access-list/set $Entry ssid-regexp=$SsidRegexp;
+ }
+ :local VlanId [ $EitherOr ($UserInfo->"vlan-id") ($Template->"vlan-id") ];
+ :if ([ :len $VlanId ] > 0) do={
+ /caps-man/access-list/set $Entry vlan-id=$VlanId;
+ /interface/wifi/access-list/set $Entry vlan-id=$VlanId;
+ }
+# NOT /interface/wifi/ #
+ :local VlanMode [ $EitherOr ($UserInfo->"vlan-mode") ($Template->"vlan-mode") ];
+ :if ([ :len $VlanMode] > 0) do={
+ /caps-man/access-list/set $Entry vlan-mode=$VlanMode;
+ }
+# NOT /interface/wifi/ #
+
+ :delay 2s;
+ /caps-man/access-list/set $Entry action=accept;
+ /interface/wifi/access-list/set $Entry action=accept;
+} on-error={ }
diff --git a/hotspot-to-wpa.wifi.rsc b/hotspot-to-wpa.wifi.rsc
new file mode 100644
index 0000000..dbf50e0
--- /dev/null
+++ b/hotspot-to-wpa.wifi.rsc
@@ -0,0 +1,95 @@
+#!rsc by RouterOS
+# RouterOS script: hotspot-to-wpa.wifi
+# Copyright (c) 2019-2024 Christian Hesse <mail@eworm.de>
+# https://git.eworm.de/cgit/routeros-scripts/about/COPYING.md
+#
+# requires RouterOS, version=7.13
+#
+# add private WPA passphrase after hotspot login
+# https://git.eworm.de/cgit/routeros-scripts/about/doc/hotspot-to-wpa.md
+#
+# !! Do not edit this file, it is generated from template!
+
+:global GlobalFunctionsReady;
+:while ($GlobalFunctionsReady != true) do={ :delay 500ms; }
+
+:do {
+ :local ScriptName [ :jobname ];
+
+ :global EitherOr;
+ :global LogPrint;
+ :global ParseKeyValueStore;
+ :global ScriptLock;
+
+ :local MacAddress $"mac-address";
+ :local UserName $username;
+
+ :if ([ $ScriptLock $ScriptName ] = false) do={
+ :error false;
+ }
+
+ :if ([ :typeof $MacAddress ] = "nothing" || [ :typeof $UserName ] = "nothing") do={
+ $LogPrint error $ScriptName ("This script is supposed to run from hotspot on login.");
+ :error false;
+ }
+
+ :local Date [ /system/clock/get date ];
+ :local UserVal ({});
+ :if ([ :len [ /ip/hotspot/user/find where name=$UserName ] ] > 0) do={
+ :set UserVal [ /ip/hotspot/user/get [ find where name=$UserName ] ];
+ }
+ :local UserInfo [ $ParseKeyValueStore ($UserVal->"comment") ];
+ :local Hotspot [ /ip/hotspot/host/get [ find where mac-address=$MacAddress authorized ] server ];
+
+ :if ([ :len [ /interface/wifi/access-list/find where comment="--- hotspot-to-wpa above ---" disabled ] ] = 0) do={
+ /interface/wifi/access-list/add comment="--- hotspot-to-wpa above ---" disabled=yes;
+ $LogPrint warning $ScriptName ("Added disabled access-list entry with comment '--- hotspot-to-wpa above ---'.");
+ }
+ :local PlaceBefore ([ /interface/wifi/access-list/find where comment="--- hotspot-to-wpa above ---" disabled ]->0);
+
+ :if ([ :len [ /interface/wifi/access-list/find where \
+ comment=("hotspot-to-wpa template " . $Hotspot) disabled ] ] = 0) do={
+ /interface/wifi/access-list/add comment=("hotspot-to-wpa template " . $Hotspot) disabled=yes place-before=$PlaceBefore;
+ $LogPrint warning $ScriptName ("Added template in access-list for hotspot '" . $Hotspot . "'.");
+ }
+ :local Template [ /interface/wifi/access-list/get ([ find where \
+ comment=("hotspot-to-wpa template " . $Hotspot) disabled ]->0) ];
+
+ :if ($Template->"action" = "reject") do={
+ $LogPrint info $ScriptName ("Ignoring login for hotspot '" . $Hotspot . "'.");
+ :error true;
+ }
+
+ # allow login page to load
+ :delay 1s;
+
+ $LogPrint info $ScriptName ("Adding/updating access-list entry for mac address " . $MacAddress . \
+ " (user " . $UserName . ").");
+ /interface/wifi/access-list/remove [ find where mac-address=$MacAddress comment~"^hotspot-to-wpa: " ];
+ /interface/wifi/access-list/add passphrase=($UserVal->"password") ssid-regexp="-wpa\$" \
+ mac-address=$MacAddress comment=("hotspot-to-wpa: " . $UserName . ", " . $MacAddress . ", " . $Date) \
+ action=reject place-before=$PlaceBefore;
+
+ :local Entry [ /interface/wifi/access-list/find where mac-address=$MacAddress \
+ comment=("hotspot-to-wpa: " . $UserName . ", " . $MacAddress . ", " . $Date) ];
+ :set ($Template->"private-passphrase") ($Template->"passphrase");
+ :local PrivatePassphrase [ $EitherOr ($UserInfo->"private-passphrase") ($Template->"private-passphrase") ];
+ :if ([ :len $PrivatePassphrase ] > 0) do={
+ :if ($PrivatePassphrase = "ignore") do={
+ /interface/wifi/access-list/set $Entry !passphrase;
+ } else={
+ /interface/wifi/access-list/set $Entry passphrase=$PrivatePassphrase;
+ }
+ }
+ :local SsidRegexp [ $EitherOr ($UserInfo->"ssid-regexp") ($Template->"ssid-regexp") ];
+ :if ([ :len $SsidRegexp ] > 0) do={
+ /interface/wifi/access-list/set $Entry ssid-regexp=$SsidRegexp;
+ }
+ :local VlanId [ $EitherOr ($UserInfo->"vlan-id") ($Template->"vlan-id") ];
+ :if ([ :len $VlanId ] > 0) do={
+ /interface/wifi/access-list/set $Entry vlan-id=$VlanId;
+ }
+
+ :delay 2s;
+ /interface/wifi/access-list/set $Entry action=accept;
+} on-error={ }
diff --git a/initial-commands b/initial-commands
deleted file mode 100644
index 79036e6..0000000
--- a/initial-commands
+++ /dev/null
@@ -1,23 +0,0 @@
-#!rsc
-# RouterOS script: initial-commands
-# Copyright (c) 2018-2020 Christian Hesse <mail@eworm.de>
-
-{
- / tool fetch "https://git.eworm.de/cgit/routeros-scripts/plain/certs/Let%27s%20Encrypt%20Authority%20X3.pem" dst-path="letsencrypt.pem";
- :delay 1s;
- / certificate {
- import file-name=letsencrypt.pem passphrase="";
- set name="ISRG-Root-X1" [ find where fingerprint="96bcec06264976f37460779acf28c5a7cfe8a3c0aae11a8ffcee05c0bddf08c6" ];
- set name="Let-s-Encrypt-Authority-X3" [ find where fingerprint="731d3d9cfaa061487a1d71445a42f67df0afca2a6c2d2f98ff7b3ce112b1f568" ];
- set name="DST-Root-CA-X3" [ find where fingerprint="0687260331a72403d909f105e69bcf0d32e1bd2493ffc6d9206d11bcd6770739" ];
- }
- :if ([ / certificate print count-only where fingerprint="96bcec06264976f37460779acf28c5a7cfe8a3c0aae11a8ffcee05c0bddf08c6" or fingerprint="731d3d9cfaa061487a1d71445a42f67df0afca2a6c2d2f98ff7b3ce112b1f568" or fingerprint="0687260331a72403d909f105e69bcf0d32e1bd2493ffc6d9206d11bcd6770739" ] != 3) do={
- :error "Something is wrong with your certificates!";
- }
- / file remove "letsencrypt.pem";
- :foreach Script in={ "global-config"; "global-config-overlay"; "global-functions"; "script-updates" } do={
- / system script add name=$Script source=([ / tool fetch check-certificate=yes-without-crl ("https://git.eworm.de/cgit/routeros-scripts/plain/" . $Script) output=user as-value]->"data");
- }
- / system script { run global-config; run global-config-overlay; run global-functions; }
- / system scheduler add name="global-scripts" start-time=startup on-event="/ system script { run global-config; run global-config-overlay; run global-functions; }"
-}
diff --git a/ip-addr-bridge b/ip-addr-bridge
deleted file mode 100644
index 947e7fe..0000000
--- a/ip-addr-bridge
+++ /dev/null
@@ -1,16 +0,0 @@
-#!rsc
-# RouterOS script: ip-addr-bridge
-# Copyright (c) 2018-2020 Christian Hesse <mail@eworm.de>
-#
-# enable or disable ip addresses based on bridge port state
-
-:foreach Bridge in=[ / interface bridge find ] do={
- :local BrName [ / interface bridge get $Bridge name ];
- :if ([ / interface bridge port print count-only where bridge=$BrName ] > 0) do={
- :if ([ / interface bridge port print count-only 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/ip-addr-bridge.rsc b/ip-addr-bridge.rsc
new file mode 100644
index 0000000..758cd46
--- /dev/null
+++ b/ip-addr-bridge.rsc
@@ -0,0 +1,18 @@
+#!rsc by RouterOS
+# RouterOS script: ip-addr-bridge
+# Copyright (c) 2018-2024 Christian Hesse <mail@eworm.de>
+# https://git.eworm.de/cgit/routeros-scripts/about/COPYING.md
+#
+# enable or disable ip addresses based on bridge port state
+# https://git.eworm.de/cgit/routeros-scripts/about/doc/ip-addr-bridge.md
+
+: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/ipsec-to-dns.rsc b/ipsec-to-dns.rsc
new file mode 100644
index 0000000..8894eee
--- /dev/null
+++ b/ipsec-to-dns.rsc
@@ -0,0 +1,79 @@
+#!rsc by RouterOS
+# RouterOS script: ipsec-to-dns
+# Copyright (c) 2021-2024 Christian Hesse <mail@eworm.de>
+# https://git.eworm.de/cgit/routeros-scripts/about/COPYING.md
+#
+# requires RouterOS, version=7.13
+#
+# and add/remove/update DNS entries from IPSec mode-config
+# https://git.eworm.de/cgit/routeros-scripts/about/doc/ipsec-to-dns.md
+
+:global GlobalFunctionsReady;
+:while ($GlobalFunctionsReady != true) do={ :delay 500ms; }
+
+:do {
+ :local ScriptName [ :jobname ];
+
+ :global Domain;
+ :global HostNameInZone;
+ :global Identity;
+ :global PrefixInZone;
+
+ :global CharacterReplace;
+ :global EscapeForRegEx;
+ :global IfThenElse;
+ :global LogPrint;
+ :global ScriptLock;
+
+ :if ([ $ScriptLock $ScriptName ] = false) do={
+ :error false;
+ }
+
+ :local Zone \
+ ([ $IfThenElse ($PrefixInZone = true) "ipsec." ] . \
+ [ $IfThenElse ($HostNameInZone = true) ($Identity . ".") ] . $Domain);
+ :local Ttl 5m;
+ :local CommentPrefix ("managed by " . $ScriptName . " for ");
+ :local CommentString ("--- " . $ScriptName . " above ---");
+
+ :if ([ :len [ /ip/dns/static/find where (name=$CommentString or (comment=$CommentString and name=-)) type=NXDOMAIN disabled ] ] = 0) do={
+ /ip/dns/static/add name=$CommentString type=NXDOMAIN disabled=yes;
+ $LogPrint warning $ScriptName ("Added disabled static dns record with name '" . $CommentString . "'.");
+ }
+ :local PlaceBefore ([ /ip/dns/static/find where (name=$CommentString or (comment=$CommentString and name=-)) type=NXDOMAIN disabled ]->0);
+
+ :foreach DnsRecord in=[ /ip/dns/static/find where comment~("^" . $CommentPrefix) ] do={
+ :local DnsRecordVal [ /ip/dns/static/get $DnsRecord ];
+ :local PeerId [ $CharacterReplace ($DnsRecordVal->"comment") $CommentPrefix "" ];
+ :if ([ :len [ /ip/ipsec/active-peers/find where id~("^(CN=)?" . [ $EscapeForRegEx $PeerId ] . "\$") \
+ dynamic-address=($DnsRecordVal->"address") ] ] > 0) do={
+ $LogPrint debug $ScriptName ("Peer " . $PeerId . " (" . $DnsRecordVal->"name" . ") still exists. Not deleting DNS entry.");
+ } else={
+ :local Found false;
+ $LogPrint info $ScriptName ("Peer " . $PeerId . " (" . $DnsRecordVal->"name" . ") has gone, deleting DNS entry.");
+ /ip/dns/static/remove $DnsRecord;
+ }
+ }
+
+ :foreach Peer in=[ /ip/ipsec/active-peers/find where !(dynamic-address=[]) ] do={
+ :local PeerVal [ /ip/ipsec/active-peers/get $Peer ];
+ :local PeerId [ $CharacterReplace ($PeerVal->"id") "CN=" "" ];
+ :local Comment ($CommentPrefix . $PeerId);
+ :local HostName [ :pick $PeerId 0 [ :find ($PeerId . ".") "." ] ];
+
+ :local Fqdn ($HostName . "." . $Zone);
+ :local DnsRecord [ /ip/dns/static/find where name=$Fqdn ];
+ :if ([ :len $DnsRecord ] > 0) do={
+ :local DnsIp [ /ip/dns/static/get $DnsRecord address ];
+ :if ($DnsIp = $PeerVal->"dynamic-address") do={
+ $LogPrint debug $ScriptName ("DNS entry for " . $Fqdn . " does not need updating.");
+ } else={
+ $LogPrint info $ScriptName ("Replacing DNS entry for " . $Fqdn . ", new address is " . $PeerVal->"dynamic-address" . ".");
+ /ip/dns/static/set name=$Fqdn address=($PeerVal->"dynamic-address") ttl=$Ttl comment=$Comment $DnsRecord;
+ }
+ } else={
+ $LogPrint info $ScriptName ("Adding new DNS entry for " . $Fqdn . ", address is " . $PeerVal->"dynamic-address" . ".");
+ /ip/dns/static/add name=$Fqdn address=($PeerVal->"dynamic-address") ttl=$Ttl comment=$Comment place-before=$PlaceBefore;
+ }
+ }
+} on-error={ }
diff --git a/ipv6-update b/ipv6-update
deleted file mode 100644
index 1004119..0000000
--- a/ipv6-update
+++ /dev/null
@@ -1,38 +0,0 @@
-#!rsc
-# RouterOS script: ipv6-update
-# Copyright (c) 2013-2020 Christian Hesse <mail@eworm.de>
-#
-# update firewall and dns settings on IPv6 prefix change
-
-:local PdPrefix $"pd-prefix";
-
-:global ParseKeyValueStore;
-
-:if ([ :typeof $PdPrefix ] = "nothing") do={
- :log error "This script is supposed to run from ipv6 dhcp-client.";
- :error "Error: See log for details.";
-}
-
-:local Pool [ / ipv6 pool get [ find where prefix=$PdPrefix ] name ];
-:local AddrList [ / ipv6 firewall address-list find where comment=("ipv6-pool-" . $Pool) ];
-:local OldPrefix [ / ipv6 firewall address-list get $AddrList address ];
-
-# give the interfaces a moment to receive their addresses
-:delay 2s;
-
-if ($OldPrefix != $PdPrefix) do={
- :log info ("Updating IPv6 address list with new IPv6 prefix " . $PdPrefix);
- / ipv6 firewall address-list set address=$PdPrefix $AddrList;
-
- :foreach Record in=[ / ip dns static find where comment~("^ipv6-pool-" . $Pool . ",") ] do={
- :local RecordVal [ / ip dns static get $Record ];
- :local Comment [ $ParseKeyValueStore ($RecordVal->"comment") ];
-
- :local Prefix [ / ipv6 address get [ find where interface=($Comment->"interface") from-pool=$Pool global ] address ];
- :set Prefix [ :toip6 [ :pick $Prefix 0 [ :find $Prefix "/64" ] ] ];
- :local Address ($Prefix | ([ :toip6 ($RecordVal->"address") ] & ::ffff:ffff:ffff:ffff));
-
- :log info ("Updating DNS record for " . ($RecordVal->"name") . ($RecordVal->"regexp") . " to " . $Address);
- / ip dns static set address=$Address $Record;
- }
-}
diff --git a/ipv6-update.rsc b/ipv6-update.rsc
new file mode 100644
index 0000000..ec9a03a
--- /dev/null
+++ b/ipv6-update.rsc
@@ -0,0 +1,87 @@
+#!rsc by RouterOS
+# RouterOS script: ipv6-update
+# Copyright (c) 2013-2024 Christian Hesse <mail@eworm.de>
+# https://git.eworm.de/cgit/routeros-scripts/about/COPYING.md
+#
+# requires RouterOS, version=7.13
+#
+# update firewall and dns settings on IPv6 prefix change
+# https://git.eworm.de/cgit/routeros-scripts/about/doc/ipv6-update.md
+
+:global GlobalFunctionsReady;
+:while ($GlobalFunctionsReady != true) do={ :delay 500ms; }
+
+:do {
+ :local ScriptName [ :jobname ];
+
+ :global LogPrint;
+ :global ParseKeyValueStore;
+ :global ScriptLock;
+
+ :local PdPrefix $"pd-prefix";
+
+ :if ([ $ScriptLock $ScriptName ] = false) do={
+ :error false;
+ }
+
+ :if ([ :typeof $PdPrefix ] = "nothing") do={
+ $LogPrint error $ScriptName ("This script is supposed to run from ipv6 dhcp-client.");
+ :error false;
+ }
+
+ :local Pool [ /ipv6/pool/get [ find where prefix=$PdPrefix ] name ];
+ :if ([ :len [ /ipv6/firewall/address-list/find where comment=("ipv6-pool-" . $Pool) ] ] = 0) do={
+ /ipv6/firewall/address-list/add list=("ipv6-pool-" . $Pool) address=:: comment=("ipv6-pool-" . $Pool);
+ $LogPrint warning $ScriptName ("Added ipv6 address list entry for ipv6-pool-" . $Pool);
+ }
+ :local AddrList [ /ipv6/firewall/address-list/find where comment=("ipv6-pool-" . $Pool) ];
+ :local OldPrefix [ /ipv6/firewall/address-list/get ($AddrList->0) address ];
+
+ :if ($OldPrefix != $PdPrefix) do={
+ $LogPrint info $ScriptName ("Updating IPv6 address list with new IPv6 prefix " . $PdPrefix);
+ /ipv6/firewall/address-list/set address=$PdPrefix $AddrList;
+
+ # give the interfaces a moment to receive their addresses
+ :delay 2s;
+
+ :foreach ListEntry in=[ /ipv6/firewall/address-list/find where comment~("^ipv6-pool-" . $Pool . ",") ] do={
+ :local ListEntryVal [ /ipv6/firewall/address-list/get $ListEntry ];
+ :local Comment [ $ParseKeyValueStore ($ListEntryVal->"comment") ];
+
+ :local Prefix [ /ipv6/address/find where from-pool=$Pool interface=($Comment->"interface") global ];
+ :if ([ :len $Prefix ] = 1) do={
+ :set Prefix [ /ipv6/address/get $Prefix address ];
+
+ :if ([ :typeof [ :find ($ListEntryVal->"address") "/128" ] ] = "num" ) do={
+ :set Prefix ([ :toip6 [ :pick $Prefix 0 [ :find $Prefix "/64" ] ] ] & ffff:ffff:ffff:ffff::);
+ :local Address ($ListEntryVal->"address");
+ :local Address ($Prefix | ([ :toip6 [ :pick $Address 0 [ :find $Address "/128" ] ] ] & ::ffff:ffff:ffff:ffff));
+
+ $LogPrint info $ScriptName ("Updating IPv6 address list with new IPv6 host address " . $Address . \
+ " from interface " . ($Comment->"interface"));
+ /ipv6/firewall/address-list/set address=$Address $ListEntry;
+ } else={
+ $LogPrint info $ScriptName ("Updating IPv6 address list with new IPv6 prefix " . $Prefix . \
+ " from interface " . ($Comment->"interface"));
+ /ipv6/firewall/address-list/set address=$Prefix $ListEntry;
+ }
+ }
+ }
+
+ :foreach Record in=[ /ip/dns/static/find where comment~("^ipv6-pool-" . $Pool . ",") ] do={
+ :local RecordVal [ /ip/dns/static/get $Record ];
+ :local Comment [ $ParseKeyValueStore ($RecordVal->"comment") ];
+
+ :local Prefix [ /ipv6/address/find where from-pool=$Pool interface=($Comment->"interface") global ];
+ :if ([ :len $Prefix ] = 1) do={
+ :set Prefix [ /ipv6/address/get $Prefix address ];
+ :set Prefix ([ :toip6 [ :pick $Prefix 0 [ :find $Prefix "/64" ] ] ] & ffff:ffff:ffff:ffff::);
+ :local Address ($Prefix | ([ :toip6 ($RecordVal->"address") ] & ::ffff:ffff:ffff:ffff));
+
+ $LogPrint info $ScriptName ("Updating DNS record for " . ($RecordVal->"name") . \
+ ($RecordVal->"regexp") . " to " . $Address);
+ /ip/dns/static/set address=$Address $Record;
+ }
+ }
+ }
+} on-error={ }
diff --git a/learn-mac-based-vlan b/learn-mac-based-vlan
deleted file mode 100644
index 73ffc37..0000000
--- a/learn-mac-based-vlan
+++ /dev/null
@@ -1,12 +0,0 @@
-#!rsc
-# RouterOS script: learn-mac-based-vlan
-# Copyright (c) 2013-2020 Christian Hesse <mail@eworm.de>
-#
-# learn MAC address for MAC-based-VLAN
-
-:local NewVlanId 33;
-
-:if ( [ / interface ethernet switch mac-based-vlan print count-only where src-mac-address=$leaseActMAC ] = 0 ) do={
- :log info ("MAC-based-VLAN: learning MAC address " . $leaseActMAC . " for VLAN " . $NewVlanId . ".");
- / interface ethernet switch mac-based-vlan add src-mac-address=$leaseActMAC new-customer-vid=$NewVlanId;
-}
diff --git a/lease-script b/lease-script
deleted file mode 100644
index 0b32d17..0000000
--- a/lease-script
+++ /dev/null
@@ -1,46 +0,0 @@
-#!rsc
-# RouterOS script: lease-script
-# Copyright (c) 2013-2020 Christian Hesse <mail@eworm.de>
-#
-# run scripts on DHCP lease
-
-:if ([ :typeof $leaseActIP ] = "nothing" || \
- [ :typeof $leaseActMAC ] = "nothing" || \
- [ :typeof $leaseServerName ] = "nothing" || \
- [ :typeof $leaseBound ] = "nothing") do={
- :log error "This script is supposed to run from ip dhcp-client.";
- :error "Error: See log for details.";
-}
-
-:local Scripts;
-:local ScriptsAssign {
- "dhcp-to-dns";
- "collect-wireless-mac.local";
- "dhcp-lease-comment.local";
- "collect-wireless-mac.capsman";
- "dhcp-lease-comment.capsman"
-}
-:local ScriptsDeAssign {
- "dhcp-to-dns"
-}
-
-:local State "";
-:if ($leaseBound = 0) do={
- :set State "de";
- :set Scripts $ScriptsDeAssign;
-} else={
- :set Scripts $ScriptsAssign;
-}
-
-:log debug ("DHCP Server " . $leaseServerName . " " . \
- $State . "assigned lease " . $leaseActIP . " to " . $leaseActMAC);
-
-# delay a moment to update the lease table, do not run in parallel for de/assign
-:delay ((1 + $leaseBound) . "s");
-
-:foreach Script in=$Scripts do={
- :if ([ / system script print count-only where name=$Script ] > 0) do={
- :log debug ("Running script from lease-script: " . $Script);
- / system script run $Script;
- }
-}
diff --git a/lease-script.rsc b/lease-script.rsc
new file mode 100644
index 0000000..a9d4b68
--- /dev/null
+++ b/lease-script.rsc
@@ -0,0 +1,59 @@
+#!rsc by RouterOS
+# RouterOS script: lease-script
+# Copyright (c) 2013-2024 Christian Hesse <mail@eworm.de>
+# https://git.eworm.de/cgit/routeros-scripts/about/COPYING.md
+#
+# requires RouterOS, version=7.13
+#
+# run scripts on DHCP lease
+# https://git.eworm.de/cgit/routeros-scripts/about/doc/lease-script.md
+
+:global GlobalFunctionsReady;
+:while ($GlobalFunctionsReady != true) do={ :delay 500ms; }
+
+:do {
+ :local ScriptName [ :jobname ];
+
+ :global Grep;
+ :global IfThenElse;
+ :global LogPrint;
+ :global ParseKeyValueStore;
+ :global ScriptLock;
+
+ :if ([ :typeof $leaseActIP ] = "nothing" || \
+ [ :typeof $leaseActMAC ] = "nothing" || \
+ [ :typeof $leaseServerName ] = "nothing" || \
+ [ :typeof $leaseBound ] = "nothing") do={
+ $LogPrint error $ScriptName ("This script is supposed to run from ip dhcp-server.");
+ :error false;
+ }
+
+ $LogPrint debug $ScriptName ("DHCP Server " . $leaseServerName . " " . [ $IfThenElse ($leaseBound = 0) \
+ "de" "" ] . "assigned lease " . $leaseActIP . " to " . $leaseActMAC);
+
+ :if ([ $ScriptLock $ScriptName 10 ] = false) do={
+ :error false;
+ }
+
+ :if ([ :len [ /system/script/job/find where script=$ScriptName ] ] > 1) do={
+ $LogPrint debug $ScriptName ("More invocations are waiting, exiting early.");
+ :error true;
+ }
+
+ :local RunOrder ({});
+ :foreach Script in=[ /system/script/find where source~("\n# provides: lease-script\\b") ] do={
+ :local ScriptVal [ /system/script/get $Script ];
+ :local Store [ $ParseKeyValueStore [ $Grep ($ScriptVal->"source") ("\23 provides: lease-script, ") ] ];
+
+ :set ($RunOrder->($Store->"order" . "-" . $ScriptVal->"name")) ($ScriptVal->"name");
+ }
+
+ :foreach Order,Script in=$RunOrder do={
+ :do {
+ $LogPrint debug $ScriptName ("Running script with order " . $Order . ": " . $Script);
+ /system/script/run $Script;
+ } on-error={
+ $LogPrint warning $ScriptName ("Running script '" . $Script . "' failed!");
+ }
+ }
+} on-error={ }
diff --git a/leds-day-mode b/leds-day-mode
deleted file mode 100644
index 0b50648..0000000
--- a/leds-day-mode
+++ /dev/null
@@ -1,7 +0,0 @@
-#!rsc
-# RouterOS script: leds-day-mode
-# Copyright (c) 2013-2020 Christian Hesse <mail@eworm.de>
-#
-# enable LEDs
-
-/ system leds settings set all-leds-off=never;
diff --git a/leds-day-mode.rsc b/leds-day-mode.rsc
new file mode 100644
index 0000000..b7c6b5b
--- /dev/null
+++ b/leds-day-mode.rsc
@@ -0,0 +1,9 @@
+#!rsc by RouterOS
+# RouterOS script: leds-day-mode
+# Copyright (c) 2013-2024 Christian Hesse <mail@eworm.de>
+# https://git.eworm.de/cgit/routeros-scripts/about/COPYING.md
+#
+# enable LEDs
+# https://git.eworm.de/cgit/routeros-scripts/about/doc/leds-mode.md
+
+/system/leds/settings/set all-leds-off=never;
diff --git a/leds-night-mode b/leds-night-mode
deleted file mode 100644
index 6a973e4..0000000
--- a/leds-night-mode
+++ /dev/null
@@ -1,7 +0,0 @@
-#!rsc
-# RouterOS script: leds-night-mode
-# Copyright (c) 2013-2020 Christian Hesse <mail@eworm.de>
-#
-# disable LEDs
-
-/ system leds settings set all-leds-off=immediate;
diff --git a/leds-night-mode.rsc b/leds-night-mode.rsc
new file mode 100644
index 0000000..fb7c7a2
--- /dev/null
+++ b/leds-night-mode.rsc
@@ -0,0 +1,9 @@
+#!rsc by RouterOS
+# RouterOS script: leds-night-mode
+# Copyright (c) 2013-2024 Christian Hesse <mail@eworm.de>
+# https://git.eworm.de/cgit/routeros-scripts/about/COPYING.md
+#
+# disable LEDs
+# https://git.eworm.de/cgit/routeros-scripts/about/doc/leds-mode.md
+
+/system/leds/settings/set all-leds-off=immediate;
diff --git a/leds-toggle-mode b/leds-toggle-mode
deleted file mode 100644
index 7518ef4..0000000
--- a/leds-toggle-mode
+++ /dev/null
@@ -1,11 +0,0 @@
-#!rsc
-# RouterOS script: leds-toggle-mode
-# Copyright (c) 2018-2020 Christian Hesse <mail@eworm.de>
-#
-# toggle LEDs mode
-
-:if ([ / system leds settings get all-leds-off ] = "never") do={
- / system leds settings set all-leds-off=immediate;
-} else={
- / system leds settings set all-leds-off=never;
-}
diff --git a/leds-toggle-mode.rsc b/leds-toggle-mode.rsc
new file mode 100644
index 0000000..136c9d1
--- /dev/null
+++ b/leds-toggle-mode.rsc
@@ -0,0 +1,13 @@
+#!rsc by RouterOS
+# RouterOS script: leds-toggle-mode
+# Copyright (c) 2018-2024 Christian Hesse <mail@eworm.de>
+# https://git.eworm.de/cgit/routeros-scripts/about/COPYING.md
+#
+# toggle LEDs mode
+# https://git.eworm.de/cgit/routeros-scripts/about/doc/leds-mode.md
+
+:if ([ /system/leds/settings/get all-leds-off ] = "never") do={
+ /system/leds/settings/set all-leds-off=immediate;
+} else={
+ /system/leds/settings/set all-leds-off=never;
+}
diff --git a/log-forward.rsc b/log-forward.rsc
new file mode 100644
index 0000000..7abcb4d
--- /dev/null
+++ b/log-forward.rsc
@@ -0,0 +1,102 @@
+#!rsc by RouterOS
+# RouterOS script: log-forward
+# Copyright (c) 2020-2024 Christian Hesse <mail@eworm.de>
+# https://git.eworm.de/cgit/routeros-scripts/about/COPYING.md
+#
+# requires RouterOS, version=7.13
+#
+# forward log messages via notification
+# https://git.eworm.de/cgit/routeros-scripts/about/doc/log-forward.md
+
+:global GlobalFunctionsReady;
+:while ($GlobalFunctionsReady != true) do={ :delay 500ms; }
+
+:do {
+ :local ScriptName [ :jobname ];
+
+ :global Identity;
+ :global LogForwardFilter;
+ :global LogForwardFilterMessage;
+ :global LogForwardInclude;
+ :global LogForwardIncludeMessage;
+ :global LogForwardLast;
+ :global LogForwardRateLimit;
+
+ :global EitherOr;
+ :global HexToNum;
+ :global IfThenElse;
+ :global LogForwardFilterLogForwarding;
+ :global LogPrint;
+ :global MAX;
+ :global ScriptLock;
+ :global SendNotification2;
+ :global SymbolForNotification;
+
+ :if ([ $ScriptLock $ScriptName ] = false) do={
+ :error false;
+ }
+
+ :if ([ :typeof $LogForwardRateLimit ] = "nothing") do={
+ :set LogForwardRateLimit 0;
+ }
+
+ :if ($LogForwardRateLimit > 30) do={
+ :set LogForwardRateLimit ($LogForwardRateLimit - 1);
+ $LogPrint info $ScriptName ("Rate limit in action, not forwarding logs, if any!");
+ :error false;
+ }
+
+ :local Count 0;
+ :local Duplicates false;
+ :local Last [ $IfThenElse ([ :len $LogForwardLast ] > 0) [ $HexToNum $LogForwardLast ] -1 ];
+ :local Messages "";
+ :local Warning false;
+ :local MessageVal;
+ :local MessageDups ({});
+
+ :local LogForwardFilterLogForwardingCached [ $EitherOr [ $LogForwardFilterLogForwarding ] ("\$^") ];
+ :foreach Message in=[ /log/find where (!(message="") and \
+ !(message~$LogForwardFilterLogForwardingCached) and \
+ !(topics~$LogForwardFilter) and !(message~$LogForwardFilterMessage)) or \
+ topics~$LogForwardInclude or message~$LogForwardIncludeMessage ] do={
+ :set MessageVal [ /log/get $Message ];
+ :local Bullet "information";
+
+ :if ($Last < [ $HexToNum ($MessageVal->".id") ]) do={
+ :local DupCount ($MessageDups->($MessageVal->"message"));
+ :if ($MessageVal->"topics" ~ "(warning)") do={
+ :set Warning true;
+ :set Bullet "large-orange-circle";
+ }
+ :if ($MessageVal->"topics" ~ "(emergency|alert|critical|error)") do={
+ :set Warning true;
+ :set Bullet "large-red-circle";
+ }
+ :if ($DupCount < 3) do={
+ :set Messages ($Messages . "\n" . [ $SymbolForNotification $Bullet ] . \
+ $MessageVal->"time" . " " . [ :tostr ($MessageVal->"topics") ] . " " . $MessageVal->"message");
+ } else={
+ :set Duplicates true;
+ }
+ :set ($MessageDups->($MessageVal->"message")) ($DupCount + 1);
+ :set Count ($Count + 1);
+ }
+ }
+
+ :if ($Count > 0) do={
+ :set LogForwardRateLimit ($LogForwardRateLimit + 10);
+
+ $SendNotification2 ({ origin=$ScriptName; \
+ subject=([ $SymbolForNotification [ $IfThenElse ($Warning = true) "warning-sign" "memo" ] ] . \
+ "Log Forwarding"); \
+ message=("The log on " . $Identity . " contains " . [ $IfThenElse ($Count = 1) "this message" \
+ ("these " . $Count . " messages") ] . " after " . [ /system/resource/get uptime ] . " uptime." . \
+ [ $IfThenElse ($Duplicates = true) (" Multi-repeated messages have been skipped.") ] . \
+ [ $IfThenElse ($LogForwardRateLimit > 30) ("\nRate limit in action, delaying forwarding.") ] . \
+ "\n" . $Messages) });
+
+ :set LogForwardLast ($MessageVal->".id");
+ } else={
+ :set LogForwardRateLimit [ $MAX 0 ($LogForwardRateLimit - 1) ];
+ }
+} on-error={ }
diff --git a/logo.avif b/logo.avif
new file mode 100644
index 0000000..399a2f5
--- /dev/null
+++ b/logo.avif
Binary files differ
diff --git a/logo.png b/logo.png
new file mode 100644
index 0000000..d97b75d
--- /dev/null
+++ b/logo.png
Binary files differ
diff --git a/logo.svg b/logo.svg
new file mode 100644
index 0000000..a30e04e
--- /dev/null
+++ b/logo.svg
@@ -0,0 +1,29 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- Created with Inkscape (http://www.inkscape.org/) -->
+<svg id="svg" width="96" height="96" version="1.1" viewBox="0 0 25.4 25.4" xmlns="http://www.w3.org/2000/svg">
+ <defs id="defs">
+ <radialGradient id="radGradDark" cx="7.5006" cy="9.4015" r="9.7854" gradientTransform="matrix(1.6107 1.0797 -.58681 .87543 .93614 -7.022)" gradientUnits="userSpaceOnUse">
+ <stop id="dark-1" stop-color="#222" offset="0"/>
+ <stop id="dark-2" stop-color="#444" offset="1"/>
+ </radialGradient>
+ <radialGradient id="radGradRed" cx="14.501" cy="10.029" r="2.6711" gradientTransform="matrix(1.3827 .62837 -.44627 .98203 -1.0744 -8.9965)" gradientUnits="userSpaceOnUse">
+ <stop id="red-1" stop-color="#a00" offset="0"/>
+ <stop id="red-2" stop-color="#c22" offset="1"/>
+ </radialGradient>
+ </defs>
+ <g id="layer1">
+ <rect id="background" x="4.5766e-15" width="25.4" height="25.4" ry="5.1528" fill="#fff" stroke-linecap="round" stroke-linejoin="round" stroke-width="13.229"/>
+ <path id="hexagon" d="m17.758 12.437-1.3326 0.77071a0.94166 0.94328 0 0 1-0.94166 0l-1.3326-0.77071a0.94166 0.94328 0 0 1-0.47083-0.8169v-1.5414a0.94166 0.94328 0 0 1 0.47083-0.8169l1.3326-0.77071a0.94166 0.94328 0 0 1 0.94166 0l1.3326 0.77071a0.94166 0.94328 0 0 1 0.47083 0.8169v1.5414a0.94166 0.94328 0 0 1-0.47083 0.8169z" fill="url(#radGradRed)" stroke="#000" stroke-linecap="round" stroke-linejoin="round" stroke-width=".79375"/>
+ <g id="text" fill="url(#radGradDark)" stroke="#000" stroke-width=".1">
+ <g id="shebang" aria-label="#!">
+ <path id="path904" d="m13.767 4.8761v-1.6506h-0.88032l0.26724-2.0593h-2.0593l-0.26724 2.0593h-1.572l0.26724-2.0593h-2.0593l-0.26724 2.0593h-1.1318v1.6506h0.91175l-0.393 3.0182h-1.1004v1.6506h0.88031l-0.29868 2.2479h2.0593l0.29868-2.2479h1.572l-0.29868 2.2479h2.0593l0.29868-2.2479h1.1318v-1.6506h-0.91175l0.393-3.0182zm-3.5527 3.0182h-1.572l0.393-3.0182h1.572z"/>
+ <path id="path906" d="m17.209 0.89898h-2.6409l0.3144 6.8853s0.66885-0.28785 1.0123-0.28746c0.33937 3.865e-4 0.99985 0.28746 0.99985 0.28746z"/>
+ </g>
+ <g id="rsc" aria-label="rsc">
+ <path id="path910" d="m7.5809 12.875c-0.92632 0-1.716 0.66817-2.0804 1.7919l-0.2126-1.5641h-2.0804v8.0636h2.3993v-4.0546c0.27334-1.1997 0.68335-1.9134 1.6704-1.9134 0.25816 0 0.47075 0.04556 0.72891 0.1063l0.37964-2.3234c-0.27334-0.075928-0.50112-0.1063-0.80484-0.1063z"/>
+ <path id="path912" d="m11.954 12.845c-2.0349 0-3.2953 1.0782-3.2953 2.4601 0 1.2452 0.78965 2.0652 2.3841 2.5208 1.4578 0.41001 1.7008 0.57705 1.7008 1.1237 0 0.48594-0.44038 0.75928-1.1693 0.75928-0.78965 0-1.5337-0.3189-2.1412-0.78965l-1.1693 1.306c0.78965 0.71372 1.9741 1.1997 3.3712 1.1997 2.0045 0 3.5838-0.98706 3.5838-2.6575 0-1.4426-0.89595-2.1108-2.4904-2.5664-1.4426-0.4252-1.6552-0.60742-1.6552-1.0326 0-0.36445 0.3189-0.60742 0.97188-0.60742 0.69854 0 1.3667 0.22778 1.9893 0.62261l0.88076-1.3515c-0.7441-0.60742-1.7919-0.98706-2.9612-0.98706z"/>
+ <path id="path914" d="m19.896 12.845c-2.4145 0-3.9483 1.7919-3.9483 4.3583 0 2.5512 1.5186 4.2216 3.9938 4.2216 1.1085 0 1.9741-0.36446 2.7182-0.95669l-1.0478-1.4882c-0.57705 0.36445-0.97188 0.54668-1.5489 0.54668-0.95669 0-1.5945-0.54668-1.5945-2.3386 0-1.8071 0.59224-2.5056 1.6249-2.5056 0.54668 0 1.0174 0.18223 1.5337 0.57705l1.0326-1.4274c-0.77446-0.65298-1.64-0.98706-2.7638-0.98706z"/>
+ </g>
+ </g>
+ </g>
+</svg>
diff --git a/manage-umts b/manage-umts
deleted file mode 100644
index f1eb861..0000000
--- a/manage-umts
+++ /dev/null
@@ -1,28 +0,0 @@
-#!rsc
-# RouterOS script: manage-umts
-# Copyright (c) 2013-2020 Christian Hesse <mail@eworm.de>
-#
-# manage UMTS interface based on ethernet and wireless status
-
-:local EtherInt "en1";
-:local WlanInt "wl-station";
-:local UmtsInt "t-mobile";
-
-:local EtherStatus [ / interface ethernet get $EtherInt running ];
-:local WlanStatus [ / interface wireless get $WlanInt running ];
-
-:if ($EtherStatus = true || $WlanStatus = true) do={
- :if ([ / interface get $UmtsInt disabled ] = false) do={
- :log info ("Ethernet (" . $EtherInt . " / " . $EtherStatus . ") or " . \
- "wireless (" . $WlanInt . " / " . $WlanStatus . ") is running, " . \
- "UMTS interface " . $UmtsInt . " is enabled. Disabling...");
- / interface set disabled=yes $UmtsInt;
- }
-} else={
- :if ([ / interface get $UmtsInt disabled ] = true) do={
- :log info ("Neither ethernet (" . $EtherInt . ") nor wireless (" . \
- $WlanInt . ") interface is running, UMTS interface " . $UmtsInt . \
- " is disabled. Enabling...");
- / interface set disabled=no $UmtsInt;
- }
-}
diff --git a/mod/bridge-port-to.rsc b/mod/bridge-port-to.rsc
new file mode 100644
index 0000000..000532a
--- /dev/null
+++ b/mod/bridge-port-to.rsc
@@ -0,0 +1,68 @@
+#!rsc by RouterOS
+# RouterOS script: mod/bridge-port-to
+# Copyright (c) 2013-2024 Christian Hesse <mail@eworm.de>
+# https://git.eworm.de/cgit/routeros-scripts/about/COPYING.md
+#
+# requires RouterOS, version=7.13
+#
+# reset bridge ports to default bridge
+# https://git.eworm.de/cgit/routeros-scripts/about/doc/mod/bridge-port-to.md
+
+:global BridgePortTo;
+
+:set BridgePortTo do={
+ :local BridgePortTo [ :tostr $1 ];
+
+ :global IfThenElse;
+ :global LogPrint;
+ :global ParseKeyValueStore;
+
+ :local InterfaceReEnable ({});
+ :foreach BridgePort in=[ /interface/bridge/port/find where !(comment=[]) ] do={
+ :local BridgePortVal [ /interface/bridge/port/get $BridgePort ];
+ :foreach Config,BridgeDefault in=[ $ParseKeyValueStore ($BridgePortVal->"comment") ] do={
+ :if ($Config = $BridgePortTo) do={
+ :local DHCPClient [ /ip/dhcp-client/find where interface=$BridgePortVal->"interface" comment="toggle with bridge port" ];
+
+ :if ($BridgeDefault = "dhcp-client") do={
+ :if ([ :len $DHCPClient ] != 1) do={
+ $LogPrint warning $0 ([ $IfThenElse ([ :len $DHCPClient ] = 0) "Missing" "Duplicate" ] . \
+ " dhcp client configuration for interface " . $BridgePortVal->"interface" . "!");
+ :return false;
+ }
+ :local DHCPClientDisabled [ /ip/dhcp-client/get $DHCPClient disabled ];
+
+ :if ($BridgePortVal->"disabled" = false || $DHCPClientDisabled = true) do={
+ $LogPrint info $0 ("Disabling bridge port for interface " . $BridgePortVal->"interface" . ", enabling dhcp client.");
+ /interface/bridge/port/disable $BridgePort;
+ :delay 200ms;
+ /ip/dhcp-client/enable $DHCPClient;
+ }
+ } else={
+ :if ($BridgePortVal->"disabled" = true || $BridgeDefault != $BridgePortVal->"bridge") do={
+ $LogPrint info $0 ("Enabling bridge port for interface " . $BridgePortVal->"interface" . ", changing to " . $BridgePortTo . \
+ " bridge " . $BridgeDefault . ", disabling dhcp client.");
+ :if ([ :len $DHCPClient ] = 1) do={
+ /ip/dhcp-client/disable $DHCPClient;
+ :delay 200ms;
+ }
+ :local Disable [ /interface/ethernet/find where name=$BridgePortVal->"interface" ];
+ :if ([ :len $Disable ] > 0) do={
+ /interface/ethernet/disable $Disable;
+ :set InterfaceReEnable ($InterfaceReEnable, $Disable);
+ }
+ /interface/bridge/port/set disabled=no bridge=$BridgeDefault $BridgePort;
+ } else={
+ $LogPrint debug $0 ("Interface " . $BridgePortVal->"interface" . " already connected to " . $BridgePortTo . \
+ " bridge " . $BridgeDefault . ".");
+ }
+ }
+ }
+ }
+ }
+ :if ([ :len $InterfaceReEnable ] > 0) do={
+ :delay 5s;
+ $LogPrint info $0 ("Re-enabling interfaces...");
+ /interface/ethernet/enable $InterfaceReEnable;
+ }
+}
diff --git a/mod/bridge-port-vlan.rsc b/mod/bridge-port-vlan.rsc
new file mode 100644
index 0000000..760e8a6
--- /dev/null
+++ b/mod/bridge-port-vlan.rsc
@@ -0,0 +1,77 @@
+#!rsc by RouterOS
+# RouterOS script: mod/bridge-port-vlan
+# Copyright (c) 2013-2024 Christian Hesse <mail@eworm.de>
+# https://git.eworm.de/cgit/routeros-scripts/about/COPYING.md
+#
+# requires RouterOS, version=7.13
+#
+# manage VLANs on bridge ports
+# https://git.eworm.de/cgit/routeros-scripts/about/doc/mod/bridge-port-vlan.md
+
+:global BridgePortVlan;
+
+:global BridgePortVlan do={
+ :local ConfigTo [ :tostr $1 ];
+
+ :global IfThenElse;
+ :global LogPrint;
+ :global ParseKeyValueStore;
+
+ :local InterfaceReEnable ({});
+ :foreach BridgePort in=[ /interface/bridge/port/find where !(comment=[]) ] do={
+ :local BridgePortVal [ /interface/bridge/port/get $BridgePort ];
+ :foreach Config,Vlan in=[ $ParseKeyValueStore ($BridgePortVal->"comment") ] do={
+ :if ($Config = $ConfigTo) do={
+ :local DHCPClient [ /ip/dhcp-client/find where interface=$BridgePortVal->"interface" comment="toggle with bridge port" ];
+
+ :if ($Vlan = "dhcp-client") do={
+ :if ([ :len $DHCPClient ] != 1) do={
+ $LogPrint warning $0 ([ $IfThenElse ([ :len $DHCPClient ] = 0) "Missing" "Duplicate" ] . \
+ " dhcp client configuration for interface " . $BridgePortVal->"interface" . "!");
+ :return false;
+ }
+ :local DHCPClientDisabled [ /ip/dhcp-client/get $DHCPClient disabled ];
+
+ :if ($BridgePortVal->"disabled" = false || $DHCPClientDisabled = true) do={
+ $LogPrint info $0 ("Disabling bridge port for interface " . $BridgePortVal->"interface" . ", enabling dhcp client.");
+ /interface/bridge/port/disable $BridgePort;
+ :delay 200ms;
+ /ip/dhcp-client/enable $DHCPClient;
+ }
+ } else={
+ :local VlanName $Vlan;
+ :if ($Vlan != [ :tostr [ :tonum $Vlan ] ]) do={
+ :do {
+ :set $Vlan ([ /interface/bridge/vlan/get [ find where comment=$Vlan ] vlan-ids ]->0);
+ } on-error={
+ $LogPrint warning $0 ("Could not find VLAN '" . $Vlan . "' for interface " . $BridgePortVal->"interface" . "!");
+ :return false;
+ }
+ }
+ :if ($BridgePortVal->"disabled" = true || $Vlan != $BridgePortVal->"pvid") do={
+ $LogPrint info $0 ("Enabling bridge port for interface " . $BridgePortVal->"interface" . ", changing to " . $ConfigTo . \
+ " vlan " . $Vlan . [ $IfThenElse ($Vlan != $VlanName) (" (" . $VlanName . ")") ] . ", disabling dhcp client.");
+ :if ([ :len $DHCPClient ] = 1) do={
+ /ip/dhcp-client/disable $DHCPClient;
+ :delay 200ms;
+ }
+ :local Disable [ /interface/ethernet/find where name=$BridgePortVal->"interface" ];
+ :if ([ :len $Disable ] > 0) do={
+ /interface/ethernet/disable $Disable;
+ :set InterfaceReEnable ($InterfaceReEnable, $Disable);
+ }
+ /interface/bridge/port/set disabled=no pvid=$Vlan $BridgePort;
+ } else={
+ $LogPrint debug $0 ("Interface " . $BridgePortVal->"interface" . " already connected to " . $ConfigTo . \
+ " vlan " . $Vlan . ".");
+ }
+ }
+ }
+ }
+ }
+ :if ([ :len $InterfaceReEnable ] > 0) do={
+ :delay 5s;
+ $LogPrint info $0 ("Re-enabling interfaces...");
+ /interface/ethernet/enable $InterfaceReEnable;
+ }
+}
diff --git a/mod/inspectvar.rsc b/mod/inspectvar.rsc
new file mode 100644
index 0000000..5adca0a
--- /dev/null
+++ b/mod/inspectvar.rsc
@@ -0,0 +1,59 @@
+#!rsc by RouterOS
+# RouterOS script: mod/inspectvar
+# Copyright (c) 2020-2024 Christian Hesse <mail@eworm.de>
+# https://git.eworm.de/cgit/routeros-scripts/about/COPYING.md
+#
+# requires RouterOS, version=7.13
+#
+# inspect variables
+# https://git.eworm.de/cgit/routeros-scripts/about/doc/mod/inspectvar.md
+
+:global InspectVar;
+:global InspectVarReturn;
+
+# inspect variable and print on terminal
+:set InspectVar do={
+ :global InspectVarReturn;
+ :global PrettyPrint;
+
+ $PrettyPrint [ $InspectVarReturn $1 ];
+}
+
+# inspect variable and return formatted string
+:set InspectVarReturn do={
+ :local Input $1;
+ :local Level (0 + [ :tonum $2 ]);
+
+ :global IfThenElse;
+ :global InspectVarReturn;
+
+ :local IndentReturn do={
+ :local Prefix [ :tostr $1 ];
+ :local Value [ :tostr $2 ];
+ :local Level [ :tonum $3 ];
+
+ :local Indent "";
+ :for I from=1 to=$Level step=1 do={
+ :set Indent ($Indent . " ");
+ }
+ :return ($Indent . "-" . $Prefix . "-> " . $Value);
+ }
+
+ :local TypeOf [ :typeof $Input ];
+ :local Return [ $IndentReturn "type" $TypeOf $Level ];
+
+ :if ($TypeOf = "array") do={
+ :foreach Key,Value in=$Input do={
+ :set $Return ($Return . "\n" . \
+ [ $IndentReturn "key" $Key ($Level + 1) ] . "\n" . \
+ [ $InspectVarReturn $Value ($Level + 2) ]);
+ }
+ } else={
+ :if ($TypeOf != "nothing") do={
+ :set $Return ($Return . "\n" . \
+ [ $IndentReturn "value" [ $IfThenElse ([ :len $Input ] > 80) \
+ ([ :pick $Input 0 77 ] . "...") $Input ] $Level ]);
+ }
+ }
+ :return $Return;
+}
diff --git a/mod/ipcalc.rsc b/mod/ipcalc.rsc
new file mode 100644
index 0000000..128ca54
--- /dev/null
+++ b/mod/ipcalc.rsc
@@ -0,0 +1,52 @@
+#!rsc by RouterOS
+# RouterOS script: mod/ipcalc
+# Copyright (c) 2020-2024 Christian Hesse <mail@eworm.de>
+# https://git.eworm.de/cgit/routeros-scripts/about/COPYING.md
+#
+# requires RouterOS, version=7.13
+#
+# ip address calculation
+# https://git.eworm.de/cgit/routeros-scripts/about/doc/mod/ipcalc.md
+
+:global IPCalc;
+:global IPCalcReturn;
+
+# print netmask, network, min host, max host and broadcast
+:set IPCalc do={
+ :local Input [ :tostr $1 ];
+
+ :global FormatLine;
+ :global IPCalcReturn;
+ :global PrettyPrint;
+
+ :local Values [ $IPCalcReturn $1 ];
+
+ $PrettyPrint ( \
+ [ $FormatLine "Address" ($Values->"address") ] . "\n" . \
+ [ $FormatLine "Netmask" ($Values->"netmask") ] . "\n" . \
+ [ $FormatLine "Network" ($Values->"network") ] . "\n" . \
+ [ $FormatLine "HostMin" ($Values->"hostmin") ] . "\n" . \
+ [ $FormatLine "HostMax" ($Values->"hostmax") ] . "\n" . \
+ [ $FormatLine "Broadcast" ($Values->"broadcast") ]);
+}
+
+# calculate and return netmask, network, min host, max host and broadcast
+:set IPCalcReturn 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);
+ }
+
+ :return $Return;
+}
diff --git a/mod/notification-email.rsc b/mod/notification-email.rsc
new file mode 100644
index 0000000..df2e81a
--- /dev/null
+++ b/mod/notification-email.rsc
@@ -0,0 +1,240 @@
+#!rsc by RouterOS
+# RouterOS script: mod/notification-email
+# Copyright (c) 2013-2024 Christian Hesse <mail@eworm.de>
+# https://git.eworm.de/cgit/routeros-scripts/about/COPYING.md
+#
+# requires RouterOS, version=7.13
+#
+# send notifications via e-mail
+# https://git.eworm.de/cgit/routeros-scripts/about/doc/mod/notification-email.md
+
+:global EMailGenerateFrom;
+:global FlushEmailQueue;
+:global LogForwardFilterLogForwarding;
+:global NotificationEMailSubject;
+:global NotificationFunctions;
+:global PurgeEMailQueue;
+:global QuotedPrintable;
+:global SendEMail;
+:global SendEMail2;
+
+# generate from-property with display name
+:set EMailGenerateFrom do={
+ :global Identity;
+
+ :global CleanName;
+
+ :local From [ /tool/e-mail/get from ];
+
+ :if ($From ~ "<.*>\$") do={
+ :return $From;
+ }
+
+ :return ([ $CleanName $Identity ] . " via routeros-scripts <" . $From . ">");
+}
+
+# flush e-mail queue
+:set FlushEmailQueue do={
+ :global EmailQueue;
+
+ :global EitherOr;
+ :global EMailGenerateFrom;
+ :global IsDNSResolving;
+ :global IsTimeSync;
+ :global LogPrint;
+
+ :local AllDone true;
+ :local QueueLen [ :len $EmailQueue ];
+ :local Scheduler [ /system/scheduler/find where name="_FlushEmailQueue" ];
+
+ :if ([ :len $Scheduler ] > 0 && [ /system/scheduler/get $Scheduler interval ] < 1m) do={
+ /system/scheduler/set interval=1m comment="Doing initial checks..." $Scheduler;
+ }
+
+ :if ([ /tool/e-mail/get last-status ] = "in-progress") do={
+ $LogPrint debug $0 ("Sending mail is currently in progress, not flushing.");
+ :return false;
+ }
+
+ :if ([ $IsTimeSync ] = false) do={
+ $LogPrint debug $0 ("Time is not synced, not flushing.");
+ :return false;
+ }
+
+ :local EMailSettings [ /tool/e-mail/get ];
+ :if ([ :typeof [ :toip ($EMailSettings->"server") ] ] != "ip" && [ $IsDNSResolving ] = false) do={
+ $LogPrint debug $0 ("Server address is a DNS name and resolving fails, not flushing.");
+ :return false;
+ }
+
+ :if ([ :len $Scheduler ] > 0 && $QueueLen = 0) do={
+ $LogPrint warning $0 ("Flushing E-Mail messages from scheduler, but queue is empty.");
+ }
+
+ /system/scheduler/set interval=([ $EitherOr $QueueLen 1 ] . "m") comment="Sending..." $Scheduler;
+
+ :foreach Id,Message in=$EmailQueue do={
+ :if ([ :typeof $Message ] = "array" ) do={
+ :local Attach ({});
+ :while ([ /tool/e-mail/get last-status ] = "in-progress") do={ :delay 1s; }
+ :foreach File in=[ :toarray [ $EitherOr ($Message->"attach") "" ] ] do={
+ :if ([ :len [ /file/find where name=$File ] ] = 1) do={
+ :set Attach ($Attach, $File);
+ } else={
+ $LogPrint warning $0 ("File '" . $File . "' does not exist, can not attach.");
+ }
+ }
+ /tool/e-mail/send from=[ $EMailGenerateFrom ] to=($Message->"to") cc=($Message->"cc") \
+ subject=($Message->"subject") body=($Message->"body") file=$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 (($Message->"remove-attach") = true) do={
+ :foreach File in=$Attach do={
+ /file/remove $File;
+ }
+ }
+ }
+ :if ($Status = "failed") do={
+ :set AllDone false;
+ :set Wait false;
+ }
+ } while=($Wait = true);
+ }
+ }
+
+ :if ($AllDone = true && $QueueLen = [ :len $EmailQueue ]) do={
+ /system/scheduler/remove $Scheduler;
+ :set EmailQueue;
+ } else={
+ /system/scheduler/set interval=1m comment="Waiting for retry..." $Scheduler;
+ }
+}
+
+# generate filter for log-forward
+:set LogForwardFilterLogForwarding do={
+ :global EscapeForRegEx;
+ :global NotificationEMailSubject;
+ :global SymbolForNotification;
+
+ :return ("^Error sending e-mail <(" . \
+ [ $EscapeForRegEx [ $NotificationEMailSubject ([ $SymbolForNotification \
+ "memo" ] . "Log Forwarding") ] ] . "|" . \
+ [ $EscapeForRegEx [ $NotificationEMailSubject ([ $SymbolForNotification \
+ "warning-sign" ] . "Log Forwarding") ] ] . ")>:");
+}
+
+# generate the e-mail subject
+:set NotificationEMailSubject do={
+ :global Identity;
+ :global IdentityExtra;
+
+ :global QuotedPrintable;
+
+ :return [ $QuotedPrintable ("[" . $IdentityExtra . $Identity . "] " . $1) ];
+}
+
+# send notification via e-mail - expects one array argument
+:set ($NotificationFunctions->"email") do={
+ :local Notification $1;
+
+ :global EmailGeneralTo;
+ :global EmailGeneralToOverride;
+ :global EmailGeneralCc;
+ :global EmailGeneralCcOverride;
+ :global EmailQueue;
+
+ :global EitherOr;
+ :global IfThenElse;
+ :global NotificationEMailSignature;
+ :global NotificationEMailSubject;
+
+ :local To [ $EitherOr ($EmailGeneralToOverride->($Notification->"origin")) $EmailGeneralTo ];
+ :local Cc [ $EitherOr ($EmailGeneralCcOverride->($Notification->"origin")) $EmailGeneralCc ];
+
+ :local EMailSettings [ /tool/e-mail/get ];
+ :if ([ :len $To ] = 0 || ($EMailSettings->"server") = "0.0.0.0" || ($EMailSettings->"from") = "<>") do={
+ :return false;
+ }
+
+ :if ([ :typeof $EmailQueue ] = "nothing") do={
+ :set EmailQueue ({});
+ }
+ :local Signature [ $EitherOr [ $NotificationEMailSignature ] [ /system/note/get note ] ];
+ :set ($EmailQueue->[ :len $EmailQueue ]) {
+ to=$To; cc=$Cc;
+ subject=[ $NotificationEMailSubject ($Notification->"subject") ];
+ body=(($Notification->"message") . \
+ [ $IfThenElse ([ :len ($Notification->"link") ] > 0) ("\n\n" . ($Notification->"link")) "" ] . \
+ [ $IfThenElse ([ :len $Signature ] > 0) ("\n-- \n" . $Signature) "" ]); \
+ attach=($Notification->"attach"); remove-attach=($Notification->"remove-attach") };
+ :if ([ :len [ /system/scheduler/find where name="_FlushEmailQueue" ] ] = 0) do={
+ /system/scheduler/add name="_FlushEmailQueue" interval=1s start-time=startup \
+ comment="Queuing new mail..." on-event=(":global FlushEmailQueue; \$FlushEmailQueue;");
+ }
+}
+
+# purge the e-mail queue
+:set PurgeEMailQueue do={
+ :global EmailQueue;
+
+ /system/scheduler/remove [ find where name="_FlushEmailQueue" ];
+ :set EmailQueue;
+}
+
+# convert string to quoted-printable
+:global QuotedPrintable do={
+ :local Input [ :tostr $1 ];
+
+ :global CharacterMultiply;
+
+ :if ([ :len $Input ] = 0) do={
+ :return $Input;
+ }
+
+ :local Return "";
+ :local Chars ( \
+ "\00\01\02\03\04\05\06\07\08\09\0A\0B\0C\0D\0E\0F\10\11\12\13\14\15\16\17\18\19\1A\1B\1C\1D\1E\1F" . \
+ [ $CharacterMultiply ("\00") 29 ] . "=\00?" . [ $CharacterMultiply ("\00") 63 ] . "\7F" . \
+ "\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 "0123456789ABCDEF";
+
+ :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 ("=" . [ :pick $Hex ($Replace / 16)] . [ :pick $Hex ($Replace % 16) ]);
+ }
+ :set Return ($Return . $Char);
+ }
+
+ :if ($Input = $Return) do={
+ :return $Input;
+ }
+
+ :return ("=?utf-8?Q?" . $Return . "?=");
+}
+
+# send notification via e-mail - expects at least two string arguments
+:set SendEMail do={
+ :global SendEMail2;
+
+ $SendEMail2 ({ origin=$0; subject=$1; message=$2; link=$3 });
+}
+
+# send notification via e-mail - expects one array argument
+:set SendEMail2 do={
+ :local Notification $1;
+
+ :global NotificationFunctions;
+
+ ($NotificationFunctions->"email") ("\$NotificationFunctions->\"email\"") $Notification;
+}
diff --git a/mod/notification-matrix.rsc b/mod/notification-matrix.rsc
new file mode 100644
index 0000000..196633a
--- /dev/null
+++ b/mod/notification-matrix.rsc
@@ -0,0 +1,266 @@
+#!rsc by RouterOS
+# RouterOS script: mod/notification-matrix
+# Copyright (c) 2013-2024 Michael Gisbers <michael@gisbers.de>
+# Christian Hesse <mail@eworm.de>
+# https://git.eworm.de/cgit/routeros-scripts/about/COPYING.md
+#
+# requires RouterOS, version=7.13
+#
+# send notifications via Matrix
+# https://git.eworm.de/cgit/routeros-scripts/about/doc/mod/notification-matrix.md
+
+:global FlushMatrixQueue;
+:global NotificationFunctions;
+:global PurgeMatrixQueue;
+:global SendMatrix;
+:global SendMatrix2;
+:global SetupMatrixAuthenticate;
+:global SetupMatrixJoinRoom;
+
+# flush Matrix queue
+:set FlushMatrixQueue do={
+ :global MatrixQueue;
+
+ :global IsFullyConnected;
+ :global LogPrint;
+
+ :if ([ $IsFullyConnected ] = false) do={
+ $LogPrint debug $0 ("System is not fully connected, not flushing.");
+ :return false;
+ }
+
+ :local AllDone true;
+ :local QueueLen [ :len $MatrixQueue ];
+
+ :if ([ :len [ /system/scheduler/find where name="_FlushMatrixQueue" ] ] > 0 && $QueueLen = 0) do={
+ $LogPrint warning $0 ("Flushing Matrix messages from scheduler, but queue is empty.");
+ }
+
+ :foreach Id,Message in=$MatrixQueue do={
+ :if ([ :typeof $Message ] = "array" ) do={
+ :do {
+ /tool/fetch check-certificate=yes-without-crl output=none \
+ http-header-field=($Message->"headers") http-method=post \
+ http-data=[ :serialize to=json { "msgtype"="m.text"; "body"=($Message->"plain");
+ "format"="org.matrix.custom.html"; "formatted_body"=($Message->"formatted") } ] \
+ ("https://" . $Message->"homeserver" . "/_matrix/client/r0/rooms/" . $Message->"room" . \
+ "/send/m.room.message?access_token=" . $Message->"accesstoken") as-value;
+ :set ($MatrixQueue->$Id);
+ } on-error={
+ $LogPrint debug $0 ("Sending queued Matrix message failed.");
+ :set AllDone false;
+ }
+ }
+ }
+
+ :if ($AllDone = true && $QueueLen = [ :len $MatrixQueue ]) do={
+ /system/scheduler/remove [ find where name="_FlushMatrixQueue" ];
+ :set MatrixQueue;
+ }
+}
+
+# send notification via Matrix - expects one array argument
+:set ($NotificationFunctions->"matrix") do={
+ :local Notification $1;
+
+ :global Identity;
+ :global IdentityExtra;
+ :global MatrixAccessToken;
+ :global MatrixAccessTokenOverride;
+ :global MatrixHomeServer;
+ :global MatrixHomeServerOverride;
+ :global MatrixQueue;
+ :global MatrixRoom;
+ :global MatrixRoomOverride;
+
+ :global EitherOr;
+ :global FetchUserAgentStr;
+ :global LogPrint;
+ :global ProtocolStrip;
+ :global SymbolForNotification;
+
+ :local PrepareText do={
+ :local Input [ :tostr $1 ];
+
+ :if ([ :len $Input ] = 0) do={
+ :return "";
+ }
+
+ :local Return "";
+ :local Chars { "\""; "\n"; "&"; "<"; ">" };
+ :local Subs { "&quot;"; "<br/>"; "&amp;"; "&lt;"; "&gt;" };
+
+ :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;
+ }
+
+ :local AccessToken [ $EitherOr ($MatrixAccessTokenOverride->($Notification->"origin")) $MatrixAccessToken ];
+ :local HomeServer [ $EitherOr ($MatrixHomeServerOverride->($Notification->"origin")) $MatrixHomeServer ];
+ :local Room [ $EitherOr ($MatrixRoomOverride->($Notification->"origin")) $MatrixRoom ];
+
+ :if ([ :len $AccessToken ] = 0 || [ :len $HomeServer ] = 0 || [ :len $Room ] = 0) do={
+ :return false;
+ }
+
+ :local Headers ({ [ $FetchUserAgentStr ($Notification->"origin") ] });
+ :local Plain ("## [" . $IdentityExtra . $Identity . "] " . \
+ ($Notification->"subject") . "\n```\n" . ($Notification->"message") . "\n```");
+ :local Formatted ("<h2>" . [ $PrepareText ("[" . $IdentityExtra . $Identity . "] " . \
+ ($Notification->"subject")) ] . "</h2>" . "<pre><code>" . \
+ [ $PrepareText ($Notification->"message") ] . "</code></pre>");
+ :if ([ :len ($Notification->"link") ] > 0) do={
+ :local Label [ $ProtocolStrip ($Notification->"link") ];
+ :set Plain ($Plain . "\n" . [ $SymbolForNotification "link" ] . \
+ "[" . $Label . "](" . $Notification->"link" . ")");
+ :set Formatted ($Formatted . "<br/>" . [ $SymbolForNotification "link" ] . \
+ "<a href=\"" . [ $PrepareText ($Notification->"link") ] . "\">" . \
+ [ $PrepareText $Label ] . "</a>");
+ }
+
+ :do {
+ /tool/fetch check-certificate=yes-without-crl output=none \
+ http-header-field=$Headers http-method=post \
+ http-data=[ :serialize to=json { "msgtype"="m.text"; "body"=$Plain;
+ "format"="org.matrix.custom.html"; "formatted_body"=$Formatted } ] \
+ ("https://" . $HomeServer . "/_matrix/client/r0/rooms/" . $Room . \
+ "/send/m.room.message?access_token=" . $AccessToken) as-value;
+ } on-error={
+ $LogPrint info $0 ("Failed sending Matrix notification! Queuing...");
+
+ :if ([ :typeof $MatrixQueue ] = "nothing") do={
+ :set MatrixQueue ({});
+ }
+ :local Symbol [ $SymbolForNotification "alarm-clock" ];
+ :local DateTime ([ /system/clock/get date ] . " " . [ /system/clock/get time ]);
+ :set Plain ($Plain . "\n" . $Symbol . "This message was queued since *" . \
+ $DateTime . "* and may be obsolete.");
+ :set Formatted ($Formatted . "<br/>" . $Symbol . "This message was queued since <em>" . \
+ $DateTime . "</em> and may be obsolete.");
+ :set ($MatrixQueue->[ :len $MatrixQueue ]) { headers=$Headers; \
+ accesstoken=$AccessToken; homeserver=$HomeServer; room=$Room; \
+ plain=$Plain; formatted=$Formatted };
+ :if ([ :len [ /system/scheduler/find where name="_FlushMatrixQueue" ] ] = 0) do={
+ /system/scheduler/add name="_FlushMatrixQueue" interval=1m start-time=startup \
+ on-event=(":global FlushMatrixQueue; \$FlushMatrixQueue;");
+ }
+ }
+}
+
+# purge the Matrix queue
+:set PurgeMatrixQueue do={
+ :global MatrixQueue;
+
+ /system/scheduler/remove [ find where name="_FlushMatrixQueue" ];
+ :set MatrixQueue;
+}
+
+# send notification via Matrix - expects at least two string arguments
+:set SendMatrix do={
+ :global SendMatrix2;
+
+ $SendMatrix2 ({ origin=$0; subject=$1; message=$2; link=$3 });
+}
+
+# send notification via Matrix - expects one array argument
+:set SendMatrix2 do={
+ :local Notification $1;
+
+ :global NotificationFunctions;
+
+ ($NotificationFunctions->"matrix") ("\$NotificationFunctions->\"matrix\"") $Notification;
+}
+
+# setup - get home server and access token
+:set SetupMatrixAuthenticate do={
+ :local User [ :tostr $1 ];
+ :local Pass [ :tostr $2 ];
+
+ :global FetchUserAgentStr;
+ :global LogPrint;
+
+ :global MatrixAccessToken;
+ :global MatrixHomeServer;
+
+ :local Domain [ :pick $User ([ :find $User ":" ] + 1) [ :len $User] ];
+ :do {
+ :local Data ([ /tool/fetch check-certificate=yes-without-crl output=user \
+ http-header-field=({ [ $FetchUserAgentStr $0 ] }) \
+ ("https://" . $Domain . "/.well-known/matrix/client") as-value ]->"data");
+ :set MatrixHomeServer ([ :deserialize from=json value=$Data ]->"m.homeserver"->"base_url");
+ $LogPrint debug $0 ("Home server is: " . $MatrixHomeServer);
+ } on-error={
+ $LogPrint error $0 ("Failed getting home server!");
+ :return false;
+ }
+
+ :if ([ :pick $MatrixHomeServer 0 8 ] = "https://") do={
+ :set MatrixHomeServer [ :pick $MatrixHomeServer 8 [ :len $MatrixHomeServer ] ];
+ }
+
+ :do {
+ :local Data ([ /tool/fetch check-certificate=yes-without-crl output=user \
+ http-header-field=({ [ $FetchUserAgentStr $0 ] }) http-method=post \
+ http-data=[ :serialize to=json { "type"="m.login.password"; "user"=$User; "password"=$Pass } ] \
+ ("https://" . $MatrixHomeServer . "/_matrix/client/r0/login") as-value ]->"data");
+ :set MatrixAccessToken ([ :deserialize from=json value=$Data ]->"access_token");
+ $LogPrint debug $0 ("Access token is: " . $MatrixAccessToken);
+ } on-error={
+ $LogPrint error $0 ("Failed logging in (and getting access token)!");
+ :return false;
+ }
+
+ :do {
+ /system/script/remove [ find where name="global-config-overlay.d/mod/notification-matrix" ];
+ /system/script/add name="global-config-overlay.d/mod/notification-matrix" source=( \
+ "# configuration snippet: mod/notification-matrix\n\n" . \
+ ":global MatrixHomeServer \"" . $MatrixHomeServer . "\";\n" . \
+ ":global MatrixAccessToken \"" . $MatrixAccessToken . "\";\n");
+ $LogPrint info $0 ("Added configuration snippet. Now create and join a room, please!");
+ } on-error={
+ $LogPrint error $0 ("Failed adding configuration snippet!");
+ :return false;
+ }
+}
+
+# setup - join a room
+:set SetupMatrixJoinRoom do={
+ :global MatrixRoom [ :tostr $1 ];
+
+ :global FetchUserAgentStr;
+ :global LogPrint;
+ :global UrlEncode;
+
+ :global MatrixAccessToken;
+ :global MatrixHomeServer;
+ :global MatrixRoom;
+
+ :do {
+ /tool/fetch check-certificate=yes-without-crl output=none \
+ http-header-field=({ [ $FetchUserAgentStr $0 ] }) http-method=post http-data="" \
+ ("https://" . $MatrixHomeServer . "/_matrix/client/r0/rooms/" . [ $UrlEncode $MatrixRoom ] . \
+ "/join?access_token=" . [ $UrlEncode $MatrixAccessToken ]) as-value;
+ $LogPrint debug $0 ("Joined the room.");
+ } on-error={
+ $LogPrint error $0 ("Failed joining the room!");
+ :return false;
+ }
+
+ :do {
+ :local Snippet [ /system/script/find where name="global-config-overlay.d/mod/notification-matrix" ];
+ /system/script/set $Snippet source=([ get $Snippet source ] . \
+ ":global MatrixRoom \"" . $MatrixRoom . "\";\n");
+ $LogPrint info $0 ("Appended configuration to configuration snippet. Please review!");
+ } on-error={
+ $LogPrint error $0 ("Failed appending configuration to snippet!");
+ :return false;
+ }
+}
diff --git a/mod/notification-ntfy.rsc b/mod/notification-ntfy.rsc
new file mode 100644
index 0000000..4413f07
--- /dev/null
+++ b/mod/notification-ntfy.rsc
@@ -0,0 +1,148 @@
+#!rsc by RouterOS
+# RouterOS script: mod/notification-ntfy
+# Copyright (c) 2013-2024 Christian Hesse <mail@eworm.de>
+# https://git.eworm.de/cgit/routeros-scripts/about/COPYING.md
+#
+# requires RouterOS, version=7.13
+#
+# send notifications via Ntfy (ntfy.sh)
+# https://git.eworm.de/cgit/routeros-scripts/about/doc/mod/notification-ntfy.md
+
+:global FlushNtfyQueue;
+:global NotificationFunctions;
+:global PurgeNtfyQueue;
+:global SendNtfy;
+:global SendNtfy2;
+
+# flush ntfy queue
+:set FlushNtfyQueue do={
+ :global NtfyQueue;
+ :global NtfyMessageIDs;
+
+ :global IsFullyConnected;
+ :global LogPrint;
+
+ :if ([ $IsFullyConnected ] = false) do={
+ $LogPrint debug $0 ("System is not fully connected, not flushing.");
+ :return false;
+ }
+
+ :local AllDone true;
+ :local QueueLen [ :len $NtfyQueue ];
+
+ :if ([ :len [ /system/scheduler/find where name="_FlushNtfyQueue" ] ] > 0 && $QueueLen = 0) do={
+ $LogPrint warning $0 ("Flushing Ntfy messages from scheduler, but queue is empty.");
+ }
+
+ :foreach Id,Message in=$NtfyQueue do={
+ :if ([ :typeof $Message ] = "array" ) do={
+ :do {
+ /tool/fetch check-certificate=yes-without-crl output=none http-method=post \
+ http-header-field=($Message->"headers") http-data=($Message->"text") \
+ ($Message->"url") user=($Message->"user") password=($Message->"pass") as-value;
+ :set ($NtfyQueue->$Id);
+ } on-error={
+ $LogPrint debug $0 ("Sending queued Ntfy message failed.");
+ :set AllDone false;
+ }
+ }
+ }
+
+ :if ($AllDone = true && $QueueLen = [ :len $NtfyQueue ]) do={
+ /system/scheduler/remove [ find where name="_FlushNtfyQueue" ];
+ :set NtfyQueue;
+ }
+}
+
+# send notification via ntfy - expects one array argument
+:set ($NotificationFunctions->"ntfy") do={
+ :local Notification $1;
+
+ :global Identity;
+ :global IdentityExtra;
+ :global NtfyQueue;
+ :global NtfyServer;
+ :global NtfyServerOverride;
+ :global NtfyServerPass;
+ :global NtfyServerPassOverride;
+ :global NtfyServerUser;
+ :global NtfyServerUserOverride;
+ :global NtfyTopic;
+ :global NtfyTopicOverride;
+
+ :global CertificateAvailable;
+ :global EitherOr;
+ :global FetchUserAgentStr;
+ :global IfThenElse;
+ :global LogPrint;
+ :global SymbolForNotification;
+ :global UrlEncode;
+
+ :local Server [ $EitherOr ($NtfyServerOverride->($Notification->"origin")) $NtfyServer ];
+ :local User [ $EitherOr ($NtfyServerUserOverride->($Notification->"origin")) $NtfyServerUser ];
+ :local Pass [ $EitherOr ($NtfyServerPassOverride->($Notification->"origin")) $NtfyServerPass ];
+ :local Topic [ $EitherOr ($NtfyTopicOverride->($Notification->"origin")) $NtfyTopic ];
+
+ :if ([ :len $Topic ] = 0) do={
+ :return false;
+ }
+
+ :local Url ("https://" . $NtfyServer . "/" . [ $UrlEncode $NtfyTopic ]);
+ :local Headers ({ [ $FetchUserAgentStr ($Notification->"origin") ]; \
+ ("Priority: " . [ $IfThenElse ($Notification->"silent") "low" "default" ]); \
+ ("Title: " . "[" . $IdentityExtra . $Identity . "] " . ($Notification->"subject")) });
+ :local Text (($Notification->"message") . "\n");
+ :if ([ :len ($Notification->"link") ] > 0) do={
+ :set Text ($Text . "\n" . [ $SymbolForNotification "link" ] . ($Notification->"link"));
+ }
+
+ :do {
+ :if ($NtfyServer = "ntfy.sh") do={
+ :if ([ $CertificateAvailable "R3" ] = false) do={
+ $LogPrint warning $0 ("Downloading required certificate failed.");
+ :error false;
+ }
+ }
+ /tool/fetch check-certificate=yes-without-crl output=none http-method=post \
+ http-header-field=$Headers http-data=$Text $Url user=$User password=$Pass as-value;
+ } on-error={
+ $LogPrint info $0 ("Failed sending ntfy notification! Queuing...");
+
+ :if ([ :typeof $NtfyQueue ] = "nothing") do={
+ :set NtfyQueue ({});
+ }
+ :set Text ($Text . "\n" . [ $SymbolForNotification "alarm-clock" ] . \
+ "This message was queued since " . [ /system/clock/get date ] . " " . \
+ [ /system/clock/get time ] . " and may be obsolete.");
+ :set ($NtfyQueue->[ :len $NtfyQueue ]) \
+ { url=$Url; user=$User; pass=$Pass; headers=$Headers; text=$Text };
+ :if ([ :len [ /system/scheduler/find where name="_FlushNtfyQueue" ] ] = 0) do={
+ /system/scheduler/add name="_FlushNtfyQueue" interval=1m start-time=startup \
+ on-event=(":global FlushNtfyQueue; \$FlushNtfyQueue;");
+ }
+ }
+}
+
+# purge the Ntfy queue
+:set PurgeNtfyQueue do={
+ :global NtfyQueue;
+
+ /system/scheduler/remove [ find where name="_FlushNtfyQueue" ];
+ :set NtfyQueue;
+}
+
+# send notification via ntfy - expects at least two string arguments
+:set SendNtfy do={
+ :global SendNtfy2;
+
+ $SendNtfy2 ({ origin=$0; subject=$1; message=$2; link=$3; silent=$4 });
+}
+
+# send notification via ntfy - expects one array argument
+:set SendNtfy2 do={
+ :local Notification $1;
+
+ :global NotificationFunctions;
+
+ ($NotificationFunctions->"ntfy") ("\$NotificationFunctions->\"ntfy\"") $Notification;
+}
diff --git a/mod/notification-telegram.rsc b/mod/notification-telegram.rsc
new file mode 100644
index 0000000..9a628ce
--- /dev/null
+++ b/mod/notification-telegram.rsc
@@ -0,0 +1,196 @@
+#!rsc by RouterOS
+# RouterOS script: mod/notification-telegram
+# Copyright (c) 2013-2024 Christian Hesse <mail@eworm.de>
+# https://git.eworm.de/cgit/routeros-scripts/about/COPYING.md
+#
+# requires RouterOS, version=7.13
+#
+# send notifications via Telegram
+# https://git.eworm.de/cgit/routeros-scripts/about/doc/mod/notification-telegram.md
+
+:global FlushTelegramQueue;
+:global NotificationFunctions;
+:global PurgeTelegramQueue;
+:global SendTelegram;
+:global SendTelegram2;
+
+# flush telegram queue
+:set FlushTelegramQueue do={
+ :global TelegramQueue;
+ :global TelegramMessageIDs;
+
+ :global IsFullyConnected;
+ :global LogPrint;
+ :global UrlEncode;
+
+ :if ([ $IsFullyConnected ] = false) do={
+ $LogPrint debug $0 ("System is not fully connected, not flushing.");
+ :return false;
+ }
+
+ :local AllDone true;
+ :local QueueLen [ :len $TelegramQueue ];
+
+ :if ([ :len [ /system/scheduler/find where name="_FlushTelegramQueue" ] ] > 0 && $QueueLen = 0) do={
+ $LogPrint warning $0 ("Flushing Telegram messages from scheduler, but queue is empty.");
+ }
+
+ :foreach Id,Message in=$TelegramQueue do={
+ :if ([ :typeof $Message ] = "array" ) do={
+ :do {
+ :local Data ([ /tool/fetch check-certificate=yes-without-crl output=user http-method=post \
+ ("https://api.telegram.org/bot" . ($Message->"tokenid") . "/sendMessage") \
+ http-data=("chat_id=" . ($Message->"chatid") . "&disable_notification=" . ($Message->"silent") . \
+ "&reply_to_message_id=" . ($Message->"replyto") . "&disable_web_page_preview=true" . \
+ "&parse_mode=MarkdownV2&text=" . [ $UrlEncode ($Message->"text") ]) as-value ]->"data");
+ :set ($TelegramQueue->$Id);
+ :set ($TelegramMessageIDs->[ :tostr ([ :deserialize from=json value=$Data ]->"result"->"message_id") ]) 1;
+ } on-error={
+ $LogPrint debug $0 ("Sending queued Telegram message failed.");
+ :set AllDone false;
+ }
+ }
+ }
+
+ :if ($AllDone = true && $QueueLen = [ :len $TelegramQueue ]) do={
+ /system/scheduler/remove [ find where name="_FlushTelegramQueue" ];
+ :set TelegramQueue;
+ }
+}
+
+# send notification via telegram - expects one array argument
+:set ($NotificationFunctions->"telegram") do={
+ :local Notification $1;
+
+ :global Identity;
+ :global IdentityExtra;
+ :global TelegramChatId;
+ :global TelegramChatIdOverride;
+ :global TelegramMessageIDs;
+ :global TelegramQueue;
+ :global TelegramTokenId;
+ :global TelegramTokenIdOverride;
+
+ :global CertificateAvailable;
+ :global CharacterReplace;
+ :global EitherOr;
+ :global IfThenElse;
+ :global LogPrint;
+ :global ProtocolStrip;
+ :global SymbolForNotification;
+ :global UrlEncode;
+
+ :local EscapeMD do={
+ :local Text [ :tostr $1 ];
+ :local Mode [ :tostr $2 ];
+ :local Excl [ :tostr $3 ];
+
+ :global CharacterReplace;
+ :global IfThenElse;
+
+ :local Chars {
+ "body"={ "\\"; "`" };
+ "plain"={ "_"; "*"; "["; "]"; "("; ")"; "~"; "`"; ">";
+ "#"; "+"; "-"; "="; "|"; "{"; "}"; "."; "!" };
+ }
+ :foreach Char in=($Chars->$Mode) do={
+ :if ([ :typeof [ :find $Excl $Char ] ] = "nil") do={
+ :set Text [ $CharacterReplace $Text $Char ("\\" . $Char) ];
+ }
+ }
+
+ :if ($Mode = "body") do={
+ :return ("```\n" . $Text . "\n```");
+ }
+
+ :return $Text;
+ }
+
+ :local ChatId [ $EitherOr ($Notification->"chatid") \
+ [ $EitherOr ($TelegramChatIdOverride->($Notification->"origin")) $TelegramChatId ] ];
+ :local TokenId [ $EitherOr ($TelegramTokenIdOverride->($Notification->"origin")) $TelegramTokenId ];
+
+ :if ([ :len $TokenId ] = 0 || [ :len $ChatId ] = 0) do={
+ :return false;
+ }
+
+ :if ([ :typeof $TelegramMessageIDs ] = "nothing") do={
+ :set TelegramMessageIDs ({});
+ }
+
+ :local Truncated false;
+ :local Text ("*__" . [ $EscapeMD ("[" . $IdentityExtra . $Identity . "] " . \
+ ($Notification->"subject")) "plain" ] . "__*\n\n");
+ :local LenSubject [ :len $Text ];
+ :local LenMessage [ :len ($Notification->"message") ];
+ :local LenLink ([ :len ($Notification->"link") ] * 2);
+ :local LenSum ($LenSubject + $LenMessage + $LenLink);
+ :if ($LenSum > 3968) do={
+ :set Text ($Text . [ $EscapeMD ([ :pick ($Notification->"message") 0 (3840 - $LenSubject - $LenLink) ] . "...") "body" ]);
+ :set Truncated true;
+ } else={
+ :set Text ($Text . [ $EscapeMD ($Notification->"message") "body" ]);
+ }
+ :if ($LenLink > 0) do={
+ :set Text ($Text . "\n" . [ $SymbolForNotification "link" ] . \
+ "[" . [ $EscapeMD [ $ProtocolStrip ($Notification->"link") ] "plain" ] . "]" . \
+ "(" . [ $EscapeMD ($Notification->"link") "plain" ] . ")");
+ }
+ :if ($Truncated = true) do={
+ :set Text ($Text . "\n" . [ $SymbolForNotification "scissors" ] . \
+ [ $EscapeMD ("The message was too long and has been truncated, cut off _" . \
+ (($LenSum - [ :len $Text ]) * 100 / $LenSum) . "%_!") "plain" "_" ]);
+ }
+
+ :do {
+ :if ([ $CertificateAvailable "Go Daddy Secure Certificate Authority - G2" ] = false) do={
+ $LogPrint warning $0 ("Downloading required certificate failed.");
+ :error false;
+ }
+ :local Data ([ /tool/fetch check-certificate=yes-without-crl output=user http-method=post \
+ ("https://api.telegram.org/bot" . $TokenId . "/sendMessage") \
+ http-data=("chat_id=" . $ChatId . "&disable_notification=" . ($Notification->"silent") . \
+ "&reply_to_message_id=" . ($Notification->"replyto") . "&disable_web_page_preview=true" . \
+ "&parse_mode=MarkdownV2&text=" . [ $UrlEncode $Text ]) as-value ]->"data");
+ :set ($TelegramMessageIDs->[ :tostr ([ :deserialize from=json value=$Data ]->"result"->"message_id") ]) 1;
+ } on-error={
+ $LogPrint info $0 ("Failed sending Telegram notification! Queuing...");
+
+ :if ([ :typeof $TelegramQueue ] = "nothing") do={
+ :set TelegramQueue ({});
+ }
+ :set Text ($Text . "\n" . [ $SymbolForNotification "alarm-clock" ] . \
+ [ $EscapeMD ("This message was queued since _" . [ /system/clock/get date ] . \
+ " " . [ /system/clock/get time ] . "_ and may be obsolete.") "plain" "_" ]);
+ :set ($TelegramQueue->[ :len $TelegramQueue ]) { chatid=$ChatId; tokenid=$TokenId;
+ text=$Text; silent=($Notification->"silent"); replyto=($Notification->"replyto") };
+ :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;");
+ }
+ }
+}
+
+# purge the Telegram queue
+:set PurgeTelegramQueue do={
+ :global TelegramQueue;
+
+ /system/scheduler/remove [ find where name="_FlushTelegramQueue" ];
+ :set TelegramQueue;
+}
+
+# send notification via telegram - expects at least two string arguments
+:set SendTelegram do={
+ :global SendTelegram2;
+
+ $SendTelegram2 ({ origin=$0; 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;
+}
diff --git a/mod/scriptrunonce.rsc b/mod/scriptrunonce.rsc
new file mode 100644
index 0000000..c3972a0
--- /dev/null
+++ b/mod/scriptrunonce.rsc
@@ -0,0 +1,52 @@
+#!rsc by RouterOS
+# RouterOS script: mod/scriptrunonece
+# Copyright (c) 2020-2024 Christian Hesse <mail@eworm.de>
+# https://git.eworm.de/cgit/routeros-scripts/about/COPYING.md
+#
+# requires RouterOS, version=7.13
+#
+# download script and run it once
+# https://git.eworm.de/cgit/routeros-scripts/about/doc/mod/scriptrunonce.md
+
+:global ScriptRunOnce;
+
+# fetch and run script(s) once
+:set ScriptRunOnce do={
+ :local Scripts [ :toarray $1 ];
+
+ :global ScriptRunOnceBaseUrl;
+ :global ScriptRunOnceUrlSuffix;
+
+ :global LogPrint;
+ :global ValidateSyntax;
+
+ :foreach Script in=$Scripts do={
+ :if (!($Script ~ "^(ftp|https?|sftp)://")) do={
+ :if ([ :len $ScriptRunOnceBaseUrl ] = 0) do={
+ $LogPrint warning $0 ("Script '" . $Script . "' is not an url and base url is not available.");
+ :return false;
+ }
+ :set Script ($ScriptRunOnceBaseUrl . $Script . ".rsc" . $ScriptRunOnceUrlSuffix);
+ }
+
+ :local Source;
+ :do {
+ :set Source ([ /tool/fetch check-certificate=yes-without-crl $Script output=user as-value ]->"data");
+ } on-error={
+ $LogPrint warning $0 ("Failed fetching script '" . $Script . "'!");
+ }
+
+ :if ([ :len $Source ] > 0) do={
+ :if ([ $ValidateSyntax $Source ] = true) do={
+ :do {
+ $LogPrint info $0 ("Running script '" . $Script . "' now.");
+ [ :parse $Source ];
+ } on-error={
+ $LogPrint warning $0 ("The script '" . $Script . "' failed to run!");
+ }
+ } else={
+ $LogPrint warning $0 ("The script '" . $Script . "' failed syntax validation!");
+ }
+ }
+ }
+}
diff --git a/mod/ssh-keys-import.rsc b/mod/ssh-keys-import.rsc
new file mode 100644
index 0000000..6272a93
--- /dev/null
+++ b/mod/ssh-keys-import.rsc
@@ -0,0 +1,114 @@
+#!rsc by RouterOS
+# RouterOS script: mod/ssh-keys-import
+# Copyright (c) 2020-2024 Christian Hesse <mail@eworm.de>
+# https://git.eworm.de/cgit/routeros-scripts/about/COPYING.md
+#
+# requires RouterOS, version=7.13
+#
+# import ssh keys for public key authentication
+# https://git.eworm.de/cgit/routeros-scripts/about/doc/mod/ssh-keys-import.md
+
+:global SSHKeysImport;
+:global SSHKeysImportFile;
+
+# import single key passed as string
+:set SSHKeysImport do={
+ :local Key [ :tostr $1 ];
+ :local User [ :tostr $2 ];
+
+ :global CharacterReplace;
+ :global GetRandom20CharAlNum;
+ :global LogPrint;
+ :global MkDir;
+ :global WaitForFile;
+
+ :if ([ :len $Key ] = 0 || [ :len $User ] = 0) do={
+ $LogPrint warning $0 ("Missing argument(s), please pass key and user!");
+ :return false;
+ }
+
+ :if ([ :len [ /user/find where name=$User ] ] = 0) do={
+ $LogPrint warning $0 ("User '" . $User . "' does not exist.");
+ :return false;
+ }
+
+ :local KeyVal [ :toarray [ $CharacterReplace $Key " " "," ] ];
+ :if (!($KeyVal->0 = "ssh-ed25519" || $KeyVal->0 = "ssh-rsa")) do={
+ $LogPrint warning $0 ("SSH key of type '" . $KeyVal->0 . "' is not supported.");
+ :return false;
+ }
+
+ :local FingerPrintMD5 [ :convert from=base64 transform=md5 to=hex ($KeyVal->1) ];
+
+ :if ([ :len [ /user/ssh-keys/find where user=$User key-owner~("\\bmd5=" . $FingerPrintMD5 . "\\b") ] ] > 0) do={
+ $LogPrint warning $0 ("The ssh public key (MD5:" . $FingerPrintMD5 . \
+ ") is already available for user '" . $User . "'.");
+ :return false;
+ }
+
+ :if ([ $MkDir "tmpfs/ssh-keys-import" ] = false) do={
+ $LogPrint warning $0 ("Creating directory 'tmpfs/ssh-keys-import' failed!");
+ :return false;
+ }
+
+ :local FileName ("tmpfs/ssh-keys-import/key-" . [ $GetRandom20CharAlNum 6 ] . ".pub");
+ /file/add name=$FileName contents=($Key . ", md5=" . $FingerPrintMD5);
+ $WaitForFile $FileName;
+
+ :do {
+ /user/ssh-keys/import public-key-file=$FileName user=$User;
+ $LogPrint info $0 ("Imported ssh public key (" . $KeyVal->2 . ", " . $KeyVal->0 . ", " . \
+ "MD5:" . $FingerPrintMD5 . ") for user '" . $User . "'.");
+ /file/remove "tmpfs/ssh-keys-import";
+ } on-error={
+ $LogPrint warning $0 ("Failed importing key.");
+ /file/remove "tmpfs/ssh-keys-import";
+ :return false;
+ }
+}
+
+# import keys from a file
+:set SSHKeysImportFile do={
+ :local FileName [ :tostr $1 ];
+ :local User [ :tostr $2 ];
+
+ :global CharacterReplace;
+ :global EitherOr;
+ :global LogPrint;
+ :global ParseKeyValueStore;
+ :global SSHKeysImport;
+
+ :if ([ :len $FileName ] = 0 || [ :len $User ] = 0) do={
+ $LogPrint warning $0 ("Missing argument(s), please pass file name and user!");
+ :return false;
+ }
+
+ :local File [ /file/find where name=$FileName ];
+ :if ([ :len $File ] = 0) do={
+ $LogPrint warning $0 ("File '" . $FileName . "' does not exist.");
+ :return false;
+ }
+ :local Keys ([ /file/get $FileName contents ] . "\n");
+
+ :do {
+ :local Continue false;
+ :local Line [ :pick $Keys 0 [ :find $Keys "\n" ] ];
+ :set Keys [ :pick $Keys ([ :find $Keys "\n" ] + 1) [ :len $Keys ] ];
+ :local KeyVal [ :toarray [ $CharacterReplace $Line " " "," ] ];
+ :if ($KeyVal->0 = "ssh-ed25519" || $KeyVal->0 = "ssh-rsa") do={
+ :do {
+ $SSHKeysImport $Line $User;
+ } on-error={
+ $LogPrint warning $0 ("Failed importing key for user '" . $User . "'.");
+ }
+ :set Continue true;
+ }
+ :if ($Continue = false && $KeyVal->0 = "#") do={
+ :set User [ $EitherOr ([ $ParseKeyValueStore [ :pick $Line 2 [ :len $Line ] ] ]->"user") $User ];
+ :set Continue true;
+ }
+ :if ($Continue = false && [ :len ($KeyVal->0) ] > 0) do={
+ $LogPrint warning $0 ("SSH key of type '" . $KeyVal->0 . "' is not supported.");
+ }
+ } while=([ :len $Keys ] > 0);
+}
diff --git a/mode-button-event b/mode-button-event
deleted file mode 100644
index 91b0edb..0000000
--- a/mode-button-event
+++ /dev/null
@@ -1,20 +0,0 @@
-#!rsc
-# RouterOS script: mode-button-event
-# Copyright (c) 2018-2020 Christian Hesse <mail@eworm.de>
-#
-# run on mode-button event and count button presses
-
-:global ModeButton;
-
-:set ($ModeButton->"count") ($ModeButton->"count" + 1);
-
-:local Scheduler [ / system scheduler find where name="mode-button-scheduler" ];
-
-:if ([ :len $Scheduler ] = 0) do={
- :log info "Creating mode-button scheduler, counting presses...";
- / system scheduler add name="mode-button-scheduler" \
- on-event="/ system script run mode-button-scheduler;" interval=3s;
-} else={
- :log debug "Updating mode-button-scheduler...";
- / system scheduler set $Scheduler start-time=[ /system clock get time ];
-}
diff --git a/mode-button-scheduler b/mode-button-scheduler
deleted file mode 100644
index 2b6f083..0000000
--- a/mode-button-scheduler
+++ /dev/null
@@ -1,31 +0,0 @@
-#!rsc
-# RouterOS script: mode-button-scheduler
-# Copyright (c) 2018-2020 Christian Hesse <mail@eworm.de>
-#
-# act on multiple mode-botton presses from scheduler
-
-:global ModeButton;
-
-:local Count ($ModeButton->"count");
-:local Code ($ModeButton->[ :tostr $Count ]);
-
-:set ($ModeButton->"count") 0;
-/ system scheduler remove mode-button-scheduler;
-
-:if ([ :len $Code ] > 0) do={
- :log info ("Acting on " . $Count . " mode-button presses: " . $Code);
-
- :if ([ / system routerboard settings get silent-boot ] = false) do={
- :for I from=1 to=$Count do={
- :beep length=200ms;
- :delay 200ms;
- }
- } else={
- :delay 1s;
- }
-
- :local Parsed [ :parse $Code ];
- $Parsed;
-} else={
- :log info ("No action defined for " . $Count . " mode-button presses.");
-}
diff --git a/mode-button.rsc b/mode-button.rsc
new file mode 100644
index 0000000..4994f6b
--- /dev/null
+++ b/mode-button.rsc
@@ -0,0 +1,81 @@
+#!rsc by RouterOS
+# RouterOS script: mode-button
+# Copyright (c) 2018-2024 Christian Hesse <mail@eworm.de>
+# https://git.eworm.de/cgit/routeros-scripts/about/COPYING.md
+#
+# requires RouterOS, version=7.13
+#
+# act on multiple mode and reset button presses
+# https://git.eworm.de/cgit/routeros-scripts/about/doc/mode-button.md
+
+:global GlobalFunctionsReady;
+:while ($GlobalFunctionsReady != true) do={ :delay 500ms; }
+
+:do {
+ :local ScriptName [ :jobname ];
+
+ :global ModeButton;
+
+ :global LogPrint;
+
+ :set ($ModeButton->"count") ($ModeButton->"count" + 1);
+
+ :local Scheduler [ /system/scheduler/find where name="_ModeButtonScheduler" ];
+
+ :if ([ :len $Scheduler ] = 0) do={
+ $LogPrint info $ScriptName ("Creating scheduler _ModeButtonScheduler, counting presses...");
+ :global ModeButtonScheduler do={
+ :global ModeButton;
+
+ :global LogPrint;
+ :global ModeButtonScheduler;
+ :global ValidateSyntax;
+
+ :local LEDInvert do={
+ :global ModeButtonLED;
+
+ :global IfThenElse;
+
+ :local LED [ /system/leds/find where leds=$ModeButtonLED type~"^(on|off)\$" interface=[] ];
+ :if ([ :len $LED ] = 0) do={
+ :return false;
+ }
+ /system/leds/set type=[ $IfThenElse ([ get $LED type ] = "on") "off" "on" ] $LED;
+ }
+
+ :local Count ($ModeButton->"count");
+ :local Code ($ModeButton->[ :tostr $Count ]);
+
+ :set ($ModeButton->"count") 0;
+ :set ModeButtonScheduler;
+ /system/scheduler/remove [ find where name="_ModeButtonScheduler" ];
+
+ :if ([ :len $Code ] > 0) do={
+ :if ([ $ValidateSyntax $Code ] = true) do={
+ $LogPrint info $ScriptName ("Acting on " . $Count . " mode-button presses: " . $Code);
+
+ :for I from=1 to=$Count do={
+ $LEDInvert;
+ :if ([ /system/routerboard/settings/get silent-boot ] = false) do={
+ :beep length=200ms;
+ }
+ :delay 200ms;
+ $LEDInvert;
+ :delay 200ms;
+ }
+
+ [ :parse $Code ];
+ } else={
+ $LogPrint warning $ScriptName ("The code for " . $Count . " mode-button presses failed syntax validation!");
+ }
+ } else={
+ $LogPrint info $ScriptName ("No action defined for " . $Count . " mode-button presses.");
+ }
+ }
+ /system/scheduler/add name="_ModeButtonScheduler" \
+ on-event=":global ModeButtonScheduler; \$ModeButtonScheduler;" interval=3s;
+ } else={
+ $LogPrint debug $ScriptName ("Updating scheduler _ModeButtonScheduler...");
+ /system/scheduler/set $Scheduler start-time=[ /system/clock/get time ];
+ }
+} on-error={ }
diff --git a/netwatch-dns.rsc b/netwatch-dns.rsc
new file mode 100644
index 0000000..09365ba
--- /dev/null
+++ b/netwatch-dns.rsc
@@ -0,0 +1,130 @@
+#!rsc by RouterOS
+# RouterOS script: netwatch-dns
+# Copyright (c) 2022-2024 Christian Hesse <mail@eworm.de>
+# https://git.eworm.de/cgit/routeros-scripts/about/COPYING.md
+#
+# requires RouterOS, version=7.13
+#
+# monitor and manage dns/doh with netwatch
+# https://git.eworm.de/cgit/routeros-scripts/about/doc/netwatch-dns.md
+
+:global GlobalFunctionsReady;
+:while ($GlobalFunctionsReady != true) do={ :delay 500ms; }
+
+:do {
+ :local ScriptName [ :jobname ];
+
+ :global CertificateAvailable;
+ :global EitherOr;
+ :global LogPrint;
+ :global ParseKeyValueStore;
+ :global ScriptLock;
+
+ :if ([ $ScriptLock $ScriptName ] = false) do={
+ :error false;
+ }
+
+ :local SettleTime (5m30s - [ /system/resource/get uptime ]);
+ :if ($SettleTime > 0s) do={
+ $LogPrint info $ScriptName ("System just booted, giving netwatch " . $SettleTime . " to settle.");
+ :error true;
+ }
+
+ :local DnsServers ({});
+ :local DnsFallback ({});
+ :local DnsCurrent [ /ip/dns/get servers ];
+
+ :foreach Host in=[ /tool/netwatch/find where comment~"\\bdns\\b" status="up" ] do={
+ :local HostVal [ /tool/netwatch/get $Host ];
+ :local HostInfo [ $ParseKeyValueStore ($HostVal->"comment") ];
+
+ :if ($HostInfo->"disabled" != true) do={
+ :if ($HostInfo->"dns" = true) do={
+ :set DnsServers ($DnsServers, $HostVal->"host");
+ }
+ :if ($HostInfo->"dns-fallback" = true) do={
+ :set DnsFallback ($DnsFallback, $HostVal->"host");
+ }
+ }
+ }
+
+ :if ([ :len $DnsServers ] > 0) do={
+ :if ($DnsServers != $DnsCurrent) do={
+ $LogPrint info $ScriptName ("Updating DNS servers: " . [ :tostr $DnsServers ]);
+ /ip/dns/set servers=$DnsServers;
+ /ip/dns/cache/flush;
+ }
+ } else={
+ :if ([ :len $DnsFallback ] > 0) do={
+ :if ($DnsFallback != $DnsCurrent) do={
+ $LogPrint info $ScriptName ("Updating DNS servers to fallback: " . [ :tostr $DnsFallback ]);
+ /ip/dns/set servers=$DnsFallback;
+ /ip/dns/cache/flush;
+ }
+ }
+ }
+
+ :local DohCurrent [ /ip/dns/get use-doh-server ];
+ :local DohServers ({});
+
+ :foreach Host in=[ /tool/netwatch/find where comment~"\\bdoh\\b" status="up" ] do={
+ :local HostVal [ /tool/netwatch/get $Host ];
+ :local HostInfo [ $ParseKeyValueStore ($HostVal->"comment") ];
+ :local HostName [ /ip/dns/static/find where name address=($HostVal->"host") \
+ (!type or type="A" or type="AAAA") !disabled !dynamic ];
+ :if ([ :len $HostName ] > 0) do={
+ :set HostName [ /ip/dns/static/get ($HostName->0) name ];
+ }
+
+ :if ($HostInfo->"doh" = true && $HostInfo->"disabled" != true) do={
+ :if ([ :len ($HostInfo->"doh-url") ] = 0) do={
+ :set ($HostInfo->"doh-url") ("https://" . [ $EitherOr $HostName ($HostVal->"host") ] . "/dns-query");
+ }
+
+ :if ($DohCurrent = $HostInfo->"doh-url") do={
+ $LogPrint debug $ScriptName ("Current DoH server is still up: " . $DohCurrent);
+ :error true;
+ }
+
+ :set ($DohServers->[ :len $DohServers ]) $HostInfo;
+ }
+ }
+
+ :if ([ :len $DohCurrent ] > 0) do={
+ $LogPrint info $ScriptName ("Current DoH server is down, disabling: " . $DohCurrent);
+ /ip/dns/set use-doh-server="";
+ /ip/dns/cache/flush;
+ }
+
+ :foreach DohServer in=$DohServers do={
+ :if ([ :len ($DohServer->"doh-cert") ] > 0) do={
+ :if ([ $CertificateAvailable ($DohServer->"doh-cert") ] = false) do={
+ $LogPrint warning $ScriptName ("Downloading certificate failed, trying without.");
+ }
+ }
+
+ :local Data false;
+ :do {
+ :set Data ([ /tool/fetch check-certificate=yes-without-crl output=user \
+ http-header-field=({ "accept: application/dns-message" }) \
+ url=(($DohServer->"doh-url") . "?dns=" . [ :convert to=base64 ([ :rndstr length=2 ] . \
+ "\01\00" . "\00\01" . "\00\00" . "\00\00" . "\00\00" . "\09doh-check\05eworm\02de\00" . \
+ "\00\10" . "\00\01") ]) as-value ]->"data");
+ } on-error={
+ $LogPrint warning $ScriptName ("Request to DoH server failed (network or certificate issue): " . \
+ ($DohServer->"doh-url"));
+ }
+
+ :if ($Data != false) do={
+ :if ([ :typeof [ :find $Data "doh-check-OK" ] ] = "num") do={
+ /ip/dns/set use-doh-server=($DohServer->"doh-url") verify-doh-cert=yes;
+ /ip/dns/cache/flush;
+ $LogPrint info $ScriptName ("Setting DoH server: " . ($DohServer->"doh-url"));
+ :error true;
+ } else={
+ $LogPrint warning $ScriptName ("Received unexpected response from DoH server: " . \
+ ($DohServer->"doh-url"));
+ }
+ }
+ }
+} on-error={ }
diff --git a/netwatch-notify.rsc b/netwatch-notify.rsc
new file mode 100644
index 0000000..17682f0
--- /dev/null
+++ b/netwatch-notify.rsc
@@ -0,0 +1,220 @@
+#!rsc by RouterOS
+# RouterOS script: netwatch-notify
+# Copyright (c) 2020-2024 Christian Hesse <mail@eworm.de>
+# https://git.eworm.de/cgit/routeros-scripts/about/COPYING.md
+#
+# requires RouterOS, version=7.13
+#
+# monitor netwatch and send notifications
+# https://git.eworm.de/cgit/routeros-scripts/about/doc/netwatch-notify.md
+
+:global GlobalFunctionsReady;
+:while ($GlobalFunctionsReady != true) do={ :delay 500ms; }
+
+:do {
+ :local ScriptName [ :jobname ];
+
+ :global NetwatchNotify;
+
+ :global EitherOr;
+ :global IfThenElse;
+ :global IsDNSResolving;
+ :global LogPrint;
+ :global ParseKeyValueStore;
+ :global ScriptFromTerminal;
+ :global ScriptLock;
+ :global SendNotification2;
+ :global SymbolForNotification;
+
+ :local NetwatchNotifyHook do={
+ :local ScriptName [ :tostr $1 ];
+ :local Name [ :tostr $2 ];
+ :local Type [ :tostr $3 ];
+ :local State [ :tostr $4 ];
+ :local Hook [ :tostr $5 ];
+
+ :global LogPrint;
+ :global ValidateSyntax;
+
+ :if ([ $ValidateSyntax $Hook ] = true) do={
+ :do {
+ [ :parse $Hook ];
+ } on-error={
+ $LogPrint warning $ScriptName ("The " . $State . "-hook for " . $Type . " '" . $Name . "' failed to run.");
+ :return ("The hook failed to run.");
+ }
+ } else={
+ $LogPrint warning $ScriptName ("The " . $State . "-hook for " . $Type . " '" . $Name . "' failed syntax validation.");
+ :return ("The hook failed syntax validation.");
+ }
+
+ $LogPrint info $ScriptName ("Ran hook on " . $Type . " '" . $Name . "' " . $State . ": " . $Hook);
+ :return ("Ran hook:\n" . $Hook);
+ }
+
+ :local ResolveExpected do={
+ :local ScriptName [ :tostr $1 ];
+ :local Name [ :tostr $2 ];
+ :local Expected [ :tostr $3 ];
+
+ :global GetRandom20CharAlNum;
+
+ :local FwAddrList ($ScriptName . "-" . [ $GetRandom20CharAlNum ]);
+ /ip/firewall/address-list/add address=$Name list=$FwAddrList dynamic=yes timeout=1s;
+ :delay 20ms;
+ :if ([ :len [ /ip/firewall/address-list/find where list=$FwAddrList address=$Expected ] ] > 0) do={
+ :return true;
+ }
+ /ipv6/firewall/address-list/add address=$Name list=$FwAddrList dynamic=yes timeout=1s;
+ :delay 20ms;
+ :if ([ :len [ /ipv6/firewall/address-list/find where list=$FwAddrList address=$Expected ] ] > 0) do={
+ :return true;
+ }
+
+ :return false;
+ }
+
+ :if ([ $ScriptLock $ScriptName ] = false) do={
+ :error false;
+ }
+
+ :local ScriptFromTerminalCached [ $ScriptFromTerminal $ScriptName ];
+
+ :if ([ :typeof $NetwatchNotify ] = "nothing") do={
+ :set NetwatchNotify ({});
+ }
+
+ :foreach Host in=[ /tool/netwatch/find where comment~"\\bnotify\\b" !disabled status!="unknown" ] do={
+ :local HostVal [ /tool/netwatch/get $Host ];
+ :local Type [ $IfThenElse ($HostVal->"type" ~ "^(https?-get|tcp-conn)\$") "service" "host" ];
+ :local HostInfo [ $ParseKeyValueStore ($HostVal->"comment") ];
+ :local HostDetails ($HostVal->"host" . \
+ [ $IfThenElse ([ :len ($HostInfo->"resolve") ] > 0) (", " . $HostInfo->"resolve") ]);
+
+ :if ($HostInfo->"notify" = true && $HostInfo->"disabled" != true) do={
+ :local Name [ $EitherOr ($HostInfo->"name") ($HostVal->"name") ];
+
+ :local Metric { "count-down"=0; "count-up"=0; "notified"=false; "resolve-failcnt"=0 };
+ :if ([ :typeof ($NetwatchNotify->$Name) ] = "array") do={
+ :set $Metric ($NetwatchNotify->$Name);
+ }
+
+ :if ([ :typeof ($HostInfo->"resolve") ] = "str") do={
+ :if ([ $IsDNSResolving ] = true) do={
+ :do {
+ :local Resolve [ :resolve ($HostInfo->"resolve") ];
+ :if ($Resolve != $HostVal->"host") do={
+ :if ([ $ResolveExpected $ScriptName ($HostInfo->"resolve") ($HostVal->"host") ] = false) do={
+ $LogPrint info $ScriptName ("Name '" . $HostInfo->"resolve" . [ $IfThenElse \
+ ($HostInfo->"resolve" != $HostInfo->"name") ("' for " . $Type . " '" . \
+ $HostInfo->"name") "" ] . "' resolves to different address " . $Resolve . \
+ ", updating.");
+ /tool/netwatch/set host=$Resolve $Host;
+ :set ($Metric->"resolve-failcnt") 0;
+ :set ($HostVal->"status") "unknown";
+ }
+ }
+ } on-error={
+ :set ($Metric->"resolve-failcnt") ($Metric->"resolve-failcnt" + 1);
+ :if ($Metric->"resolve-failcnt" = 3) do={
+ $LogPrint [ $IfThenElse ($HostInfo->"no-resolve-fail" != true) warning debug ] \
+ $ScriptName ("Resolving name '" . $HostInfo->"resolve" . [ $IfThenElse \
+ ($HostInfo->"resolve" != $HostInfo->"name") ("' for " . $Type . " '" . \
+ $HostInfo->"name") "" ] . "' failed.");
+ }
+ }
+ }
+ }
+
+ :if ($HostVal->"status" = "up") do={
+ :local CountDown ($Metric->"count-down");
+ :if ($CountDown > 0) do={
+ $LogPrint info $ScriptName \
+ ("The " . $Type . " '" . $Name . "' (" . $HostDetails . ") is up.");
+ :set ($Metric->"count-down") 0;
+ }
+ :set ($Metric->"count-up") ($Metric->"count-up" + 1);
+ :if ($Metric->"notified" = true) do={
+ :local Message ("The " . $Type . " '" . $Name . "' (" . $HostDetails . \
+ ") is up since " . $HostVal->"since" . ".\n" . \
+ "It was down for " . $CountDown . " checks since " . ($Metric->"since") . ".");
+ :if ([ :typeof ($HostInfo->"note") ] = "str") do={
+ :set Message ($Message . "\n\nNote:\n" . ($HostInfo->"note"));
+ }
+ :if ([ :typeof ($HostInfo->"up-hook") ] = "str") do={
+ :set Message ($Message . "\n\n" . [ $NetwatchNotifyHook $ScriptName $Name $Type "up" \
+ ($HostInfo->"up-hook") ]);
+ }
+ $SendNotification2 ({ origin=[ $EitherOr ($HostInfo->"origin") $ScriptName ]; silent=($HostInfo->"silent"); \
+ subject=([ $SymbolForNotification "white-heavy-check-mark" ] . "Netwatch Notify: " . $Name . " up"); \
+ message=$Message; link=($HostInfo->"link") });
+ }
+ :set ($Metric->"notified") false;
+ :set ($Metric->"parent") ($HostInfo->"parent");
+ :set ($Metric->"since");
+ }
+
+ :if ($HostVal->"status" = "down") do={
+ :set ($Metric->"count-down") ($Metric->"count-down" + 1);
+ :set ($Metric->"count-up") 0;
+ :set ($Metric->"parent") ($HostInfo->"parent");
+ :set ($Metric->"since") ($HostVal->"since");
+ :local CountDown [ $IfThenElse ([ :tonum ($HostInfo->"count") ] > 0) ($HostInfo->"count") 5 ];
+ :local Parent ($HostInfo->"parent");
+ :local ParentUp false;
+ :while ([ :len $Parent ] > 0) do={
+ :set CountDown ($CountDown + 1);
+ :set Parent ($NetwatchNotify->$Parent->"parent");
+ }
+ :set Parent ($HostInfo->"parent");
+ :local ParentNotified false;
+ :while ($ParentNotified = false && [ :len $Parent ] > 0) do={
+ :set ParentNotified [ $IfThenElse (($NetwatchNotify->$Parent->"notified") = true) \
+ true false ];
+ :set ParentUp ($NetwatchNotify->$Parent->"count-up");
+ :if ($ParentNotified = false) do={
+ :set Parent ($NetwatchNotify->$Parent->"parent");
+ }
+ }
+ :if ($Metric->"notified" = false || $Metric->"count-down" % 120 = 0 || \
+ $ScriptFromTerminalCached = true) do={
+ $LogPrint [ $IfThenElse ($HostInfo->"no-down-notification" != true) info debug ] $ScriptName \
+ ("The " . $Type . " '" . $Name . "' (" . $HostDetails . ") is down for " . \
+ $Metric->"count-down" . " checks, " . [ $IfThenElse ($ParentNotified = false) [ $IfThenElse \
+ ($Metric->"notified" = true) ("already notified.") ($CountDown - $Metric->"count-down" . \
+ " to go.") ] ("parent " . $Type . " " . $Parent . " is down.") ]);
+ }
+ :if ((($CountDown * 2) - ($Metric->"count-down" * 3)) / 2 = 0 && \
+ [ :typeof ($HostInfo->"pre-down-hook") ] = "str") do={
+ $NetwatchNotifyHook $ScriptName $Name $Type "pre-down" ($HostInfo->"pre-down-hook");
+ }
+ :if ($ParentNotified = false && $Metric->"count-down" >= $CountDown && \
+ ($ParentUp = false || $ParentUp > 2) && $Metric->"notified" != true) do={
+ :local Message ("The " . $Type . " '" . $Name . "' (" . $HostDetails . \
+ ") is down since " . $HostVal->"since" . ".");
+ :if ([ :typeof ($HostInfo->"note") ] = "str") do={
+ :set Message ($Message . "\n\nNote:\n" . ($HostInfo->"note"));
+ }
+ :if ([ :typeof ($HostInfo->"down-hook") ] = "str") do={
+ :set Message ($Message . "\n\n" . [ $NetwatchNotifyHook $ScriptName $Name $Type "down" \
+ ($HostInfo->"down-hook") ]);
+ }
+ :if ($HostInfo->"no-down-notification" != true) do={
+ $SendNotification2 ({ origin=[ $EitherOr ($HostInfo->"origin") $ScriptName ]; silent=($HostInfo->"silent"); \
+ subject=([ $SymbolForNotification "cross-mark" ] . "Netwatch Notify: " . $Name . " down"); \
+ message=$Message; link=($HostInfo->"link") });
+ }
+ :set ($Metric->"notified") true;
+ }
+ }
+
+ :set ($NetwatchNotify->$Name) {
+ "count-down"=($Metric->"count-down");
+ "count-up"=($Metric->"count-up");
+ "notified"=($Metric->"notified");
+ "parent"=($Metric->"parent");
+ "resolve-failcnt"=($Metric->"resolve-failcnt");
+ "since"=($Metric->"since") };
+ }
+ }
+} on-error={ }
diff --git a/netwatch-syslog b/netwatch-syslog
deleted file mode 100644
index b76d31d..0000000
--- a/netwatch-syslog
+++ /dev/null
@@ -1,15 +0,0 @@
-#!rsc
-# RouterOS script: netwatch-syslog
-# Copyright (c) 2013-2020 Christian Hesse <mail@eworm.de>
-#
-# requires: dont-require-permissions=yes
-#
-# manage remote logging facilities
-
-:local Remote [ /system logging action get ([ find where target=remote ]->0) remote ];
-
-if ([ / tool netwatch get [ find where host=$Remote ] status ] = "up") do={
- / system logging set disabled=no [ find where action=remote disabled=yes ];
-} else={
- / system logging set disabled=yes [ find where action=remote disabled=no ];
-}
diff --git a/news-and-changes.rsc b/news-and-changes.rsc
new file mode 100644
index 0000000..8ddeb91
--- /dev/null
+++ b/news-and-changes.rsc
@@ -0,0 +1,63 @@
+# News, changes and migration by RouterOS Scripts
+# Copyright (c) 2019-2024 Christian Hesse <mail@eworm.de>
+# https://git.eworm.de/cgit/routeros-scripts/about/COPYING.md
+
+:global IDonate;
+
+:global IfThenElse;
+:global RequiredRouterOS;
+:global SymbolForNotification;
+
+:local Resource [ /system/resource/get ];
+
+# News, changes and migration up to change 95:
+# https://git.eworm.de/cgit/routeros-scripts/plain/global-config.changes?h=change-95
+
+# Changes for global-config to be added to notification on script updates
+:global GlobalConfigChanges {
+ 96="Added support for notes in 'netwatch-notify', these are included verbatim into the notification.";
+ 97="Modified 'dhcp-to-dns' to always add A records for names with mac address, and optionally add CNAME records if the host name is available.";
+ 98="Extended 'check-certificates' to download new certificate by SubjectAltNames if download by CommonName fails.";
+ 99="Modified 'dhcp-to-dns', which dropped global configuration. Settings moved to dhcp server's network definitions.";
+ 100="The script 'ssh-keys-import' became a module 'mod/ssh-keys-import' with enhanced functionality.";
+ 101="Introduced new script 'fw-addr-lists' to download, import and update firewall address-lists.";
+ 102="Modified 'hotspot-to-wpa' to support non-local (radius) users.";
+ 103="Dropped hard-coded name and timeout from 'hotspot-to-wpa-cleanup', instead a comment is required for dhcp server now.";
+ 104="All relevant scripts were ported to new wifiwave2 and are available for AX devices now!";
+ 105="Extended 'check-routeros-update' to support automatic update from specific neighbor(s).";
+ 106="Modified 'telegram-chat' to make it act on message replies, without activation. Also made it answer a single question mark with a short notice.";
+ 107="Dropped support for non-fixed width font in Telegram notifications.";
+ 108="Enhanced 'log-forward' to list log messages with colorful bullets to indicate severity.";
+ 109="Added support to send notifications via Ntfy (ntfy.sh).";
+ 110="Dropped support for loading scripts from local storage.";
+ 111="Modified 'dhcp-to-dns' to allow multiple records for one mac address.";
+ 112="Enhanced 'mod/ssh-keys-import' to record the fingerprint of keys.";
+ 113="Added helper functions for easier setup to Matrix notification module.";
+ 114="All relevant scripts were ported to new wifi package for RouterOS 7.13 and later. Migration is complex and thus not done automatically!";
+ 115=("Celebrating " . [ $SymbolForNotification "sparkles,star" ] . "1.000 stars " . [ $SymbolForNotification "star,sparkles" ] . "on Github! Please continue starring...");
+ 116=("... and also please keep in mind that it takes a huge amount of time maintaining these scripts. " . [ $IfThenElse ($IDonate != true) \
+ ("Following the donation hint " . [ $SymbolForNotification "arrow-down" "below" ] . "to keep me motivated is much appreciated. Thanks!") \
+ ("Looks like you did donate already. " . [ $SymbolForNotification "heart" "<3" ] . "Much appreciated, thanks!") ]);
+ 117="Enhanced 'packages-update' to support deferred reboot on automatically installed updates.";
+ 118=("RouterOS packages increase in size with each release. This becomes a problem for devices with 16MB storage and below. " . \
+ [ $IfThenElse ($Resource->"total-hdd-space" < 16000000) ("Your " . $Resource->"board-name" . " is specifically affected! ") \
+ [ $IfThenElse ($Resource->"free-hdd-space" > 4000000) ("(Your " . $Resource->"board-name" . " does not suffer this issue.) ") ] ] . \
+ "Huge configuration and lots of scripts give an extra risk. Take care!");
+ 119="Added support for IPv6 to script 'fw-addr-lists'.";
+ 120="Implemented a workaround in 'backup-cloud'. Now script should no longer just crash, but send notification with error.";
+ 121="The 'wifiwave2' scripts are finally gone. Development continues with 'wifi' in RouterOS 7.13 and later.";
+ 122="The global configuration was enhanced to support loading snippets. Configuration can be split off to scripts where name starts with 'global-config-overlay.d/'.";
+ 123="Introduced new function '\$LogPrint', and deprecated '\$LogPrintExit2'. Please update custom scripts if you use it.";
+ 124="Added support for links in 'netwatch-notify', these are added below the formatted notification text.";
+ 125=("April's Fool! " . [ $SymbolForNotification "smiley-partying-face" ] . "Well, you missed it... - no charge nor fees. (Anyway... Donations are much appreciated, " . [ $SymbolForNotification "smiley-smiling-face" ] . "thanks!)");
+ 126="Made 'telegram-chat' capable of handling large command output. Telegram messages still limit the size, so it is truncated now.";
+ 127="Added support for authentication to Ntfy notification module.";
+};
+
+# Migration steps to be applied on script updates
+:global GlobalConfigMigration {
+ 97=":local Rec [ /ip/dns/static/find where comment~\"^managed by dhcp-to-dns for \" ]; :if ([ :len \$Rec ] > 0) do={ /ip/dns/static/remove \$Rec; /system/script/run dhcp-to-dns; }";
+ 100=":global ScriptInstallUpdate; :if ([ :len [ /system/script/find where name=\"ssh-keys-import\" source~\"^#!rsc by RouterOS\\n\" ] ] > 0) do={ /system/script/set name=\"mod/ssh-keys-import\" ssh-keys-import; \$ScriptInstallUpdate; }";
+ 104=":global CharacterReplace; :global ScriptInstallUpdate; :foreach Script in={ \"capsman-download-packages\"; \"capsman-rolling-upgrade\"; \"hotspot-to-wpa\"; \"hotspot-to-wpa-cleanup\" } do={ /system/script/set name=(\$Script . \".capsman\") [ find where name=\$Script ]; :foreach Scheduler in=[ /system/scheduler/find where on-event~(\$Script . \"([^-.]|\\\$)\") ] do={ /system/scheduler/set \$Scheduler on-event=[ \$CharacterReplace [ get \$Scheduler on-event ] \$Script (\$Script . \".capsman\") ]; }; }; /ip/hotspot/user/profile/set on-login=\"hotspot-to-wpa.capsman\" [ find where on-login=\"hotspot-to-wpa\" ]; \$ScriptInstallUpdate;";
+ 111=":local Rec [ /ip/dns/static/find where comment~\"^managed by dhcp-to-dns for \" ]; :if ([ :len \$Rec ] > 0) do={ /ip/dns/static/remove \$Rec; /system/script/run dhcp-to-dns; }";
+};
diff --git a/ospf-to-leds.rsc b/ospf-to-leds.rsc
new file mode 100644
index 0000000..0932815
--- /dev/null
+++ b/ospf-to-leds.rsc
@@ -0,0 +1,45 @@
+#!rsc by RouterOS
+# RouterOS script: ospf-to-leds
+# Copyright (c) 2020-2024 Christian Hesse <mail@eworm.de>
+# https://git.eworm.de/cgit/routeros-scripts/about/COPYING.md
+#
+# requires RouterOS, version=7.13
+#
+# visualize ospf instance state via leds
+# https://git.eworm.de/cgit/routeros-scripts/about/doc/ospf-to-leds.md
+
+:global GlobalFunctionsReady;
+:while ($GlobalFunctionsReady != true) do={ :delay 500ms; }
+
+:do {
+ :local ScriptName [ :jobname ];
+
+ :global LogPrint;
+ :global ParseKeyValueStore;
+ :global ScriptLock;
+
+ :if ([ $ScriptLock $ScriptName ] = false) do={
+ :error false;
+ }
+
+ :foreach Instance in=[ /routing/ospf/instance/find where comment~"^ospf-to-leds," ] do={
+ :local InstanceVal [ /routing/ospf/instance/get $Instance ];
+ :local LED ([ $ParseKeyValueStore ($InstanceVal->"comment") ]->"leds");
+ :local LEDType [ /system/leds/get [ find where leds=$LED ] type ];
+
+ :local NeighborCount 0;
+ :foreach Area in=[ /routing/ospf/area/find where instance=($InstanceVal->"name") ] do={
+ :local AreaName [ /routing/ospf/area/get $Area name ];
+ :set NeighborCount ($NeighborCount + [ :len [ /routing/ospf/neighbor/find where area=$AreaName ] ]);
+ }
+
+ :if ($NeighborCount > 0 && $LEDType = "off") do={
+ $LogPrint info $ScriptName ("OSPF instance " . $InstanceVal->"name" . " has " . $NeighborCount . " neighbors, led on!");
+ /system/leds/set type=on [ find where leds=$LED ];
+ }
+ :if ($NeighborCount = 0 && $LEDType = "on") do={
+ $LogPrint info $ScriptName ("OSPF instance " . $InstanceVal->"name" . " has no neighbors, led off!");
+ /system/leds/set type=off [ find where leds=$LED ];
+ }
+ }
+} on-error={ }
diff --git a/packages-update b/packages-update
deleted file mode 100644
index 8918656..0000000
--- a/packages-update
+++ /dev/null
@@ -1,59 +0,0 @@
-#!rsc
-# RouterOS script: packages-update
-# Copyright (c) 2019-2020 Christian Hesse <mail@eworm.de>
-#
-# download packages and reboot for installation
-
-:global DownloadPackage;
-:global ScriptFromTerminal;
-:global ScriptLock;
-
-$ScriptLock "packages-update";
-
-:local Update [ / system package update get ];
-
-:if ([ :typeof ($Update->"latest-version") ] = "nothing") do={
- :log warning "Latest version is not known.";
- :error "Latest version is not known.";
-}
-
-:if ($Update->"installed-version" = $Update->"latest-version") do={
- :log info ("Version " . $Update->"latest-version" . " is already installed.");
- :error "No updates available.";
-}
-
-:foreach Package in=[ / system package find where !bundle ] do={
- :local PkgName [ / system package get $Package name ];
- if ([ $DownloadPackage $PkgName ($Update->"latest-version") ] = false) do={
- :log error ("Download for package " . $PkgName . " failed.");
- :error "Error: See log for details.";
- }
-}
-
-:foreach Script in=[ / system script find where name~"^(email|upload)-backup\$" ] do={
- / system script run $Script;
-}
-
-:if ([ $ScriptFromTerminal "packages-update" ] = true) do={
- :if (!([ /system resource get version ] ~ ($Update->"channel"))) do={
- :put "Update channel changed. Want to downgrade? [y/N]";
- :if ([ :terminal inkey timeout=60 ] = 121) do={
- :log info ("Rebooting for downgrade.");
- :delay 1s;
- / system package downgrade;
- }
- }
-
- :put "Do you want to (s)chedule reboot or (r)eboot now? [s/R]";
- :if ([ :terminal inkey timeout=60 ] = 115) do={
- / system scheduler add name="reboot-for-update" start-time=03:00:00 interval=1d \
- on-event=(":global RandomDelay; \$RandomDelay 3600; " . \
- "/ system scheduler remove reboot-for-update; / system reboot;");
- :log info ("Scheduled reboot for update between 03:00 and 04:00.");
- :error ("Scheduled reboot.");
- }
-}
-
-:log info ("Rebooting for update.");
-:delay 1s;
-/ system reboot;
diff --git a/packages-update.rsc b/packages-update.rsc
new file mode 100644
index 0000000..0208b1e
--- /dev/null
+++ b/packages-update.rsc
@@ -0,0 +1,145 @@
+#!rsc by RouterOS
+# RouterOS script: packages-update
+# Copyright (c) 2019-2024 Christian Hesse <mail@eworm.de>
+# https://git.eworm.de/cgit/routeros-scripts/about/COPYING.md
+#
+# requires RouterOS, version=7.13
+#
+# download packages and reboot for installation
+# https://git.eworm.de/cgit/routeros-scripts/about/doc/packages-update.md
+
+:global GlobalFunctionsReady;
+:while ($GlobalFunctionsReady != true) do={ :delay 500ms; }
+
+:do {
+ :local ScriptName [ :jobname ];
+
+ :global DownloadPackage;
+ :global Grep;
+ :global LogPrint;
+ :global ParseKeyValueStore;
+ :global ScriptFromTerminal;
+ :global ScriptLock;
+ :global VersionToNum;
+
+ :global PackagesUpdateDeferReboot;
+ :global PackagesUpdateBackupFailure;
+
+ :local Schedule do={
+ :local ScriptName [ :tostr $1 ];
+
+ :global GetRandomNumber;
+ :global LogPrint;
+
+ :global RebootForUpdate do={
+ /system/reboot;
+ }
+
+ :local StartTime [ :tostr [ :totime (10800 + [ $GetRandomNumber 7200 ]) ] ];
+ /system/scheduler/add name="_RebootForUpdate" start-time=$StartTime interval=1d \
+ on-event=("/system/scheduler/remove \"_RebootForUpdate\"; " . \
+ ":global RebootForUpdate; \$RebootForUpdate;");
+ $LogPrint info $ScriptName ("Scheduled reboot for update at " . $StartTime . \
+ " local time (" . [ /system/clock/get time-zone-name ] . ").");
+ :return true;
+ }
+
+ :if ([ $ScriptLock $ScriptName ] = false) do={
+ :error false;
+ }
+
+ :local Update [ /system/package/update/get ];
+
+ :if ([ :typeof ($Update->"latest-version") ] = "nothing") do={
+ $LogPrint warning $ScriptName ("Latest version is not known.");
+ :error false;
+ }
+
+ :if ($Update->"installed-version" = $Update->"latest-version") do={
+ $LogPrint info $ScriptName ("Version " . $Update->"latest-version" . " is already installed.");
+ :error true;
+ }
+
+ :local NumInstalled [ $VersionToNum ($Update->"installed-version") ];
+ :local NumLatest [ $VersionToNum ($Update->"latest-version") ];
+
+ :local DoDowngrade false;
+ :if ($NumInstalled > $NumLatest) do={
+ :if ([ $ScriptFromTerminal $ScriptName ] = true) do={
+ :put "Latest version is older than installed one. Want to downgrade? [y/N]";
+ :if (([ /terminal/inkey timeout=60 ] % 32) = 25) do={
+ :set DoDowngrade true;
+ } else={
+ :put "Canceled...";
+ }
+ } else={
+ $LogPrint warning $ScriptName ("Not installing downgrade automatically.");
+ :error false;
+ }
+ }
+
+ :foreach Package in=[ /system/package/find where !bundle ] do={
+ :local PkgName [ /system/package/get $Package name ];
+ :if ([ $DownloadPackage $PkgName ($Update->"latest-version") ] = false) do={
+ $LogPrint error $ScriptName ("Download for package " . $PkgName . " failed, update aborted.");
+ :error false;
+ }
+ }
+
+ :local RunOrder ({});
+ :foreach Script in=[ /system/script/find where source~("\n# provides: backup-script\\b") ] do={
+ :local ScriptVal [ /system/script/get $Script ];
+ :local Store [ $ParseKeyValueStore [ $Grep ($ScriptVal->"source") ("\23 provides: backup-script, ") ] ];
+
+ :set ($RunOrder->($Store->"order" . "-" . $ScriptVal->"name")) ($ScriptVal->"name");
+ }
+
+ :foreach Order,Script in=$RunOrder do={
+ :set PackagesUpdateBackupFailure false;
+ :do {
+ $LogPrint info $ScriptName ("Running backup script " . $Script . " before update.");
+ /system/script/run $Script;
+ } on-error={
+ :set PackagesUpdateBackupFailure true;
+ }
+
+ :if ($PackagesUpdateBackupFailure = true) do={
+ $LogPrint warning $ScriptName ("Running backup script " . $Script . " before update failed!");
+ :if ([ $ScriptFromTerminal $ScriptName ] = true) do={
+ :put "Do you want to continue anyway? [y/N]";
+ :if (([ /terminal/inkey timeout=60 ] % 32) = 25) do={
+ $LogPrint info $ScriptName ("User requested to continue anyway.");
+ } else={
+ $LogPrint info $ScriptName ("Canceled update...");
+ :error false;
+ }
+ } else={
+ $LogPrint warning $ScriptName ("Canceled non-interactive update.");
+ :error false;
+ }
+ }
+ }
+
+ :if ($DoDowngrade = true) do={
+ $LogPrint info $ScriptName ("Rebooting for downgrade.");
+ :delay 1s;
+ /system/package/downgrade;
+ }
+
+ :if ([ $ScriptFromTerminal $ScriptName ] = true) do={
+ :put "Do you want to (s)chedule reboot or (r)eboot now? [s/R]";
+ :if (([ /terminal/inkey timeout=60 ] % 32) = 19) do={
+ $Schedule $ScriptName;
+ :error true;
+ }
+ } else={
+ :if ($PackagesUpdateDeferReboot = true) do={
+ $Schedule $ScriptName;
+ :error true;
+ }
+ }
+
+ $LogPrint info $ScriptName ("Rebooting for update.");
+ :delay 1s;
+ /system/reboot;
+} on-error={ }
diff --git a/ppp-on-up b/ppp-on-up
deleted file mode 100644
index b8f8fcb..0000000
--- a/ppp-on-up
+++ /dev/null
@@ -1,28 +0,0 @@
-#!rsc
-# RouterOS script: ppp-on-up
-# Copyright (c) 2013-2020 Christian Hesse <mail@eworm.de>
-#
-# run scripts on ppp up
-
-:local Interface $interface;
-
-:if ([ :typeof $Interface ] = "nothing") do={
- :log error "This script is supposed to run from ppp on-up script hook.";
- :error "Error: See log for details.";
-}
-
-:local IntName [ / interface get $Interface name ];
-:log info ("PPP interface " . $IntName . " is up.");
-
-/ ipv6 dhcp-client release [ find where interface=$IntName !disabled ];
-
-:local Scripts {
- "update-tunnelbroker"
-}
-
-:foreach Script in=$Scripts do={
- :if ([ / system script print count-only where name=$Script ] > 0) do={
- :log debug ("Running script from ppp-on-up: " . $Script);
- / system script run $Script;
- }
-}
diff --git a/ppp-on-up.rsc b/ppp-on-up.rsc
new file mode 100644
index 0000000..4ed92c5
--- /dev/null
+++ b/ppp-on-up.rsc
@@ -0,0 +1,40 @@
+#!rsc by RouterOS
+# RouterOS script: ppp-on-up
+# Copyright (c) 2013-2024 Christian Hesse <mail@eworm.de>
+# https://git.eworm.de/cgit/routeros-scripts/about/COPYING.md
+#
+# requires RouterOS, version=7.13
+#
+# run scripts on ppp up
+# https://git.eworm.de/cgit/routeros-scripts/about/doc/ppp-on-up.md
+
+:global GlobalFunctionsReady;
+:while ($GlobalFunctionsReady != true) do={ :delay 500ms; }
+
+:do {
+ :local ScriptName [ :jobname ];
+
+ :global LogPrint;
+
+ :local Interface $interface;
+
+ :if ([ :typeof $Interface ] = "nothing") do={
+ $LogPrint error $ScriptName ("This script is supposed to run from ppp on-up script hook.");
+ :error false;
+ }
+
+ :local IntName [ /interface/get $Interface name ];
+ $LogPrint info $ScriptName ("PPP interface " . $IntName . " is up.");
+
+ /ipv6/dhcp-client/release [ find where interface=$IntName !disabled ];
+
+ :foreach Script in=[ /system/script/find where source~("\n# provides: ppp-on-up\n") ] do={
+ :local ScriptName [ /system/script/get $Script name ];
+ :do {
+ $LogPrint debug $ScriptName ("Running script: " . $ScriptName);
+ /system/script/run $Script;
+ } on-error={
+ $LogPrint warning $ScriptName ("Running script '" . $ScriptName . "' failed!");
+ }
+ }
+} on-error={ }
diff --git a/rotate-ntp b/rotate-ntp
deleted file mode 100644
index 465a46c..0000000
--- a/rotate-ntp
+++ /dev/null
@@ -1,17 +0,0 @@
-#!rsc
-# RouterOS script: rotate-ntp
-# Copyright (c) 2013-2020 Christian Hesse <mail@eworm.de>
-#
-# rotate the ntp servers
-
-:global NtpPool;
-
-:local Ntp1 [ :resolve ("0." . $NtpPool) ];
-:local Ntp2 [ :resolve ("1." . $NtpPool) ];
-
-:if ([ / system ntp client get enabled ] != true) do={
- :log warning "NTP client is not enabled!";
-}
-
-:log info ("Updating NTP servers to " . $Ntp1 . " and " . $Ntp2);
-/ system ntp client set primary-ntp=$Ntp1 secondary-ntp=$Ntp2;
diff --git a/script-updates b/script-updates
deleted file mode 100644
index 95af07d..0000000
--- a/script-updates
+++ /dev/null
@@ -1,130 +0,0 @@
-#!rsc
-# RouterOS script: script-updates
-# Copyright (c) 2013-2020 Christian Hesse <mail@eworm.de>
-#
-# update installed scripts from file or url
-
-:global ExpectedConfigVersion;
-:global GlobalConfigVersion;
-:global Identity;
-:global IDonate;
-:global SentConfigChangesNotification;
-:global ScriptUpdatesFetch;
-:global ScriptUpdatesBaseUrl;
-:global ScriptUpdatesUrlSuffix;
-:global ScriptUpdatesIgnore;
-:global ScriptUpdatesConfigChangesIgnore;
-
-:global SendNotification;
-
-:foreach Script in=[ / system script find ] do={
- :local Ignore 0;
- :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={
- :log warning ("Policies differ for script " . $ScriptVal->"name" . \
- " and its scheduler " . $SchedulerVal->"name" . "!");
- }
- }
-
- :if ([ :len $SourceNew ] = 0 && $ScriptUpdatesFetch = true) do={
- :foreach IgnoreLoop in=$ScriptUpdatesIgnore do={
- :if ($IgnoreLoop = $ScriptVal->"name") do={ :set Ignore 1; }
- }
-
- :if ($Ignore = 0) do={
- :log debug ("Fetching script from url: " . $ScriptVal->"name");
- :do {
- :local Result [ / tool fetch check-certificate=yes-without-crl \
- ($ScriptUpdatesBaseUrl . $ScriptVal->"name" . $ScriptUpdatesUrlSuffix) \
- output=user as-value ];
- :if ($Result->"status" = "finished") do={
- :set SourceNew ($Result->"data");
- }
- } on-error={
- :log info ("Failed fetching " . $ScriptVal->"name");
- }
- }
- }
-
- :if ([ :len $SourceNew ] > 0) do={
- :if ([ :pick $SourceNew 0 5 ] = "#!rsc") do={
- :if ($SourceNew != $ScriptVal->"source") do={
- :local DontRequirePermissions \
- ($SourceNew~"\n# requires: dont-require-permissions=yes\n");
- :log info ("Updating script: " . $ScriptVal->"name");
- / system script set owner=($ScriptVal->"name") source=$SourceNew \
- dont-require-permissions=$DontRequirePermissions $Script;
- :if ($ScriptVal->"name" = "global-config" && \
- [ / system script print count-only where name="global-config-overlay" ] > 0) do={
- / system script { run global-config; run global-config-overlay; }
- }
- :if ($ScriptVal->"name" = "global-functions") do={
- / system script run global-functions;
- }
- } else={
- :log debug ("Script " . $ScriptVal->"name" . " did not change.");
- }
- } else={
- :log warning ("Looks like new script " . $ScriptVal->"name" . " is not valid. Ignoring!");
- }
- } else={
- :log debug ("No update for script " . $ScriptVal->"name" . ".");
- }
-}
-
-:if ($ScriptUpdatesConfigChangesIgnore!=true && \
- $SentConfigChangesNotification!=$ExpectedConfigVersion && \
- $GlobalConfigVersion < $ExpectedConfigVersion) do={
- :global GlobalConfigChanges;
- :local ChangeLogCode;
- :local ConfigScript "global-config";
- :if ([ /system script print count-only where name="global-config-overlay" ] > 0) do={
- :set ConfigScript "global-config-overlay";
- }
- :local NotificationMessage ("Current configuration on " . $Identity . \
- " is out of date. Please update " . $ConfigScript . ", then increase " . \
- "variable GlobalConfigVersion (currently " . $GlobalConfigVersion . \
- ") to " . $ExpectedConfigVersion . " and re-run " . $ConfigScript . ".");
-
- :log debug ("Fetching changelog.");
- :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");
- }
- :set NotificationMessage ($NotificationMessage . "\n\nChanges:");
- [ :parse $ChangeLogCode ];
- :for I from=($GlobalConfigVersion + 1) to=$ExpectedConfigVersion do={
- :set NotificationMessage ($NotificationMessage . \
- "\n * " . $GlobalConfigChanges->[ :tostr $I ]);
- }
- :set GlobalConfigChanges;
- } on-error={
- :log info ("Failed fetching changes!");
- :set NotificationMessage ($NotificationMessage . \
- "\n\nChanges are not available.");
- }
-
- :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:\n" . \
- "https://git.eworm.de/cgit/routeros-scripts/about/#donate");
- }
-
- $SendNotification "Configuration warning!" $NotificationMessage;
- :set SentConfigChangesNotification $ExpectedConfigVersion;
-}
diff --git a/sms-action b/sms-action
deleted file mode 100644
index 026bdc8..0000000
--- a/sms-action
+++ /dev/null
@@ -1,21 +0,0 @@
-#!rsc
-# RouterOS script: sms-action
-# Copyright (c) 2018-2020 Christian Hesse <mail@eworm.de>
-#
-# run action on received SMS
-
-:global SmsAction;
-
-:local Action $action;
-
-:if ([ :typeof $Action ] = "nothing") do={
- :log error "This script is supposed to run from SMS hook with action=...";
- :error "Error: See log for details.";
-}
-
-:local Code ($SmsAction->$Action);
-:local Parsed [ :parse $Code ];
-
-:log info ("Acting on SMS action '" . $Action . "': " . $Code);
-:delay 1s;
-$Parsed;
diff --git a/sms-action.rsc b/sms-action.rsc
new file mode 100644
index 0000000..70bfb28
--- /dev/null
+++ b/sms-action.rsc
@@ -0,0 +1,37 @@
+#!rsc by RouterOS
+# RouterOS script: sms-action
+# Copyright (c) 2018-2024 Christian Hesse <mail@eworm.de>
+# https://git.eworm.de/cgit/routeros-scripts/about/COPYING.md
+#
+# requires RouterOS, version=7.13
+#
+# run action on received SMS
+# https://git.eworm.de/cgit/routeros-scripts/about/doc/sms-action.md
+
+:global GlobalFunctionsReady;
+:while ($GlobalFunctionsReady != true) do={ :delay 500ms; }
+
+:do {
+ :local ScriptName [ :jobname ];
+
+ :global SmsAction;
+
+ :global LogPrint;
+ :global ValidateSyntax;
+
+ :local Action $action;
+
+ :if ([ :typeof $Action ] = "nothing") do={
+ $LogPrint error $ScriptName ("This script is supposed to run from SMS hook with action=...");
+ :error false;
+ }
+
+ :local Code ($SmsAction->$Action);
+ :if ([ $ValidateSyntax $Code ] = true) do={
+ :log info ("Acting on SMS action '" . $Action . "': " . $Code);
+ :delay 1s;
+ [ :parse $Code ];
+ } else={
+ $LogPrint warning $ScriptName ("The code for action '" . $Action . "' failed syntax validation!");
+ }
+} on-error={ }
diff --git a/sms-forward b/sms-forward
deleted file mode 100644
index c05600e..0000000
--- a/sms-forward
+++ /dev/null
@@ -1,47 +0,0 @@
-#!rsc
-# RouterOS script: sms-forward
-# Copyright (c) 2013-2020 Christian Hesse <mail@eworm.de>
-#
-# forward SMS to e-mail
-
-:global Identity;
-
-:global SendNotification;
-
-# check mail server
-:if ([ / tool netwatch get [ find where comment=[ / tool e-mail get address ] ] status ] != "up") do={
- :log warning "Mail server is not up.";
- :error "Warning: See log for details.";
-}
-
-:local Settings [ / tool sms get ];
-
-# forward SMS in a loop
-:while ([ / tool sms inbox print count-only ] > 0) do={
- :local Phone [ / tool sms inbox get ([ find ]->0) phone ];
- :local Messages "";
- :local Delete [ :toarray "" ];
-
- :foreach Sms in=[ / tool sms inbox find where phone=$Phone ] do={
- :local SmsVal [ / tool sms inbox get $Sms ];
-
- :if ($Phone = $Settings->"allowed-number" && \
- ($SmsVal->"message")~("^:cmd " . $Settings->"secret" . " script ")) do={
- :log debug "Removing SMS, which started a script.";
- / tool sms inbox remove $Sms;
- } else={
- :set Messages ($Messages . "\n\nOn " . $SmsVal->"timestamp" . \
- " type " . $SmsVal->"type" . ":\n" . $SmsVal->"message");
- :set Delete ($Delete, $Sms);
- }
- }
-
- :if ([ :len $Messages ] > 0) do={
- $SendNotification ("SMS Forwarding from " . $Phone) \
- ("These message(s) were received by " . $Identity . \
- " from " . $Phone . ":" . $Messages);
- :foreach Sms in=$Delete do={
- / tool sms inbox remove $Sms;
- }
- }
-}
diff --git a/sms-forward.rsc b/sms-forward.rsc
new file mode 100644
index 0000000..477d11e
--- /dev/null
+++ b/sms-forward.rsc
@@ -0,0 +1,95 @@
+#!rsc by RouterOS
+# RouterOS script: sms-forward
+# Copyright (c) 2013-2024 Christian Hesse <mail@eworm.de>
+# Anatoly Bubenkov <bubenkoff@gmail.com>
+# https://git.eworm.de/cgit/routeros-scripts/about/COPYING.md
+#
+# requires RouterOS, version=7.13
+#
+# forward SMS to e-mail
+# https://git.eworm.de/cgit/routeros-scripts/about/doc/sms-forward.md
+
+:global GlobalFunctionsReady;
+:while ($GlobalFunctionsReady != true) do={ :delay 500ms; }
+
+:do {
+ :local ScriptName [ :jobname ];
+
+ :global Identity;
+ :global SmsForwardHooks;
+
+ :global IfThenElse;
+ :global LogPrint;
+ :global LogPrintOnce;
+ :global ScriptLock;
+ :global SendNotification2;
+ :global SymbolForNotification;
+ :global ValidateSyntax;
+ :global WaitFullyConnected;
+
+ :if ([ $ScriptLock $ScriptName ] = false) do={
+ :error false;
+ }
+
+ :if ([ /tool/sms/get receive-enabled ] = false) do={
+ $LogPrintOnce warning $ScriptName ("Receiving of SMS is not enabled.");
+ :error false;
+ }
+
+ $WaitFullyConnected;
+
+ :local Settings [ /tool/sms/get ];
+
+ :if ([ /interface/lte/get ($Settings->"port") running ] != true) do={
+ $LogPrint info $ScriptName ("The LTE interface is not in running state, skipping.");
+ :error true;
+ }
+
+ # forward SMS in a loop
+ :while ([ :len [ /tool/sms/inbox/find ] ] > 0) do={
+ :local Phone [ /tool/sms/inbox/get ([ find ]->0) phone ];
+ :local Messages "";
+ :local Delete ({});
+
+ :foreach Sms in=[ /tool/sms/inbox/find where phone=$Phone ] do={
+ :local SmsVal [ /tool/sms/inbox/get $Sms ];
+
+ :if ($Phone = $Settings->"allowed-number" && \
+ ($SmsVal->"message")~("^:cmd " . $Settings->"secret" . " script ")) do={
+ $LogPrint debug $ScriptName ("Removing SMS, which started a script.");
+ /tool/sms/inbox/remove $Sms;
+ } else={
+ :set Messages ($Messages . "\n\nOn " . $SmsVal->"timestamp" . \
+ " type " . $SmsVal->"type" . ":\n" . $SmsVal->"message");
+ :foreach Hook in=$SmsForwardHooks do={
+ :if ($Phone~($Hook->"allowed-number") && ($SmsVal->"message")~($Hook->"match")) do={
+ :if ([ $ValidateSyntax ($Hook->"command") ] = true) do={
+ $LogPrint info $ScriptName ("Running hook '" . $Hook->"match" . "': " . $Hook->"command");
+ :do {
+ :local Command [ :parse ($Hook->"command") ];
+ $Command Phone=$Phone Message=($SmsVal->"message");
+ :set Messages ($Messages . "\n\nRan hook '" . $Hook->"match" . "':\n" . $Hook->"command");
+ } on-error={
+ $LogPrint warning $ScriptName ("The code for hook '" . $Hook->"match" . "' failed to run!");
+ }
+ } else={
+ $LogPrint warning $ScriptName ("The code for hook '" . $Hook->"match" . "' failed syntax validation!");
+ }
+ }
+ }
+ :set Delete ($Delete, $Sms);
+ }
+ }
+
+ :if ([ :len $Messages ] > 0) do={
+ :local Count [ :len $Delete ];
+ $SendNotification2 ({ origin=$ScriptName; \
+ subject=([ $SymbolForNotification "incoming-envelope" ] . "SMS Forwarding from " . $Phone); \
+ message=("Received " . [ $IfThenElse ($Count = 1) "this message" ("these " . $Count . " messages") ] . \
+ " by " . $Identity . " from " . $Phone . ":" . $Messages) });
+ :foreach Sms in=$Delete do={
+ /tool/sms/inbox/remove $Sms;
+ }
+ }
+ }
+} on-error={ }
diff --git a/ssh-keys-import b/ssh-keys-import
deleted file mode 100644
index da933ce..0000000
--- a/ssh-keys-import
+++ /dev/null
@@ -1,11 +0,0 @@
-#!rsc
-# RouterOS script: ssh-keys-import
-# Copyright (c) 2013-2020 Christian Hesse <mail@eworm.de>
-#
-# import ssh keys from file
-
-# Split files with several keys from a shell...
-# while read type key name; do echo $type $key $name > $name.pub; done < keys.pub
-# ... then transfer with scp/sftp.
-
-:foreach Key in=[ / file find where type="ssh key" ] do={ / user ssh-key import user=admin public-key-file=[ / file get $Key name ]; }
diff --git a/super-mario-theme b/super-mario-theme.rsc
index 671abc5..63308b0 100644
--- a/super-mario-theme
+++ b/super-mario-theme.rsc
@@ -1,8 +1,10 @@
-#!rsc
+#!rsc by RouterOS
# RouterOS script: super-mario-theme
-# Copyright (c) 2013-2020 Christian Hesse <mail@eworm.de>
+# Copyright (c) 2013-2024 Christian Hesse <mail@eworm.de>
+# https://git.eworm.de/cgit/routeros-scripts/about/COPYING.md
#
# play Super Mario theme
+# https://git.eworm.de/cgit/routeros-scripts/about/doc/super-mario-theme.md
:local Beeps {
{ 660; 100 }; 150; { 660; 100 }; 300; { 660; 100 }; 300;
diff --git a/telegram-chat.rsc b/telegram-chat.rsc
new file mode 100644
index 0000000..f8dcd42
--- /dev/null
+++ b/telegram-chat.rsc
@@ -0,0 +1,178 @@
+#!rsc by RouterOS
+# RouterOS script: telegram-chat
+# Copyright (c) 2023-2024 Christian Hesse <mail@eworm.de>
+# https://git.eworm.de/cgit/routeros-scripts/about/COPYING.md
+#
+# requires RouterOS, version=7.13
+#
+# use Telegram to chat with your Router and send commands
+# https://git.eworm.de/cgit/routeros-scripts/about/doc/telegram-chat.md
+
+:global GlobalFunctionsReady;
+:while ($GlobalFunctionsReady != true) do={ :delay 500ms; }
+
+:do {
+ :local ScriptName [ :jobname ];
+
+ :global Identity;
+ :global TelegramChatActive;
+ :global TelegramChatGroups;
+ :global TelegramChatId;
+ :global TelegramChatIdsTrusted;
+ :global TelegramChatOffset;
+ :global TelegramChatRunTime;
+ :global TelegramMessageIDs;
+ :global TelegramRandomDelay;
+ :global TelegramTokenId;
+
+ :global CertificateAvailable;
+ :global EitherOr;
+ :global EscapeForRegEx;
+ :global GetRandom20CharAlNum;
+ :global IfThenElse;
+ :global LogPrint;
+ :global MAX;
+ :global MIN;
+ :global MkDir;
+ :global RandomDelay;
+ :global ScriptLock;
+ :global SendTelegram2;
+ :global SymbolForNotification;
+ :global ValidateSyntax;
+ :global WaitForFile;
+ :global WaitFullyConnected;
+
+ :if ([ $ScriptLock $ScriptName ] = false) do={
+ :error false;
+ }
+
+ $WaitFullyConnected;
+
+ :if ([ :typeof $TelegramChatOffset ] != "array") do={
+ :set TelegramChatOffset { 0; 0; 0 };
+ }
+ :if ([ :typeof $TelegramRandomDelay ] != "num") do={
+ :set TelegramRandomDelay 0;
+ }
+
+ :if ([ $CertificateAvailable "Go Daddy Secure Certificate Authority - G2" ] = false) do={
+ $LogPrint warning $ScriptName ("Downloading required certificate failed.");
+ :error false;
+ }
+
+ $RandomDelay $TelegramRandomDelay;
+
+ :local Data false;
+ :for I from=1 to=4 do={
+ :if ($Data = false) do={
+ :do {
+ :set Data ([ /tool/fetch check-certificate=yes-without-crl output=user \
+ ("https://api.telegram.org/bot" . $TelegramTokenId . "/getUpdates?offset=" . \
+ $TelegramChatOffset->0 . "&allowed_updates=%5B%22message%22%5D") as-value ]->"data");
+ :set TelegramRandomDelay [ $MAX 0 ($TelegramRandomDelay - 1) ];
+ } on-error={
+ :if ($I < 4) do={
+ $LogPrint debug $ScriptName ("Fetch failed, " . $I . ". try.");
+ :set TelegramRandomDelay [ $MIN 15 ($TelegramRandomDelay + 5) ];
+ :delay (($I * $I) . "s");
+ }
+ }
+ }
+ }
+
+ :if ($Data = false) do={
+ $LogPrint warning $ScriptName ("Failed getting updates.");
+ :error false;
+ }
+
+ :local JSON [ :deserialize from=json value=$Data ];
+ :local UpdateID 0;
+ :local Uptime [ /system/resource/get uptime ];
+ :foreach Update in=($JSON->"result") do={
+ :set UpdateID ($Update->"update_id");
+ :local Message ($Update->"message");
+ :local IsReply [ :len ($Message->"reply_to_message") ];
+ :local IsMyReply ($TelegramMessageIDs->[ :tostr ($Message->"reply_to_message"->"message_id") ]);
+ :if (($IsMyReply = 1 || $TelegramChatOffset->0 > 0 || $Uptime > 5m) && $UpdateID >= $TelegramChatOffset->2) do={
+ :local Trusted false;
+ :local Chat ($Message->"chat");
+ :local From ($Message->"from");
+
+ :foreach IdsTrusted in=($TelegramChatId, $TelegramChatIdsTrusted) do={
+ :if ($From->"id" = $IdsTrusted || $From->"username" = $IdsTrusted) do={
+ :set Trusted true;
+ }
+ }
+
+ :if ($Trusted = true) do={
+ :local Done false;
+ :if ($Message->"text" = "?") do={
+ $LogPrint info $ScriptName ("Sending notice for update " . $UpdateID . ".");
+ $SendTelegram2 ({ origin=$ScriptName; chatid=($Chat->"id"); silent=true; replyto=($Message->"message_id"); \
+ subject=([ $SymbolForNotification "speech-balloon" ] . "Telegram Chat"); \
+ message=("Online" . [ $IfThenElse $TelegramChatActive " (and active!)" ] . ", awaiting your commands!") });
+ :set Done true;
+ }
+ :if ($Done = false && [ :pick ($Message->"text") 0 1 ] = "!") do={
+ :if ($Message->"text" ~ ("^! *(" . [ $EscapeForRegEx $Identity ] . "|@" . $TelegramChatGroups . ")\$")) do={
+ :set TelegramChatActive true;
+ } else={
+ :set TelegramChatActive false;
+ }
+ $LogPrint info $ScriptName ("Now " . [ $IfThenElse $TelegramChatActive "active" "passive" ] . \
+ " from update " . $UpdateID . "!");
+ :set Done true;
+ }
+ :if ($Done = false && ($IsMyReply = 1 || ($IsReply = 0 && $TelegramChatActive = true)) && [ :len ($Message->"text") ] > 0) do={
+ :if ([ $ValidateSyntax ($Message->"text") ] = true) do={
+ :local State "";
+ :local File ("tmpfs/telegram-chat/" . [ $GetRandom20CharAlNum 6 ]);
+ :if ([ $MkDir "tmpfs/telegram-chat" ] = false) do={
+ $LogPrint error $ScriptName ("Failed creating directory!");
+ :error false;
+ }
+ $LogPrint info $ScriptName ("Running command from update " . $UpdateID . ": " . $Message->"text");
+ :execute script=(":do {\n" . $Message->"text" . "\n} on-error={ /file/add name=\"" . $File . ".failed\" };" . \
+ "/file/add name=\"" . $File . ".done\"") file=($File . "\00");
+ :if ([ $WaitForFile ($File . ".done") [ $EitherOr $TelegramChatRunTime 20s ] ] = false) do={
+ :set State ([ $SymbolForNotification "warning-sign" ] . "The command did not finish, still running in background.\n\n");
+ }
+ :if ([ :len [ /file/find where name=($File . ".failed") ] ] > 0) do={
+ :set State ([ $SymbolForNotification "cross-mark" ] . "The command failed with an error!\n\n");
+ }
+ :local Content ([ /file/read chunk-size=32768 file=$File as-value ]->"data");
+ $SendTelegram2 ({ origin=$ScriptName; chatid=($Chat->"id"); silent=true; replyto=($Message->"message_id"); \
+ subject=([ $SymbolForNotification "speech-balloon" ] . "Telegram Chat"); \
+ message=([ $SymbolForNotification "gear" ] . "Command:\n" . $Message->"text" . "\n\n" . \
+ $State . [ $IfThenElse ([ :len $Content ] > 0) \
+ ([ $SymbolForNotification "memo" ] . "Output:\n" . $Content) \
+ ([ $SymbolForNotification "memo" ] . "No output.") ]) });
+ /file/remove "tmpfs/telegram-chat";
+ } else={
+ $LogPrint info $ScriptName ("The command from update " . $UpdateID . " failed syntax validation!");
+ $SendTelegram2 ({ origin=$ScriptName; chatid=($Chat->"id"); silent=false; replyto=($Message->"message_id"); \
+ subject=([ $SymbolForNotification "speech-balloon" ] . "Telegram Chat"); \
+ message=([ $SymbolForNotification "gear" ] . "Command:\n" . $Message->"text" . "\n\n" . \
+ [ $SymbolForNotification "cross-mark" ] . "The command failed syntax validation!") });
+ }
+ }
+ } else={
+ :local MessageText ("Received a message from untrusted contact " . \
+ [ $IfThenElse ([ :len ($From->"username") ] = 0) "without username" ("'" . $From->"username" . "'") ] . \
+ " (ID " . $From->"id" . ") in update " . $UpdateID . "!");
+ :if ($Message->"text" ~ ("^! *" . [ $EscapeForRegEx $Identity ] . "\$")) do={
+ $LogPrint warning $ScriptName $MessageText;
+ $SendTelegram2 ({ origin=$ScriptName; chatid=($Chat->"id"); silent=false; replyto=($Message->"message_id"); \
+ subject=([ $SymbolForNotification "speech-balloon" ] . "Telegram Chat"); \
+ message=("You are not trusted.") });
+ } else={
+ $LogPrint info $ScriptName $MessageText;
+ }
+ }
+ } else={
+ $LogPrint debug $ScriptName ("Already handled update " . $UpdateID . ".");
+ }
+ }
+ :set TelegramChatOffset ([ :pick $TelegramChatOffset 1 3 ], \
+ [ $IfThenElse ($UpdateID >= $TelegramChatOffset->2) ($UpdateID + 1) ($TelegramChatOffset->2) ]);
+} on-error={ }
diff --git a/template.md b/template.md
deleted file mode 100644
index 8a9aeb7..0000000
--- a/template.md
+++ /dev/null
@@ -1,47 +0,0 @@
-Script `template`
-=================
-
-[◀ Go back to main README](README.md)
-
-Description
------------
-
-Short description...
-
-In detail
----------
-
-Get all the details...
-
-Requirements and installation
------------------------------
-
-We need...
-
-... then we install:
-
- [admin@MikroTik] > / system script add name=template
- [admin@MikroTik] > / system script run script-updates
-
-Configuration
--------------
-
-The configuration goes to `global-config`, These are the parameters:
-
-* ...
-
-Usage and invocation
---------------------
-
-This is intended...
-
-See also
---------
-
-* [another script](template.md)
-* ...
-
----
-[◀ Go back to main README](README.md)
-
-[▲ Go back to top](#top)
diff --git a/unattended-lte-firmware-upgrade b/unattended-lte-firmware-upgrade
deleted file mode 100644
index a0b297e..0000000
--- a/unattended-lte-firmware-upgrade
+++ /dev/null
@@ -1,28 +0,0 @@
-#!rsc
-# RouterOS script: unattended-lte-firmware-upgrade
-# Copyright (c) 2018-2020 Christian Hesse <mail@eworm.de>
-#
-# schedule unattended lte firmware upgrade
-
-:foreach Interface in=[ / interface lte find ] do={
- :local Firmware;
- :local IntName [ / interface lte get $Interface name ];
- :do {
- :set Firmware [ / interface lte firmware-upgrade $Interface once as-value ];
- } on-error={
- :log debug ("Could not get latest LTE firmware version for interface " . $IntName . ".");
- }
-
- :if (($Firmware->"installed") != ($Firmware->"latest")) do={
- :log info ("Scheduling LTE firmware upgrade for interface " . $IntName . ".");
- / system script add name=($IntName . "-firmware-upgrade") source=("# unattended-lte-firmware-upgrade\n" . \
- "/ system scheduler remove " . $IntName . "-firmware-upgrade;\n" . \
- "/ system script remove " . $IntName . "-firmware-upgrade;\n" . \
- "/ interface lte firmware-upgrade " . $IntName . " upgrade=yes;\n" . \
- ":log info (\"LTE firmware upgrade finished, waiting for installation before reset.\");\n" . \
- ":delay 150s;\n" . \
- "/ interface lte at-chat " . $IntName . " input=\"AT+RESET\";");
- / system scheduler add name=($IntName . "-firmware-upgrade") \
- on-event=("/ system script run " . $IntName . "-firmware-upgrade;") interval=1m;
- }
-}
diff --git a/unattended-lte-firmware-upgrade.rsc b/unattended-lte-firmware-upgrade.rsc
new file mode 100644
index 0000000..904f952
--- /dev/null
+++ b/unattended-lte-firmware-upgrade.rsc
@@ -0,0 +1,49 @@
+#!rsc by RouterOS
+# RouterOS script: unattended-lte-firmware-upgrade
+# Copyright (c) 2018-2024 Christian Hesse <mail@eworm.de>
+# https://git.eworm.de/cgit/routeros-scripts/about/COPYING.md
+#
+# schedule unattended lte firmware upgrade
+# https://git.eworm.de/cgit/routeros-scripts/about/doc/unattended-lte-firmware-upgrade.md
+
+:foreach Interface in=[ /interface/lte/find where running ] do={
+ :local Firmware;
+ :local IntName [ /interface/lte/get $Interface name ];
+ :do {
+ :set Firmware [ /interface/lte/firmware-upgrade $Interface once as-value ];
+ } on-error={
+ :log debug ("Could not get latest LTE firmware version for interface " . $IntName . ".");
+ }
+
+ :if ([ :typeof $Firmware ] = "array") do={
+ :if (($Firmware->"installed") != ($Firmware->"latest")) do={
+ :log info ("Scheduling LTE firmware upgrade for interface " . $IntName . ".");
+
+ :global LTEFirmwareUpgrade do={
+ :global LTEFirmwareUpgrade;
+ :set LTEFirmwareUpgrade;
+
+ /system/scheduler/remove ($1 . "-firmware-upgrade");
+ :do {
+ /interface/lte/firmware-upgrade $1 upgrade=yes;
+ :log info ("LTE firmware upgrade on '" . $1 . "' finished, waiting for reset.");
+ :delay 240s;
+ :local Firmware [ /interface/lte/firmware-upgrade $1 once as-value ];
+ :if (($Firmware->"installed") != ($Firmware->"latest")) do={
+ :log warning ("LTE firmware versions still differ. Resetting again...");
+ /interface/lte/at-chat $1 input="AT+RESET";
+ }
+ } on-error={
+ :log error ("LTE firmware upgrade on '" . $1 . "' failed.");
+ }
+ }
+
+ /system/scheduler/add name=($IntName . "-firmware-upgrade") start-time=startup interval=2s \
+ on-event=(":global LTEFirmwareUpgrade; \$LTEFirmwareUpgrade \"" . $IntName . "\";");
+ } else={
+ :log info ("The LTE firmware is up to date on interface " . $IntName . ".");
+ }
+ } else={
+ :log info ("No LTE firmware information available for interface " . $IntName . ".");
+ }
+}
diff --git a/update-gre-address b/update-gre-address
deleted file mode 100644
index fcd0183..0000000
--- a/update-gre-address
+++ /dev/null
@@ -1,23 +0,0 @@
-#!rsc
-# RouterOS script: update-gre-address
-# Copyright (c) 2013-2020 Christian Hesse <mail@eworm.de>
-#
-# update gre interface remote address with dynamic address from
-# ipsec remote peer
-
-/ interface gre set remote-address=0.0.0.0 disabled=yes [ find where !running !disabled ];
-
-:foreach Peer in=[ / ip ipsec active-peers find ] do={
- :local PeerVal [ / ip ipsec active-peers get $Peer ];
- :local GreInt [ / interface gre find where comment=$PeerVal->"id" ];
- :if ([ :len $GreInt ] > 0) do={
- :local GreIntVal [ / interface gre get $GreInt ];
- :if ([ :typeof ($PeerVal->"dynamic-address") ] = "str" && \
- ($PeerVal->"dynamic-address" != $GreIntVal->"remote-address" || \
- $GreIntVal->"disabled" = true)) do={
- :log info ("Updating remote address for interface " . $GreIntVal->"name" . " to " . $PeerVal->"dynamic-address");
- / interface gre set remote-address=0.0.0.0 disabled=yes [ find where remote-address=$PeerVal->"dynamic-address" name!=$GreIntVal->"name" ];
- / interface gre set $GreInt remote-address=($PeerVal->"dynamic-address") disabled=no;
- }
- }
-}
diff --git a/update-gre-address.rsc b/update-gre-address.rsc
new file mode 100644
index 0000000..76d0c81
--- /dev/null
+++ b/update-gre-address.rsc
@@ -0,0 +1,42 @@
+#!rsc by RouterOS
+# RouterOS script: update-gre-address
+# Copyright (c) 2013-2024 Christian Hesse <mail@eworm.de>
+# https://git.eworm.de/cgit/routeros-scripts/about/COPYING.md
+#
+# requires RouterOS, version=7.13
+#
+# update gre interface remote address with dynamic address from
+# ipsec remote peer
+# https://git.eworm.de/cgit/routeros-scripts/about/doc/update-gre-address.md
+
+:global GlobalFunctionsReady;
+:while ($GlobalFunctionsReady != true) do={ :delay 500ms; }
+
+:do {
+ :local ScriptName [ :jobname ];
+
+ :global CharacterReplace;
+ :global LogPrint;
+ :global ScriptLock;
+
+ :if ([ $ScriptLock $ScriptName ] = false) do={
+ :error false;
+ }
+
+ /interface/gre/set remote-address=0.0.0.0 disabled=yes [ find where !running !disabled ];
+
+ :foreach Peer in=[ /ip/ipsec/active-peers/find ] do={
+ :local PeerVal [ /ip/ipsec/active-peers/get $Peer ];
+ :local GreInt [ /interface/gre/find where comment=($PeerVal->"id") or comment=[ $CharacterReplace ($PeerVal->"id") "CN=" "" ] ];
+ :if ([ :len $GreInt ] > 0) do={
+ :local GreIntVal [ /interface/gre/get $GreInt ];
+ :if ([ :typeof ($PeerVal->"dynamic-address") ] = "str" && \
+ ($PeerVal->"dynamic-address" != $GreIntVal->"remote-address" || \
+ $GreIntVal->"disabled" = true)) do={
+ $LogPrint info $ScriptName ("Updating remote address for interface " . $GreIntVal->"name" . " to " . $PeerVal->"dynamic-address");
+ /interface/gre/set remote-address=0.0.0.0 disabled=yes [ find where remote-address=$PeerVal->"dynamic-address" name!=$GreIntVal->"name" ];
+ /interface/gre/set $GreInt remote-address=($PeerVal->"dynamic-address") disabled=no;
+ }
+ }
+ }
+} on-error={ }
diff --git a/update-tunnelbroker b/update-tunnelbroker
deleted file mode 100644
index 53c8600..0000000
--- a/update-tunnelbroker
+++ /dev/null
@@ -1,36 +0,0 @@
-#!rsc
-# RouterOS script: update-tunnelbroker
-# Copyright (c) 2013-2020 Christian Hesse <mail@eworm.de>
-# Michael Gisbers <michael@gisbers.de>
-
-:global CertificateAvailable;
-:global ParseKeyValueStore;
-
-:if ([ / ip cloud get ddns-enabled ] != true) do={
- :log error "IP cloud DDNS is not enabled.";
- :error "Error: See log for details.";
-}
-
-# Get the current ip address from cloud
-/ ip cloud force-update;
-:while ([ / ip cloud get status ] != "updated") do={
- :delay 1s;
-}
-:local PublicAddress [ / ip cloud get public-address ];
-
-:foreach Interface in=[ / interface 6to4 find where comment~"^tunnelbroker" !disabled ] do={
- :local InterfaceVal [ / interface 6to4 get $Interface ];
-
- :if ($PublicAddress != $InterfaceVal->"local-address") do={
- :local Comment [ $ParseKeyValueStore ($InterfaceVal->"comment") ];
-
- $CertificateAvailable "Starfield Secure Certificate Authority - G2";
- :log info ("Local address changed, sending UPDATE to tunnelbroker! New address: " . $PublicAddress);
- / tool fetch check-certificate=yes-without-crl \
- ("https://ipv4.tunnelbroker.net/nic/update\?hostname=" . $Comment->"id") \
- user=($Comment->"user") password=($Comment->"pass") keep-result=no;
- / interface 6to4 set $Interface local-address=$PublicAddress;
- } else={
- :log debug ("All tunnelbroker configuration is up to date for interface " . $InterfaceVal->"name" . ".");
- }
-}
diff --git a/update-tunnelbroker.rsc b/update-tunnelbroker.rsc
new file mode 100644
index 0000000..364dc08
--- /dev/null
+++ b/update-tunnelbroker.rsc
@@ -0,0 +1,67 @@
+#!rsc by RouterOS
+# RouterOS script: update-tunnelbroker
+# Copyright (c) 2013-2024 Christian Hesse <mail@eworm.de>
+# Michael Gisbers <michael@gisbers.de>
+# https://git.eworm.de/cgit/routeros-scripts/about/COPYING.md
+#
+# provides: ppp-on-up
+# requires RouterOS, version=7.13
+#
+# update local address of tunnelbroker interface
+# https://git.eworm.de/cgit/routeros-scripts/about/doc/update-tunnelbroker.md
+
+:global GlobalFunctionsReady;
+:while ($GlobalFunctionsReady != true) do={ :delay 500ms; }
+
+:do {
+ :local ScriptName [ :jobname ];
+
+ :global CertificateAvailable;
+ :global LogPrint;
+ :global ParseKeyValueStore;
+ :global ScriptLock;
+
+ :if ([ $ScriptLock $ScriptName ] = false) do={
+ :error false;
+ }
+
+ :if ([ $CertificateAvailable "Starfield Secure Certificate Authority - G2" ] = false) do={
+ $LogPrint error $ScriptName ("Downloading required certificate failed.");
+ :error false;
+ }
+
+ :foreach Interface in=[ /interface/6to4/find where comment~"^tunnelbroker" !disabled ] do={
+ :local Data false;
+ :local InterfaceVal [ /interface/6to4/get $Interface ];
+ :local Comment [ $ParseKeyValueStore ($InterfaceVal->"comment") ];
+
+ :for I from=2 to=0 do={
+ :if ($Data = false) do={
+ :do {
+ :set Data ([ /tool/fetch check-certificate=yes-without-crl \
+ ("https://ipv4.tunnelbroker.net/nic/update?hostname=" . $Comment->"id") \
+ user=($Comment->"user") password=($Comment->"pass") output=user as-value ]->"data");
+ } on-error={
+ $LogPrint debug $ScriptName ("Failed downloading, " . $I . " retries pending.");
+ :delay 2s;
+ }
+ }
+ }
+
+ :if (!($Data ~ "^(good|nochg) ")) do={
+ $LogPrint error $ScriptName ("Failed sending the local address to tunnelbroker or unexpected response!");
+ :error false;
+ }
+
+ :local PublicAddress [ :pick $Data ([ :find $Data " " ] + 1) [ :find $Data "\n" ] ];
+
+ :if ($PublicAddress != $InterfaceVal->"local-address") do={
+ :if ([ :len [ /ip/address find where address~("^" . $PublicAddress . "/") ] ] < 1) do={
+ $LogPrint warning $ScriptName ("The address " . $PublicAddress . " is not configured on your device. NAT by ISP?");
+ }
+
+ $LogPrint info $ScriptName ("Local address changed, updating tunnel configuration with address: " . $PublicAddress);
+ /interface/6to4/set $Interface local-address=$PublicAddress;
+ }
+ }
+} on-error={ }
diff --git a/upload-backup b/upload-backup
deleted file mode 100644
index 4a10653..0000000
--- a/upload-backup
+++ /dev/null
@@ -1,64 +0,0 @@
-#!rsc
-# RouterOS script: upload-backup
-# Copyright (c) 2013-2020 Christian Hesse <mail@eworm.de>
-#
-# create and upload backup and config file
-
-:global Identity;
-:global Domain;
-:global BackupUploadUrl;
-:global BackupUploadUser;
-:global BackupUploadPass;
-:global BackupSendBinary;
-:global BackupSendExport;
-:global BackupPassword;
-
-:global CharacterReplace;
-:global DeviceInfo;
-:global SendNotification;
-
-:if ($BackupSendBinary != true && \
- $BackupSendExport != true) do={
- :log error ("Configured to send neither backup nor config export.");
- :error "Error: See log for details.";
-}
-
-# filename based on identity
-:local FileName [ $CharacterReplace ($Identity . "." . $Domain) "." "_" ];
-:local BackupFile "none";
-:local ConfigFile "none";
-
-# binary backup
-:if ($BackupSendBinary = true) do={
- / system backup save encryption=aes-sha256 name=$FileName password=$BackupPassword;
-
- :do {
- / tool fetch upload=yes url=($BackupUploadUrl . "/" . $FileName . ".backup") \
- user=$BackupUploadUser password=$BackupUploadPass src-path=($FileName . ".backup");
- :set BackupFile ($FileName . ".backup");
- } on-error={
- :log error ("Uploading backup file failed!");
- :set BackupFile "failed";
- }
-}
-
-# create configuration export
-:if ($BackupSendExport = true) do={
- / export terse file=$FileName;
-
- :do {
- / tool fetch upload=yes url=($BackupUploadUrl . "/" . $FileName . ".rsc") \
- user=$BackupUploadUser password=$BackupUploadPass src-path=($FileName . ".rsc");
- :set ConfigFile ($FileName . ".rsc");
- } on-error={
- :log error ("Uploading configuration export failed!");
- :set ConfigFile "failed";
- }
-}
-
-$SendNotification "Backup & Config Upload" \
- ("Backup and config export upload for " . $Identity . ".\n\n" . \
- [ $DeviceInfo ] . "\n\n" . \
- "Backup file: " . $BackupFile . "\n" . \
- "Config file: " . $ConfigFile) "" "true";
-}