Расширение браузера для управления маршрутами на Микротике
- четверг, 8 февраля 2024 г. в 00:00:17
Моя домашняя сеть состоит из нескольких хостов по стране и планете, три провайдера заходит на роутер – нормальная тренировочная площадка для получения новых знаний. Можно выйти в большую сеть из любого шлюза своей, для этого написана простая система правил – локальный адрес находящийся в определённом списке адресов будет маршрутизирован на заданный в правиле файрволла шлюз, либо, при обращении к домену находящемуся в определённом списке адресов файрволла маршрут к нему будет идти через заданный в правиле шлюз. Всё просто – хочешь побродить по большой сети другими маршрутами – перенёс свой локальный адрес в нужный список, хочешь, чтобы маршрут к сайту был всегда через определённый шлюз – внёс его домен или адрес в нужный список. Знаете, как мне надоело заходить в интерфейс маршрутизатора каждый раз, когда требуется внести адрес в список? Лень взяла верх и на днях заставила написать плагин для браузеров облегчающий эту работу.
Вот небольшое видео посмотреть на плагин и его работу:
Приложение совсем не энтерпрайз, скорее, админский скрипт с нормальным интерфейсом. Публикую сейчас, пока данная версия универсальна для любого Микротика и может кому-то оказаться полезной, дальше могу уйти в написание собственного API на своём web-сервере, и решение станет намного более тяжеловесным и сложным. Плагин для браузера писал первый раз, пользовался статьёй: https://habr.com/ru/articles/703330/, большое спасибо! Подключение расширения браузера описывать не буду, об этом подробно написано в указанной статье. Плагин работает в браузерах Chrome и Edge – для сёрфинга использую именно их, Mozilla для разработки, остальное для всякого. Возможно, позже добавлю поддержку других браузеров.
Приложение работает с роутерами MikroTik по REST API, для этого стоит создать отдельного пользователя на устройстве в группе с правами: read, write, api, rest-api, и правом авторизации с ограниченного списка хостов. Управление маршрутами для локального адреса происходит следующими правилами файрволла:
/ip firewall mangle chain=prerouting action=route passthrough=no route-dst=GATEWAY_ADDRESS src-address-list=routed-mos dst-address-list=!not-routed
/ip firewall mangle chain=prerouting action=mark-routing new-routing-mark=route-lte passthrough=no src-address-list=routed-lte dst-address-list=!not-routed
Когда локальный адрес находится в одном из описанных списков, его исходящий трафик маршрутится на указанный шлюз. Необходимо завести специальный список на файрволле немаршрутизируемых адресов, в него включить все локальные сети и внешние адреса всех своих хостов с которыми ваш роутер устанавливает соединения, чтобы исключить коллизии. Управление маршрутами для внешних ресурсов происходит такими правилами:
/ip firewall mangle chain=prerouting action=route passthrough=no route-dst=GATEWAY_ADDRESS dst-address-list=mos-domains
Когда обращаемся к хосту из заданного списка, трафик к нему пойдёт через указанный шлюз. Здесь есть много тонкостей, можно хранить адрес внешнего хоста, либо его доменное имя и система сама создаст список адресов откликающихся на это имя. Доменное имя в списке файрволла даёт нагрузку на устройство, порой система буквально каждую секунду опрашивает адреса хоста, на слабых платформах процессор с таким не справляется. Хороший пример такого хоста сайт – www.quora.com, и, наверное, любые другие соцсети, с коими не знаком, но пример подвернулся годный. Однако с такими ресурсами не сработает простое добавление их текущих адресов в список файрволла, они постоянно меняются, добавляются, тут либо смириться с нагрузкой на процессор и избирательно добавлять доменные имена в свои списки контролируя нагрузку, либо добавлять в списки пулы адресов замеченные за ресурсом.
Итак, собственно, плагин. Когда он установлен в панель инструментов, при вызове опрашивает у роутера текущий маршрут локального адреса и текущий маршрут до страницы на активной вкладке браузера, и даёт возможность изменить и то и другое одним нажатием. Информация об адресах берётся с моего API о котором уже писал, забава обрела пользу. Можете пользоваться приложением с этим API, я иногда читаю логи по хозяйски проверяя состояние хостинга и работоспособность бэка, больше вам это ничем не грозит. Не сложно переписать приложение на использование любого другого API.
До подключения расширения в браузер необходимо заполнить файл manifest.json – в массив "host_permissions" надо внести адрес своего роутера: "*://192.168.88.1/". В файле settings.js необходимо заполнить массивы "routes" и "domains" – ключи массива, это то, что отображается в списке выбора – произвольное наименование маршрута, значения – имена ваших списков адресов на файрволле. Пустое значение имени списка означает удаление текущих маршрутов для домена и отключение текущих маршрутов локального адреса – локальный адрес не удаляется из списка при переходе на основной маршрут, он становится неактивным. Если приложение обнаружит несколько копий локального адреса в перечисленных списках файрволла, все операции будут применимы к первому из них, остальные будут отключены. Если маршрутизируемый домен или его адрес отключен, приложение не будет его удалять при удалении его маршрута исходя из того, что вы видимо зачем-то специально его отключили.
Заполняем массивы "exclude" и "notrouted" – они суть одно и то же, адреса и подсети, которые приложение должно игнорировать и не назначать им маршруты. Разделены по сущностям, "notrouted" – все служебные сети, "exclude" – внешние адреса хостов сети, можно заполнять только один из этих массивов, на работоспособность это не повлияет. Ключи массивов – адреса подсетей (первый адрес диапазона образуемого подсетью), значения – значение префикса подсети. В приложении реализован поиск адреса по ключу ассоциативного массива – по сути, используем готовые хэши создаваемые платформой, очень производительное решение, но оценить его можно только на больших базах подсетей.
Остальные параметры можно не заполнять в файле, а заполнить их уже в приложении и они будут сохранены в локальном хранилище расширения браузера. Есть три способа хранить учетные данные пользователя API роутера: в открытом виде указать их в файле settings.js в полях "user" и "password" – можно, но лучше так не делать, пользоваться только в процессе отладки. Лучше оставить эти поля пустыми, внести имя и пароль в приложении, сохранить и они будут сохранены в локальном хранилище расширения в полях: "an" (имя) и "ap" (пароль), в слегка зашифрованном виде, будет спокойнее. А можно скопировать эти поля с зашифрованными значениями из локального хранилища и добавить их в файл settings.js вместе с "user" и "password" ("user" и "password" имеют приоритет для приложения, держите их пустыми, если не используете), но тогда надо очистить хранилище, оно имеет приоритет для полей "an" и "ap".
Как закончили с заполнением файла settings.js, подключаем расширение в браузер, включаем его на панель инструментов, запускаем и заполняем оставшуюся часть: локальный адрес используемый по умолчанию (ваша станция), добавлять домены в списки файрволла временно или нет, время жизни добавленного домена в список. Способ добавления домена в список: Top – только верхний домен, WWW – если верхний домен содержится в списке "www", в список файрволла будет так же добавлен следующий за ним домен, All – добавить в список все домены по второй уровень. Метод добавления домена: Domain – домен будет добавлен по имени, Address – по всем доступным адресам на домене – хороший вариант, но далеко не всегда работает, при богатом наборе адресов на хосте будет доступна только часть из них.
Раскрываем раздел Settings, заполняем адрес роутера, имя и пароль пользователя API, выбираем протокол, который вы настроили, заполняем список доменов верхнего уровня на свой вкус и жмём сохранить. После этого закроем и снова откроем плагин, теперь он готов к работе.
Когда добавляем локальный адрес в список с маршрутом, происходит удаление существующих подключений локального адреса с веб-портами API хоста и хоста страницы текущей вкладки браузера – это помогает быстрее произойти переключению маршрута, но не всегда всё происходит быстро, с этим моментом ещё надо разбираться, например, переход на Yota у меня происходит 4-5 секунд, как я не старался прибить все соединения. Однако на основной функционал это не влияет, переход между проводными соединениями происходит быстро.
При добавлении домена в список с маршрутом, происходит удаление всех существующих подключений локальной сети на веб-порты добавляемых доменов, это немного ускоряет переход на новый маршрут. После изменения маршрута вызывается обновление страницы текущей вкладки, сразу видим результат.
{
"name": "MikroTik Control Panel",
"description": "MikroTik Control Panel",
"version": "1.0",
"manifest_version": 3,
"icons": {
"16": "icons/logo16.png",
"32": "icons/logo32.png",
"48": "icons/logo48.png",
"128": "icons/logo128.png"
},
"action": {
"default_popup": "popup.html"
},
"permissions": [
"scripting",
"activeTab",
"storage"
],
"host_permissions": [
"*://192.168.88.1/"
],
"background": {}
}
const settings = {
"localhost": "192.168.88.77",
"router": "192.168.88.1",
"protocol": "http",
"user": "",
"password": "",
"defaults": {
"dynamic": false,
"time": "02:00:00",
"www": "www,web,wap,m",
"what": "www",
"how": "name"
},
"routes": {
"Default": "",
"Yota": "routed-lte",
"Rostelecom": "routed-dsl",
"Saint Petersburg": "routed-spb",
"Moscow": "routed-mos",
"New York": "routed-usa",
"Singapore": "routed-sin"
},
"domains": {
"Default": "",
"Moscow": "mos-domains",
"New York": "usa-domains",
"Singapore": "sin-domains"
},
"exclude": {
"1.1.1.1": 32,
"8.8.8.8": 32
},
"notrouted": {
"0.0.0.0": 8,
"10.0.0.0": 8,
"100.64.0.0": 10,
"127.0.0.0": 8,
"169.254.0.0": 16,
"172.16.0.0": 12,
"192.168.0.0": 16,
"192.0.0.0": 24,
"192.0.2.0": 24,
"192.88.99.0": 24,
"198.18.0.0": 15,
"198.51.100.0": 24,
"203.0.113.0": 24,
"224.0.0.0": 3
}
}
<!DOCTYPE html>
<html>
<head>
<title>MikroTik control panel</title>
<link rel="stylesheet" type="text/css" href="popup.css"/>
</head>
<body>
<p class="browntext" style="font-weight:bold;font-size:13px;">MikroTik Control Panel</p>
<p class="errormessage" id="error-messages" style="font-weight:normal;font-size:13px;"></p>
<p class="infomessage" id="current-address" style="margin-top:4px;font-weight:bold;font-size:14px;">-</p>
<p class="infomessage" id="address-city" style="font-size:12px;">-</p>
<p class="infomessage" id="address-provider" style="font-size:12px;">-</p>
<div style="margin-top:8px;">
<label for="local-address">Host:</label>
<input type="text" id="local-address" value="" onkeyup="" style="width:160px;margin-left:4px;text-align:left;" />
</div>
<div style="margin-top:10px;">
<label for="route-selection">Route:</label>
<select id="route-selection" style="width:152px;margin-left:5px;"></select>
</div>
<button id="route-button" class="formsbutton2">Apply</button>
<p class="infomessage" id="current-page" style="font-weight:bold;font-size:14px;">-</p>
<p class="infomessage" id="page-address" style="font-size:14px;">-</p>
<p class="infomessage" id="page-city" style="font-size:12px;">-</p>
<div style="margin-top:10px;">
<label for="domain-selection">Route:</label>
<select id="domain-selection" style="width:152px;margin-left:5px;"></select>
</div>
<div style="margin-top:11px;">
<input type="checkbox" id="domain-dynamic"/>
<label for="domain-dynamic">Dynamic:</label>
<input type="text" id="domain-time" value="" onkeyup="" style="width:112px;margin-left:4px;text-align:left;" />
</div>
<div style="margin-top:8px;">
<input type="radio" id="domain-top" name="what-add" />
<label for="domain-top">Top</label>
<input type="radio" id="domain-www" name="what-add" style="margin-left:10px;" checked />
<label for="domain-www">WWW</label>
<input type="radio" id="domain-all" name="what-add" style="margin-left:10px;" />
<label for="domain-all">All</label>
</div>
<div style="margin-top:8px;">
<input type="radio" id="domain-name" name="how-add" checked />
<label for="domain-name">Domain</label>
<input type="radio" id="domain-address" name="how-add" style="margin-left:10px;" />
<label for="domain-address">Address</label>
</div>
<button id="domain-button" class="formsbutton1">Apply</button>
<button id="settings-button" class="headerbutton1">Settings...</button>
<div id="settings-block" hidden>
<div style="margin-top:10px;">
<label for="router-address">Router:</label>
<input type="text" id="router-address" value="" onkeyup="" style="margin-left:4px;width:147px;text-align:left;" />
</div>
<div style="margin-top:10px;">
<label for="router-user">User:</label>
<input type="text" id="router-user" value="" onkeyup="" style="margin-left:4px;width:160px;text-align:left;" />
</div>
<div style="margin-top:10px;">
<label for="router-user">Password:</label>
<input type="password" id="router-password" value="" onkeyup="" style="margin-left:4px;width:130px;text-align:left;" />
</div>
<div style="margin-top:10px;">
<label for="protocol-selection">Protocol:</label>
<select id="protocol-selection" style="width:135px;margin-left:5px;">
<option value="http">HTTP</option>
<option value="https">HTTPS</option>
</select>
</div>
<div style="margin-top:10px;">
<label for="top-domains">WWW:</label>
<input type="text" id="top-domains" value="" onkeyup="" style="margin-left:4px;width:150px;text-align:left;" />
</div>
<button id="save-button" class="formsbutton1">Save settings</button>
<p class="infomessage" id="settings-info" style="font-size:14px;font-style:italic;"></p>
</div>
<script src="settings.js"></script>
<script src="popup.js"></script>
</body>
</html>
body {
text-align: center;
width: 200px;
}
label,
button {
cursor: pointer;
font-size: 14px;
-webkit-touch-callout: none;
-webkit-user-select: none;
-khtml-user-select: none;
-moz-user-select: none;
-ms-user-select: none;
user-select: none;
}
label + input {
font-size: 14px;
margin-top: 0px;
text-decoration: none;
vertical-align: middle;
display: inline-block;
}
input[type=checkbox],
input[type=radio] {
width: 18px;
height: 18px;
cursor: pointer;
border: 1px solid #ccc;
box-sizing: border-box;
vertical-align: middle;
accent-color: #3586E5;
margin-top: -2px;
margin-bottom: 0;
margin-left: 0;
margin-right: 2px;
padding: 0;
}
textarea,
input[type=text],
input[type=password] {
font-size: 14px;
padding: 5px;
border: 1px solid #ccc;
box-sizing: border-box;
background-color: #fff;
border-radius: 0;
}
select {
font-size: 14px;
font-weight: normal;
text-align: center;
padding: 5px;
border: 1px solid #ccc;
box-sizing: border-box;
background-color: #fff;
border-radius: 0;
}
button {
margin-top: 10px;
margin-bottom: 5px;
}
button:disabled {
background-color: #D1D1D1;
}
.fileupload,
.formsbutton0,
.formsbutton1,
.formsbutton2 {
font-size: 11pt;
font-weight: normal;
font-style: normal;
padding: 6px;
border: none;
cursor: pointer;
width: 200px;
display: inline-block;
}
.fileupload:enabled:hover,
.formsbutton0:enabled:hover,
.formsbutton1:enabled:hover,
.formsbutton2:enabled:hover,
.linktext:hover {
opacity: 0.8;
}
.fileupload:disabled,
.formsbutton0:disabled,
.formsbutton1:disabled,
.formsbutton2:disabled {
background-color: #D1D1D1;
cursor: default;
}
.formsbutton0 {
background-color: #E54335;
color: white;
}
.formsbutton1 {
background-color: #43A047;
color: white;
}
.fileupload,
.formsbutton2 {
background-color: #3586E5;
color: white;
}
.headerbutton1,
.headerbutton2 {
cursor: pointer;
background:none;
border:none;
margin:0;
padding:0;
}
.headerbutton1 {
font-size: 14px;
font-weight: bold;
color: #00695C;
}
.headerbutton2 {
font-size: 12px;
font-weight: normal;
color: #004D40;
}
.headerbutton1:hover,
.headerbutton2:hover {
text-decoration: underline;
opacity: 0.8;
}
.textmessage,
.infomessage,
.errormessage,
.bluetext,
.violettext,
.yellowtext,
.browntext {
margin: 0px;
padding: 0px;
}
@media (prefers-color-scheme: light) {
body {
}
.headerbutton1 {
color: #00695C;
}
.headerbutton2 {
color: #004D40;
}
.errormessage {
color: #D50000;
}
.bluetext {
color: MediumBlue;
}
.violettext {
color: DarkViolet;
}
.yellowtext {
color: #987F08;
}
.browntext {
color: #795548;
}
.infomessage,
.textmessage {
color: black;
}
}
@media (prefers-color-scheme: dark) {
body {
background: #111B23;
}
select,
textarea,
input[type=text],
input[type=password] {
background: #142633;
color: #CFD8DC;
}
select,
textarea,
input[type=text],
input[type=password],
input[type=checkbox],
input[type=radio] {
border-color: #78909C;
}
input[type=checkbox]:not(:checked),
input[type=radio]:not(:checked) {
opacity: 0.9;
}
.fileupload:focus,
.formsbutton0:focus,
.formsbutton1:focus,
.formsbutton2:focus,
input[type=checkbox]:focus,
input[type=radio]:focus,
select:focus,
textarea:focus,
input[type=text]:focus,
input[type=password]:focus {
outline-color: #CFD8DC;
}
.appheader1,
.appheader2,
.infomessage,
.textmessage,
label, h5, th, td {
color: #CFD8DC;
}
.errormessage {
color: #F77878;
}
.linktext {
color: #7986CB;
}
.linktext:focus {
outline-color: #7986CB;
}
.linktext:visited {
color: #BA68C8;
}
.linktext:visited:focus {
outline-color: #BA68C8;
}
.headerbutton1 {
color: #26A69A;
}
.headerbutton2 {
color: #4DB6AC;
}
.headerbutton1:focus {
outline-color: #26A69A;
}
.headerbutton1:focus {
outline-color: #4DB6AC;
}
.fileupload:disabled,
.formsbutton0:disabled,
.formsbutton1:disabled,
.formsbutton2:disabled {
background-color: #626567;
color: #A6ACAF;
}
.fileupload:enabled,
.formsbutton0:enabled,
.formsbutton1:enabled,
.formsbutton2:enabled {
color: #ECEFF1;
}
.bluetext {
color: #64B5F6;
}
.violettext {
color: #CE93D8;
}
.yellowtext {
color: #E6EE9C;
}
.browntext {
color: #A1887F;
}
}
let opened_tab = null;
let api_ip_address = null;
let api_ip_query = '';
let routes_query = '';
let domains_query = '';
let domains_regular = ',';
let exclude_minprefix = 32;
let exclude_maxprefix = 0;
let notrouted_minprefix = 32;
let notrouted_maxprefix = 0;
async function InitPage()
{
let load_settings = null;
await chrome.storage.local.get(["MikroTikControlPanelSettings"]).then((result) => {
if (typeof result.MikroTikControlPanelSettings != 'undefined')
load_settings = result.MikroTikControlPanelSettings;
});
if (load_settings !== null) {
settings['user'] = DecodeString(load_settings['an']);
settings['password'] = DecodeString(load_settings['ap']);
settings['localhost'] = load_settings['localhost'];
settings['router'] = load_settings['router'];
settings['protocol'] = load_settings['protocol'];
settings['defaults']['dynamic'] = load_settings['defaults']['dynamic'];
settings['defaults']['time'] = load_settings['defaults']['time'];
settings['defaults']['www'] = load_settings['defaults']['www'];
settings['defaults']['what'] = load_settings['defaults']['what'];
settings['defaults']['how'] = load_settings['defaults']['how'];
} else {
if ((typeof settings['user'] == 'undefined' || !settings['user']) && typeof settings['an'] != 'undefined' && settings['an'])
settings['user'] = ae(settings['an']);
if ((typeof settings['password'] == 'undefined' || !settings['password']) && typeof settings['ap'] != 'undefined' && settings['ap'])
settings['password'] = ae(settings['ap']);
}
for (let subnet in settings['exclude']) {
if (settings['exclude'][subnet] > exclude_maxprefix)
exclude_maxprefix = settings['exclude'][subnet];
if (settings['exclude'][subnet] < exclude_minprefix)
exclude_minprefix = settings['exclude'][subnet];
}
for (let subnet in settings['notrouted']) {
if (settings['notrouted'][subnet] > notrouted_maxprefix)
notrouted_maxprefix = settings['notrouted'][subnet];
if (settings['notrouted'][subnet] < notrouted_minprefix)
notrouted_minprefix = settings['notrouted'][subnet];
}
document.getElementById("local-address").value = settings['localhost'];
document.getElementById("router-address").value = settings['router'];
document.getElementById("router-user").value = settings['user'];
document.getElementById("router-password").value = settings['password'];
document.getElementById("protocol-selection").value = settings['protocol'];
document.getElementById("domain-dynamic").checked = settings['defaults']['dynamic'];
document.getElementById("domain-time").value = settings['defaults']['time'];
document.getElementById("domain-" + settings['defaults']['how']).checked = true;
document.getElementById("domain-" + settings['defaults']['what']).checked = true;
document.getElementById("top-domains").value = settings['defaults']['www'];
let routes_options = '';
let domains_options = '';
for (let idx in settings['routes']) {
routes_options += '<option value="' + settings['routes'][idx] + '">' + idx + '</option>';
if (settings['routes'][idx])
routes_query += '"list=' + settings['routes'][idx] + '",' + (routes_query ? '"#|",' : '');
}
for (let idx in settings['domains']) {
domains_options += '<option value="' + settings['domains'][idx] + '">' + idx + '</option>';
domains_regular += settings['domains'][idx] + ',';
if (settings['domains'][idx])
domains_query += '"list=' + settings['domains'][idx] + '",' + (domains_query ? '"#|",' : '');
}
document.getElementById("route-selection").innerHTML = routes_options;
document.getElementById("domain-selection").innerHTML = domains_options;
document.getElementById("settings-button").addEventListener("click", () => {
HideViewBlock('settings-block');
});
await chrome.tabs.query({active: true}, (tabs) => {
const tab = tabs[0];
if (tab) {
opened_tab = tab;
const url = new URL(tab.url);
const hostname = url.hostname;
document.getElementById("current-page").innerHTML = hostname;
if ((/^(\d{1,3}\.){3}\d{1,3}$/).test(hostname))
document.getElementById("domain-button").disabled = IpIsExclude(hostname);
if (hostname.indexOf('.') > 0) {
fetch("http://api.syo.su/ipwhois?" + hostname, {
method: "GET",
headers: { "Accept": "application/json" }
}).then((response) => { return response.json(); }).then((whois) => {
document.getElementById("page-address").innerHTML = whois['ip'];
document.getElementById("page-city").innerHTML = whois['ip2location']['city'] + ", " + whois['ip2location']['country'];
document.getElementById("domain-button").disabled = IpIsExclude(whois['ip']);
RefreshDomainInfo();
}).catch(function() {
SetExternalApiError();
});
} else {
document.getElementById("domain-button").disabled = true;
}
} else {
document.getElementById("domain-button").disabled = true;
}
});
RefreshAddressInfo();
let authuser = 'Basic ' + btoa(settings['user'] + ":" + settings['password']);
fetch(settings['protocol'] + "://" + settings['router'] + "/rest/ip/firewall/address-list/print", {
method: "POST",
headers: { "Accept": "application/json", "Content-Type": "application/json", "Authorization": authuser },
body: '{".query": [' + routes_query + '"address=' + settings['localhost'] + '","disabled=false"]}'
}).then((response) => { return response.json(); }).then((data) => {
document.getElementById("route-selection").value = (data.length ? data[0]['list'] : '');
}).catch(function() {
SetRouterApiError();
});
await fetch("http://api.syo.su/gethost?api.syo.su", {
method: "GET",
headers: { "Accept": "text/html" }
}).then((response) => { return response.text(); }).then((ip) => {
api_ip_address = ip;
api_ip_query = '"dst-address=' + ip + ':443","dst-address=' + ip + ':80","#|"';
});
document.getElementById("route-button").addEventListener("click", () => {
SetLocalhostRoute();
});
document.getElementById("domain-button").addEventListener("click", () => {
SetDomainRoute();
});
document.getElementById("save-button").addEventListener("click", () => {
SaveSettings();
});
}
function HideViewBlock(block_name)
{
let info_block = document.getElementById(block_name);
info_block.hidden = !info_block.hidden;
}
function SetRouterApiError()
{
document.getElementById("error-messages").innerHTML += "<b>Error connecting to router API</b><br>Check router address, username, password and protocol in settings";
document.getElementById("route-button").disabled = true;
document.getElementById("domain-button").disabled = true;
}
function SetExternalApiError()
{
document.getElementById("error-messages").innerHTML += "<b>Error connecting to external API</b><br>Check availability http://api.syo.su";
document.getElementById("domain-button").disabled = true;
}
function RefreshAddressInfo()
{
fetch("http://api.syo.su/myip", {
method: "GET",
headers: { "Accept": "text/html" }
}).then((response) => { return response.text(); }).then((data) => {
document.getElementById("current-address").innerHTML = data;
fetch("http://api.syo.su/ipwhois?" + data, {
method: "GET",
headers: { "Accept": "application/json" }
}).then((response) => { return response.json(); }).then((whois) => {
document.getElementById("address-city").innerHTML = whois['ip2location']['city'] + ", " + whois['ip2location']['country'];
document.getElementById("address-provider").innerHTML = whois['ip2location']['provider'];
}).catch(function() {
SetExternalApiError();
});
}).catch(function() {
SetExternalApiError();
});
}
async function SetLocalhostRoute()
{
let localhost = document.getElementById("local-address").value.replaceAll(' ', '');
let addrlist = document.getElementById("route-selection").value;
let address = document.getElementById("page-address").innerHTML;
let domain = document.getElementById("current-page").innerHTML;
let router = document.getElementById("router-address").value.replaceAll(' ', '');
let username = document.getElementById("router-user").value;
let userpass = document.getElementById("router-password").value;
let protocol = document.getElementById("protocol-selection").value;
let authuser = 'Basic ' + btoa(username + ":" + userpass);
let is_exclude = (!address || address == '-' || IpIsExclude(address));
let disable_from = 0;
let current_list = await fetch(protocol + "://" + router + "/rest/ip/firewall/address-list/print", {
method: "POST",
headers: { "Accept": "application/json", "Content-Type": "application/json", "Authorization": authuser },
body: '{".query": [' + routes_query + '"address=' + localhost + '"]}'
}).then((response) => { return response.json(); }).then((data) => {
return data;
});
let current_conn_query = api_ip_query;
if (!is_exclude)
current_conn_query += '"dst-address=' + address + ':443","dst-address=' + address + ':80","#|"' + (api_ip_query ? '"#|"' : "");
let current_conn = await fetch(protocol + "://" + router + "/rest/ip/firewall/connection/print", {
method: "POST",
headers: { "Accept": "application/json", "Content-Type": "application/json", "Authorization": authuser },
body: '{".query": [' + current_conn_query + ']}'
}).then((response) => { return response.json(); }).then((data) => {
return data;
});
let gateway_address = null;
if (current_conn.length)
gateway_address = current_conn[0]['reply-dst-address'].split(':')[0];
if (addrlist) {
disable_from = 1;
let fetch_method = null;
let fetch_url = null;
let sendbody = '"address":"' + localhost + '","disabled":"false","dynamic":"false","list":"' + addrlist + '"';
if (current_list.length) {
fetch_method = "PATCH";
fetch_url = protocol + "://" + router + "/rest/ip/firewall/address-list/" + current_list[0]['.id'];
let comment = (typeof current_list[0]['comment'] != 'undefined' ? ',"comment":"' + current_list[0]['comment'] + '"' : "");
sendbody = '".id":"' + current_list[0]['.id'] + '",' + sendbody + comment;
} else {
fetch_method = "POST";
fetch_url = protocol + "://" + router + "/rest/ip/firewall/address-list/add";
}
await fetch(fetch_url, {
method: fetch_method,
headers: { "Accept": "application/json", "Content-Type": "application/json", "Authorization": authuser },
body: '{' + sendbody + '}'
}).then((response) => { return response; });
}
for (let i = disable_from; i < current_list.length; i++)
if (current_list[i]['disabled'] == 'false')
await fetch(protocol + "://" + router + "/rest/ip/firewall/address-list/" + current_list[i]['.id'], {
method: "PATCH",
headers: { "Accept": "application/json", "Content-Type": "application/json", "Authorization": authuser },
body: '{".id":"' + current_list[i]['.id'] + '","address":"' + localhost + '","disabled":"true","dynamic":"' + current_list[i]['dynamic'] + '","list":"' + current_list[i]['list'] + '"}'
}).then((response) => { return response; });
for (let i = 0; i < current_conn.length; i++) {
if (localhost == current_conn[i]['src-address'].split(':')[0]) {
let check_address = await fetch(protocol + "://" + router + "/rest/ip/firewall/connection/" + current_conn[i]['.id'], {
method: "DELETE",
headers: { "Accept": "application/json", "Authorization": authuser }
}).then((response) => { return response; });
}
};
setTimeout(RefreshAddressInfo, 1000);
if (!is_exclude)
setTimeout(function() { chrome.tabs.reload(opened_tab.id); }, 1000);
}
function RefreshDomainInfo()
{
let domain = document.getElementById("current-page").innerHTML;
let address = document.getElementById("page-address").innerHTML;
let router = document.getElementById("router-address").value.replaceAll(' ', '');
let username = document.getElementById("router-user").value;
let userpass = document.getElementById("router-password").value;
let protocol = document.getElementById("protocol-selection").value;
let authuser = 'Basic ' + btoa(username + ":" + userpass);
fetch(protocol + "://" + router + "/rest/ip/firewall/address-list/print", {
method: "POST",
headers: { "Accept": "application/json", "Content-Type": "application/json", "Authorization": authuser },
body: '{".query": [' + domains_query + '"comment=' + domain + '","address=' + address + '","#&","address=' + domain + '","#|","disabled=false","timeout","#!","dynamic=true","#&!"]}'
}).then((response) => { return response.json(); }).then((data) => {
if (data.length) {
document.getElementById("domain-time").value = (data[0]['dynamic'] == 'true' ? data[0]['timeout'] : settings['defaults']['time']);
document.getElementById("domain-dynamic").checked = (data[0]['dynamic'] == 'true');
document.getElementById("domain-selection").value = data[0]['list'];
} else {
document.getElementById("domain-time").value = settings['defaults']['time'];
document.getElementById("domain-dynamic").checked = settings['defaults']['dynamic'];
document.getElementById("domain-selection").value = "";
}
});
}
async function SetDomainRoute()
{
let domain = document.getElementById("current-page").innerHTML;
let address = document.getElementById("page-address").innerHTML;
let addrlist = document.getElementById("domain-selection").value;
let dynamic = document.getElementById("domain-dynamic").checked;
let dyntime = document.getElementById("domain-time").value.replaceAll(' ', '');
let router = document.getElementById("router-address").value.replaceAll(' ', '');
let username = document.getElementById("router-user").value;
let userpass = document.getElementById("router-password").value;
let protocol = document.getElementById("protocol-selection").value;
let top_domains_template = ',' + document.getElementById("top-domains").value.replaceAll(' ', '').toLowerCase() + ',';
let as_address = document.getElementById("domain-address").checked;
let add_www = document.getElementById("domain-www").checked;
let add_all = document.getElementById("domain-all").checked;
let authuser = 'Basic ' + btoa(username + ":" + userpass);
let is_exclude = (!address || address == '-' || IpIsExclude(address));
if (is_exclude)
return 0;
let domains = new Array();
let addresses = new Array();
let error_messages = new Array();
if (domain != address) {
let domain_struct = domain.split('.');
let steps_count = 1;
if (add_www) {
if (top_domains_template.indexOf(',' + domain_struct[0] + ',') >= 0)
steps_count = 2;
} else if (add_all) {
steps_count = domain_struct.length - 1;
}
let domain_next = domain;
for (let i = 0; i < steps_count; i++) {
domains[domain_next] = new Array();
if (as_address) {
let hosts = await fetch("http://api.syo.su/gethosts?" + domain_next, {
method: "GET",
headers: { "Accept": "text/html" }
}).then((response) => {
return response.text().split('<br>');
}).catch(function() {
return null;
});
if (hosts !== null) {
for (let i = 0; i < hosts.length; i++)
if (!addresses.includes(hosts[i])) {
addresses.push(hosts[i]);
domains[domain_next].push(hosts[i]);
}
} else
error_messages.push("Error API access");
} else {
domains[domain_next].push(domain_next);
}
domain_next = domain_next.substr(domain_struct[i].length + 1);
}
}
let current_query = '';
let current_addr_query = '';
for (let dom in domains) {
current_query += '"comment=' + dom + '","address=' + dom + '","#|",' + (current_query ? '"#|",' : '');
current_addr_query += '"comment=' + dom + '",' + (current_addr_query ? '"#|",' : '');
}
let current_list = await fetch(protocol + "://" + router + "/rest/ip/firewall/address-list/print", {
method: "POST",
headers: { "Accept": "application/json", "Content-Type": "application/json", "Authorization": authuser },
body: '{".query": [' + domains_query + current_query + '"disabled=false","timeout","#!","dynamic=true","#&!"]}'
}).then((response) => { return response.json(); }).then((data) => {
return data;
});
let current_addrs = new Array();
let current_num = null;
if (current_list.length)
current_num = 0;
if (addrlist) {
for (let dom in domains) {
for (let i = 0; i < domains[dom].length; i++) {
let set_address = domains[dom][i];
let sendbody = '"address":"' + set_address + '","list":"' + addrlist + '","disabled":"false"';
if (as_address)
sendbody += ',"comment":"' + dom + '"';
if (dynamic)
sendbody += ',"dynamic":"true","timeout":"' + dyntime + '"';
else
sendbody += ',"dynamic":"false"';
let fetch_method = null;
let fetch_url = null;
if (current_num !== null && current_num < current_list.length) {
fetch_method = "PATCH";
fetch_url = protocol + "://" + router + "/rest/ip/firewall/address-list/" + current_list[current_num]['.id'];
sendbody = '".id":"' + current_list[current_num]['.id'] + '",' + sendbody;
current_num++;
} else {
fetch_method = "POST";
fetch_url = protocol + "://" + router + "/rest/ip/firewall/address-list/add";
}
let check_address = await fetch(fetch_url, {
method: fetch_method,
headers: { "Accept": "application/json", "Content-Type": "application/json", "Authorization": authuser },
body: '{' + sendbody + '}'
}).then((response) => { return response.json(); }).then((answer) => {
return answer;
});
}
}
} else {
current_addrs = await fetch(protocol + "://" + router + "/rest/ip/firewall/address-list/print", {
method: "POST",
headers: { "Accept": "application/json", "Content-Type": "application/json", "Authorization": authuser },
body: '{".query": [' + domains_query + current_addr_query + '"disabled=false"]}'
}).then((response) => { return response.json(); }).then((data) => {
return data;
});
}
if (current_num !== null)
for (let i = current_num; i < current_list.length; i++) {
let check_address = await fetch(protocol + "://" + router + "/rest/ip/firewall/address-list/" + current_list[i]['.id'], {
method: "DELETE",
headers: { "Accept": "application/json", "Authorization": authuser }
}).then((response) => { return response; });
}
if (addrlist)
current_addrs = await fetch(protocol + "://" + router + "/rest/ip/firewall/address-list/print", {
method: "POST",
headers: { "Accept": "application/json", "Content-Type": "application/json", "Authorization": authuser },
body: '{".query": [' + domains_query + current_addr_query + '"disabled=false"]}'
}).then((response) => { return response.json(); }).then((data) => {
return data;
});
let current_conn_query = '';
for (let i = 0; i < current_addrs.length; i++) {
current_conn_query += '"dst-address=' + current_addrs[i]['address'] + ':443","dst-address=' + current_addrs[i]['address'] + ':80","#|",' + (current_conn_query ? '"#|",' : '');
}
let current_conn = await fetch(protocol + "://" + router + "/rest/ip/firewall/connection/print", {
method: "POST",
headers: { "Accept": "application/json", "Content-Type": "application/json", "Authorization": authuser },
body: '{".query": [' + current_conn_query + ']}'
}).then((response) => { return response.json(); }).then((data) => {
return data;
});
for (let i = 0; i < current_conn.length; i++) {
let check_address = await fetch(protocol + "://" + router + "/rest/ip/firewall/connection/" + current_conn[i]['.id'], {
method: "DELETE",
headers: { "Accept": "application/json", "Authorization": authuser }
}).then((response) => { return response; });
}
setTimeout(function() { chrome.tabs.reload(opened_tab.id); }, 1000);
}
function SaveSettings()
{
let localhost = document.getElementById("local-address").value;
let dynamic = document.getElementById("domain-dynamic").checked;
let dyntime = document.getElementById("domain-time").value;
let router = document.getElementById("router-address").value;
let username = document.getElementById("router-user").value;
let userpass = document.getElementById("router-password").value;
let protocol = document.getElementById("protocol-selection").value;
let top_domains_template = document.getElementById("top-domains").value;
let as_address = document.getElementById("domain-address").checked;
let add_www = document.getElementById("domain-www").checked;
let add_all = document.getElementById("domain-all").checked;
let save_settings = {
localhost: localhost,
router: router,
an: CodeString(username),
ap: CodeString(userpass),
protocol: protocol,
defaults: {
dynamic: dynamic,
time: dyntime,
www: top_domains_template,
what: (add_www ? "www" : (add_all ? "all" : "top")),
how: (as_address ? "address" : "name")
}
};
chrome.storage.local.set({"MikroTikControlPanelSettings": save_settings }).then(() => {
document.getElementById("settings-info").innerHTML = "Settings saved";
});
}
const delay = (delayInms) => {
return new Promise(resolve => setTimeout(resolve, delayInms));
};
function IpIsExclude(ip_str)
{
let ip = ParseIp(ip_str);
if (ip === null)
return false;
return IpInArray(ip, settings['exclude'], exclude_minprefix, exclude_maxprefix)
|| IpInArray(ip, settings['notrouted'], notrouted_minprefix, notrouted_maxprefix);
}
function IpInArray(ip, arr, prefmin, prefmax)
{
let submask = new Uint32Array([0xffffffff << (32 - prefmax)])[0];
let network = ip & submask;
for (let prefix = prefmax; prefix >= prefmin; prefix--) {
let subnet = ((network >>> 24) & 0xff).toString() + '.' + ((network >>> 16) & 0xff).toString() + '.' + ((network >>> 8) & 0xff).toString() + '.' + (network & 0xff).toString();
if (typeof arr[subnet] != 'undefined' && arr[subnet] <= prefix)
return true;
submask = submask << 1;
network = network & submask;
}
return false;
}
function ParseIp(ip_str, ip_format = 10)
{
let ip_octets = ip_str.split('.');
ip_octets[0] = parseInt(ip_octets[0], ip_format);
ip_octets[1] = parseInt(ip_octets[1], ip_format);
ip_octets[2] = parseInt(ip_octets[2], ip_format);
ip_octets[3] = parseInt(ip_octets[3], ip_format);
if (isNaN(ip_octets[0]) || isNaN(ip_octets[1]) || isNaN(ip_octets[2]) || isNaN(ip_octets[3])
|| ip_octets[0] > 255 || ip_octets[1] > 255 || ip_octets[2] > 255 || ip_octets[3] > 255
|| ip_octets[0] < 0 || ip_octets[1] < 0 || ip_octets[2] < 0 || ip_octets[3] < 0)
return null;
return new Uint32Array([((ip_octets[0] << 24) + (ip_octets[1] << 16) + (ip_octets[2] << 8) + ip_octets[3])])[0];
}
function ae(ai)
{
let f = "";
for (let c = 0; c < ai.length; c++)
f += String.fromCharCode(ai.charCodeAt(c) ^ (1 + (ai.length - c) % 32));
return f;
}
function StringToHex(str)
{
let hex = '';
for (let i = 0; i < str.length; i++)
hex += str.charCodeAt(i).toString(16).padStart(2, '0');
return hex;
}
function HexToString(hex)
{
let str = '';
for (let i = 0; i < hex.length; i += 2)
str += String.fromCharCode(parseInt(hex.substring(i, i + 2), 16));
return str;
}
function CodeString(str)
{
return StringToHex(ae(btoa(str)));
}
function DecodeString(str)
{
return atob(ae(HexToString(str)));
}
InitPage();
Полная версия в архиве: https://syo.su/download/MikroTikControlPanel.zip
Можно переписать плагин для работы с API других систем, будет время, сделаю версию для OPNsense и OpenWRT. Много чего удобного можно сделать в плагине с роутером – логи, мессенджер, самая разная автоматизация.