В Routeros система прав у юзеров меня не устраивает никак, поэтому в очередной раз столкнувшись с ситуацией, я вспомнил об API и о серваке, на котором можно будет решать среди других задач и эту. Приступаем…
Есть Миротик с «белым» IP-адресом. Другие способы (например через Cloud) приконнектиться через API не пробовал.
Есть сервер с Debian 9, на нём развёрнут вебсервер nginx + php-fpm +MariaDB с сайтиком, Zabbix’ом и тому подобной сопутствующей чепухой. В качестве подопытного самое то. Заходим на сервер в директорию с сайтом и создаём там директорию, где и будем ставить опыты.
Код: Выделить всё
cd /var/www/mysite.biz
mkdir mybutton
cd mybutton
Код: Выделить всё
touch index.php
nano index.php
Код: Выделить всё
<!doctype html>
<html>
<head>
<script src="https://code.jquery.com/jquery-3.3.1.js" integrity="sha256-2Kok7MbOyxpgUVvAk/HJ2jigOSYS2auK4Pfzbm7uH60=" crossorigin="anonymous"></script>
<link rel="stylesheet" href="css/style.css">
</head>
<!-- Скрипт с функциями. Каждая для определённой кнопки. В поле url ссылка на файл с исполняемым скриптом -->
<script>
function example1() {
$.ajax({
type: 'POST',
url: 'scripts/example1.php',
});
};
function example2() {
$.ajax({
type: 'POST',
url: 'scripts/example2.php',
});
};
function example3() {
$.ajax({
type: 'POST',
url: 'scripts/example3.php',
});
};
function example4() {
$.ajax({
type: 'POST',
url: 'scripts/example4.php',
});
};
function example5() {
$.ajax({
type: 'POST',
url: 'scripts/example5.php',
});
};
function example6() {
$.ajax({
type: 'POST',
url: 'scripts/example6.php',
});
};
</script>
<body>
<div class="wrap">
<button onclick="example1()"><span><i aria-hidden="true"></i>Первый скрипт</span></button>
<button onclick="example2()"><span><i aria-hidden="true"></i>Второй скрипт</span></button>
<button onclick="example3()"><span><i aria-hidden="true"></i>Третий скрипт</span></button>
<button onclick="example4()"><span><i aria-hidden="true"></i>Четвёртый скрипт</span></button>
<button onclick="example5()"><span><i aria-hidden="true"></i>Пятый скрипт</span></button>
<button onclick="example6()"><span><i aria-hidden="true"></i>Шестой скрипт</span></button>
</div>
</body>
</html>
Код: Выделить всё
mkdir css
mkdir scripts
Код: Выделить всё
cd css
touch style.css
nano style.css
Код: Выделить всё
button {
position: relative;
display: inline-block;
background: none;
outline: none;
border: none;
padding: 0;
cursor: pointer;
color: #800080;
text-transform: uppercase;
font-weight: 700;
letter-spacing: .05em;
}
button span {
position: relative;
z-index: 1;
display: block;
min-width: 1em;
padding: 1em;
border-radius: 2em;
background-color: #8FBC8F;
border: 1px solid #008080;
-webkit-box-shadow: -5px 5px 10px rgba(0, 0, 0, 0.2);
box-shadow: -5px 5px 10px rgba(0, 0, 0, 0.2);
-webkit-transition: background-color .2s, -webkit-transform .1s, -webkit-box-shadow .2s;
transition: background-color .2s, -webkit-transform .1s, -webkit-box-shadow .2s;
transition: transform .1s, box-shadow .2s, background-color .2s;
transition: transform .1s, box-shadow .2s, background-color .2s, -webkit-transform .1s, -webkit-box-shadow .2s;
}
button::after {
content: '';
position: absolute;
top: -1em;
bottom: -1em;
left: -1em;
right: -1em;
border-radius: 4em;
-webkit-box-shadow: 0 0 0px rgba(0, 0, 0, 0.2), inset 0 0 0px rgba(0, 0, 0, 0.2);
box-shadow: 0 0 0px rgba(0, 0, 0, 0.2), inset 0 0 0px rgba(0, 0, 0, 0.2);
-webkit-transition: -webkit-box-shadow .1s;
transition: -webkit-box-shadow .1s;
transition: box-shadow .1s;
transition: box-shadow .1s, -webkit-box-shadow .1s;
}
button:hover span, button:focus span {
background-color: #4682B4;
}
button:active span {
background-color: #8FBC8F;
-webkit-transform: scale(0.97);
transform: scale(0.97);
-webkit-box-shadow: -2px 2px 5px rgba(0, 0, 0, 0.2);
box-shadow: -2px 2px 5px rgba(0, 0, 0, 0.2);
}
button:active::after {
-webkit-box-shadow: 0 0 2px rgba(0, 0, 0, 0.2), inset -3px 3px 1em rgba(0, 0, 0, 0.2);
box-shadow: 0 0 2px rgba(0, 0, 0, 0.2), inset -3px 3px 1em rgba(0, 0, 0, 0.2);
}
body {
background-color: #8FBC8F;
}
.wrap {
width: 100%;
text-align: center;
position: absolute;
top: 50%;
left: 50%;
-webkit-transform: translate(-50%, -50%);
transform: translate(-50%, -50%);
}
.wrap button {
margin: 1em;
}
Теперь переходим в директорию скриптов и первым делом создаём файл библиотеки api , без которой нам никак
Код: Выделить всё
cd ../scripts
nano routeros_api.class.php
Код: Выделить всё
<?php
/*****************************
*
* RouterOS PHP API class v1.6
* Author: Denis Basta
* Contributors:
* Nick Barnes
* Ben Menking (ben [at] infotechsc [dot] com)
* Jeremy Jefferson (http://jeremyj.com)
* Cristian Deluxe (djcristiandeluxe [at] gmail [dot] com)
* Mikhail Moskalev (mmv.rus [at] gmail [dot] com)
*
* http://www.mikrotik.com
* http://wiki.mikrotik.com/wiki/API_PHP_class
*
******************************/
class RouterosAPI
{
var $debug = false; // Show debug information
var $connected = false; // Connection state
var $port = 8728; // Port to connect to (default 8729 for ssl)
var $ssl = false; // Connect using SSL (must enable api-ssl in IP/Services)
var $timeout = 3; // Connection attempt timeout and data read timeout
var $attempts = 5; // Connection attempt count
var $delay = 3; // Delay between connection attempts in seconds
var $socket; // Variable for storing socket resource
var $error_no; // Variable for storing connection error number, if any
var $error_str; // Variable for storing connection error text, if any
/* Check, can be var used in foreach */
public function isIterable($var)
{
return $var !== null
&& (is_array($var)
|| $var instanceof Traversable
|| $var instanceof Iterator
|| $var instanceof IteratorAggregate
);
}
/**
* Print text for debug purposes
*
* @param string $text Text to print
*
* @return void
*/
public function debug($text)
{
if ($this->debug) {
echo $text . "\n";
}
}
/**
*
*
* @param string $length
*
* @return void
*/
public function encodeLength($length)
{
if ($length < 0x80) {
$length = chr($length);
} elseif ($length < 0x4000) {
$length |= 0x8000;
$length = chr(($length >> 8) & 0xFF) . chr($length & 0xFF);
} elseif ($length < 0x200000) {
$length |= 0xC00000;
$length = chr(($length >> 16) & 0xFF) . chr(($length >> 8) & 0xFF) . chr($length & 0xFF);
} elseif ($length < 0x10000000) {
$length |= 0xE0000000;
$length = chr(($length >> 24) & 0xFF) . chr(($length >> 16) & 0xFF) . chr(($length >> 8) & 0xFF) . chr($length & 0xFF);
} elseif ($length >= 0x10000000) {
$length = chr(0xF0) . chr(($length >> 24) & 0xFF) . chr(($length >> 16) & 0xFF) . chr(($length >> 8) & 0xFF) . chr($length & 0xFF);
}
return $length;
}
/**
* Login to RouterOS
*
* @param string $ip Hostname (IP or domain) of the RouterOS server
* @param string $login The RouterOS username
* @param string $password The RouterOS password
*
* @return boolean If we are connected or not
*/
public function connect($ip, $login, $password)
{
for ($ATTEMPT = 1; $ATTEMPT <= $this->attempts; $ATTEMPT++) {
$this->connected = false;
$PROTOCOL = ($this->ssl ? 'ssl://' : '' );
$context = stream_context_create(array('ssl' => array('ciphers' => 'ADH:ALL', 'verify_peer' => false, 'verify_peer_name' => false)));
$this->debug('Connection attempt #' . $ATTEMPT . ' to ' . $PROTOCOL . $ip . ':' . $this->port . '...');
$this->socket = @stream_socket_client($PROTOCOL . $ip.':'. $this->port, $this->error_no, $this->error_str, $this->timeout, STREAM_CLIENT_CONNECT,$context);
if ($this->socket) {
socket_set_timeout($this->socket, $this->timeout);
$this->write('/login', false);
$this->write('=name=' . $login, false);
$this->write('=password=' . $password);
$RESPONSE = $this->read(false);
if (isset($RESPONSE[0])) {
if ($RESPONSE[0] == '!done') {
if (!isset($RESPONSE[1])) {
// Login method post-v6.43
$this->connected = true;
break;
} else {
// Login method pre-v6.43
$MATCHES = array();
if (preg_match_all('/[^=]+/i', $RESPONSE[1], $MATCHES)) {
if ($MATCHES[0][0] == 'ret' && strlen($MATCHES[0][1]) == 32) {
$this->write('/login', false);
$this->write('=name=' . $login, false);
$this->write('=response=00' . md5(chr(0) . $password . pack('H*', $MATCHES[0][1])));
$RESPONSE = $this->read(false);
if (isset($RESPONSE[0]) && $RESPONSE[0] == '!done') {
$this->connected = true;
break;
}
}
}
}
}
}
fclose($this->socket);
}
sleep($this->delay);
}
if ($this->connected) {
$this->debug('Connected...');
} else {
$this->debug('Error...');
}
return $this->connected;
}
/**
* Disconnect from RouterOS
*
* @return void
*/
public function disconnect()
{
// let's make sure this socket is still valid. it may have been closed by something else
if( is_resource($this->socket) ) {
fclose($this->socket);
}
$this->connected = false;
$this->debug('Disconnected...');
}
/**
* Parse response from Router OS
*
* @param array $response Response data
*
* @return array Array with parsed data
*/
public function parseResponse($response)
{
if (is_array($response)) {
$PARSED = array();
$CURRENT = null;
$singlevalue = null;
foreach ($response as $x) {
if (in_array($x, array('!fatal','!re','!trap'))) {
if ($x == '!re') {
$CURRENT =& $PARSED[];
} else {
$CURRENT =& $PARSED[$x][];
}
} elseif ($x != '!done') {
$MATCHES = array();
if (preg_match_all('/[^=]+/i', $x, $MATCHES)) {
if ($MATCHES[0][0] == 'ret') {
$singlevalue = $MATCHES[0][1];
}
$CURRENT[$MATCHES[0][0]] = (isset($MATCHES[0][1]) ? $MATCHES[0][1] : '');
}
}
}
if (empty($PARSED) && !is_null($singlevalue)) {
$PARSED = $singlevalue;
}
return $PARSED;
} else {
return array();
}
}
/**
* Parse response from Router OS
*
* @param array $response Response data
*
* @return array Array with parsed data
*/
public function parseResponse4Smarty($response)
{
if (is_array($response)) {
$PARSED = array();
$CURRENT = null;
$singlevalue = null;
foreach ($response as $x) {
if (in_array($x, array('!fatal','!re','!trap'))) {
if ($x == '!re') {
$CURRENT =& $PARSED[];
} else {
$CURRENT =& $PARSED[$x][];
}
} elseif ($x != '!done') {
$MATCHES = array();
if (preg_match_all('/[^=]+/i', $x, $MATCHES)) {
if ($MATCHES[0][0] == 'ret') {
$singlevalue = $MATCHES[0][1];
}
$CURRENT[$MATCHES[0][0]] = (isset($MATCHES[0][1]) ? $MATCHES[0][1] : '');
}
}
}
foreach ($PARSED as $key => $value) {
$PARSED[$key] = $this->arrayChangeKeyName($value);
}
return $PARSED;
if (empty($PARSED) && !is_null($singlevalue)) {
$PARSED = $singlevalue;
}
} else {
return array();
}
}
/**
* Change "-" and "/" from array key to "_"
*
* @param array $array Input array
*
* @return array Array with changed key names
*/
public function arrayChangeKeyName(&$array)
{
if (is_array($array)) {
foreach ($array as $k => $v) {
$tmp = str_replace("-", "_", $k);
$tmp = str_replace("/", "_", $tmp);
if ($tmp) {
$array_new[$tmp] = $v;
} else {
$array_new[$k] = $v;
}
}
return $array_new;
} else {
return $array;
}
}
/**
* Read data from Router OS
*
* @param boolean $parse Parse the data? default: true
*
* @return array Array with parsed or unparsed data
*/
public function read($parse = true)
{
$RESPONSE = array();
$receiveddone = false;
while (true) {
// Read the first byte of input which gives us some or all of the length
// of the remaining reply.
$BYTE = ord(fread($this->socket, 1));
$LENGTH = 0;
// If the first bit is set then we need to remove the first four bits, shift left 8
// and then read another byte in.
// We repeat this for the second and third bits.
// If the fourth bit is set, we need to remove anything left in the first byte
// and then read in yet another byte.
if ($BYTE & 128) {
if (($BYTE & 192) == 128) {
$LENGTH = (($BYTE & 63) << 8) + ord(fread($this->socket, 1));
} else {
if (($BYTE & 224) == 192) {
$LENGTH = (($BYTE & 31) << 8) + ord(fread($this->socket, 1));
$LENGTH = ($LENGTH << 8) + ord(fread($this->socket, 1));
} else {
if (($BYTE & 240) == 224) {
$LENGTH = (($BYTE & 15) << 8) + ord(fread($this->socket, 1));
$LENGTH = ($LENGTH << 8) + ord(fread($this->socket, 1));
$LENGTH = ($LENGTH << 8) + ord(fread($this->socket, 1));
} else {
$LENGTH = ord(fread($this->socket, 1));
$LENGTH = ($LENGTH << 8) + ord(fread($this->socket, 1));
$LENGTH = ($LENGTH << 8) + ord(fread($this->socket, 1));
$LENGTH = ($LENGTH << 8) + ord(fread($this->socket, 1));
}
}
}
} else {
$LENGTH = $BYTE;
}
$_ = "";
// If we have got more characters to read, read them in.
if ($LENGTH > 0) {
$_ = "";
$retlen = 0;
while ($retlen < $LENGTH) {
$toread = $LENGTH - $retlen;
$_ .= fread($this->socket, $toread);
$retlen = strlen($_);
}
$RESPONSE[] = $_;
$this->debug('>>> [' . $retlen . '/' . $LENGTH . '] bytes read.');
}
// If we get a !done, make a note of it.
if ($_ == "!done") {
$receiveddone = true;
}
$STATUS = socket_get_status($this->socket);
if ($LENGTH > 0) {
$this->debug('>>> [' . $LENGTH . ', ' . $STATUS['unread_bytes'] . ']' . $_);
}
if ((!$this->connected && !$STATUS['unread_bytes']) || ($this->connected && !$STATUS['unread_bytes'] && $receiveddone)) {
break;
}
}
if ($parse) {
$RESPONSE = $this->parseResponse($RESPONSE);
}
return $RESPONSE;
}
/**
* Write (send) data to Router OS
*
* @param string $command A string with the command to send
* @param mixed $param2 If we set an integer, the command will send this data as a "tag"
* If we set it to boolean true, the funcion will send the comand and finish
* If we set it to boolean false, the funcion will send the comand and wait for next command
* Default: true
*
* @return boolean Return false if no command especified
*/
public function write($command, $param2 = true)
{
if ($command) {
$data = explode("\n", $command);
foreach ($data as $com) {
$com = trim($com);
fwrite($this->socket, $this->encodeLength(strlen($com)) . $com);
$this->debug('<<< [' . strlen($com) . '] ' . $com);
}
if (gettype($param2) == 'integer') {
fwrite($this->socket, $this->encodeLength(strlen('.tag=' . $param2)) . '.tag=' . $param2 . chr(0));
$this->debug('<<< [' . strlen('.tag=' . $param2) . '] .tag=' . $param2);
} elseif (gettype($param2) == 'boolean') {
fwrite($this->socket, ($param2 ? chr(0) : ''));
}
return true;
} else {
return false;
}
}
/**
* Write (send) data to Router OS
*
* @param string $com A string with the command to send
* @param array $arr An array with arguments or queries
*
* @return array Array with parsed
*/
public function comm($com, $arr = array())
{
$count = count($arr);
$this->write($com, !$arr);
$i = 0;
if ($this->isIterable($arr)) {
foreach ($arr as $k => $v) {
switch ($k[0]) {
case "?":
$el = "$k=$v";
break;
case "~":
$el = "$k~$v";
break;
default:
$el = "=$k=$v";
break;
}
$last = ($i++ == $count - 1);
$this->write($el, $last);
}
}
return $this->read();
}
/**
* Standard destructor
*
* @return void
*/
public function __destruct()
{
$this->disconnect();
}
}
Теперь создадим файл example1.php , который станет своеобразным шаблоном для наших файлов-скриптов
Код: Выделить всё
touch example1.php
nano example1.php
Код: Выделить всё
<?php
require_once('routeros_api.class.php');
$API = new RouterosAPI();
$API->debug = false;
if ($API->connect('Your_IP', 'api_login', 'api_password')) {
$API->write('/system/script/getall', false);
$API->write('=.proplist=.id',false);
$API->write('?name=Script name in Mikrotik');
$READ = $API->read(false);
$ARRAY = $API->parseResponse($READ);
if(isset($ARRAY['0']) && isset($ARRAY['0']['.id'])){
$API->write('/system/script/run',false);
$API->write('=.id='. $ARRAY['0']['.id']);
$READ = $API->read(false);
}
$API->disconnect();
}
?>
Код: Выделить всё
if ($API->connect('Your_IP', 'api_login', 'api_password')) {
Код: Выделить всё
$API->write('?name=Script name in Mikrotik');
На сервере пока всё. Идём на Микротик. Создаём нового пользователя в группе full (потом экспериментально можно подобрать нужный набор разрешений)
Код: Выделить всё
/user add name=testapi group=full password=apitestapi
Код: Выделить всё
/ip service set api disabled=no address=IP_Mikrotik port=8728
Код: Выделить всё
/system script add name="Script name in Mikrotik" owner=testapi source=":log info \"Script run!!!\""
Искренне надеюсь, что у вас получится с первого раза.

Теперь самое время заняться безопасностью. Забрать лишние права у пользователя, переведя его в отдельную группу. Работоспособность проверяйте пошагово. Страницу сайта надо бы закрыть паролем, если кому-то очень надо, у меня есть простейший вариант, я покажу.
На этом всё. Всем спокойствия и удачи в настройках. Ваш podarok66.
P. S.: Посмотрел нужную группу прав, вышло немного непонятно
Код: Выделить всё
add name=api policy=\
ftp,reboot,read,write,policy,test,password,sniff,sensitive,api,romon,!local,!telnet,!ssh,!winbox,!web,!dude,!tikapp
Ну и самое главное. Не всё опубликованное авторский код. Часть честно взята из свободного доступа в сети и перелопачена под себя. Выражаю бесконечную признательность illinory за помощь с ajax, библиотекой и вообще за интеллектуальную и моральную поддержку. Без его багажа знаний всё было бы неизмеримо сложнее.