diff --git a/.github/copilot-instructions.md b/.github/copilot-instructions.md new file mode 100644 index 0000000..54a69ad --- /dev/null +++ b/.github/copilot-instructions.md @@ -0,0 +1,23 @@ +# Cacti mikrotik Plugin AI Instructions + +## Project Overview +This is a Cacti plugin. It integrates with the Cacti monitoring platform via the plugin hook architecture. + +## Technology Stack +- PHP 7.4+ (targeting Cacti 1.2.x compatibility) +- MySQL/MariaDB via Cacti's DB abstraction layer +- PSR-12 coding standards + +## Key Rules +- Use prepared statements (db_execute_prepared, db_fetch_row_prepared, etc.) for ALL queries with variables +- Use get_request_var() / get_filter_request_var() for ALL user input, never raw $_REQUEST/$_GET/$_POST +- Use html_escape() / htmlspecialchars() for ALL output of DB/user values in HTML context +- Use cacti_escapeshellarg() for ALL shell command arguments +- No PHP 8.0+ features (str_contains, match, union types, named args) - target PHP 7.4 +- Use ?? and ??= operators (PHP 7.4) instead of isset() ternary patterns +- All unserialize() calls must use allowed_classes => false + +## Testing +- Tests in tests/ directory +- Use Pest PHP or PHPUnit +- php -l lint check required before commit diff --git a/.github/workflows/plugin-ci-workflow.yml b/.github/workflows/plugin-ci-workflow.yml new file mode 100644 index 0000000..1fb3b67 --- /dev/null +++ b/.github/workflows/plugin-ci-workflow.yml @@ -0,0 +1,225 @@ +# +-------------------------------------------------------------------------+ +# | Copyright (C) 2004-2026 The Cacti Group | +# | | +# | 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 2 | +# | 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. | +# +-------------------------------------------------------------------------+ +# | Cacti: The Complete RRDtool-based Graphing Solution | +# +-------------------------------------------------------------------------+ +# | This code is designed, written, and maintained by the Cacti Group. See | +# | about.php and/or the AUTHORS file for specific developer information. | +# +-------------------------------------------------------------------------+ +# | http://www.cacti.net/ | +# +-------------------------------------------------------------------------+ + +name: Plugin Integration Tests + +on: + push: + branches: + - main + - develop + pull_request: + branches: + - main + - develop + +jobs: + integration-test: + runs-on: ${{ matrix.os }} + + strategy: + fail-fast: false + matrix: + php: ['8.1', '8.2', '8.3', '8.4'] + os: [ubuntu-latest] + + services: + mariadb: + image: mariadb:10.6 + env: + MYSQL_ROOT_PASSWORD: cactiroot + MYSQL_DATABASE: cacti + MYSQL_USER: cactiuser + MYSQL_PASSWORD: cactiuser + ports: + - 3306:3306 + options: >- + --health-cmd="mysqladmin ping" + --health-interval=10s + --health-timeout=5s + --health-retries=3 + + name: PHP ${{ matrix.php }} Integration Test on ${{ matrix.os }} + + steps: + - name: Checkout Cacti + uses: actions/checkout@v4 + with: + repository: Cacti/cacti + path: cacti + + - name: Checkout mikrotik Plugin + uses: actions/checkout@v4 + with: + path: cacti/plugins/mikrotik + + - name: Install PHP ${{ matrix.php }} + uses: shivammathur/setup-php@v2 + with: + php-version: ${{ matrix.php }} + extensions: intl, mysql, gd, ldap, gmp, xml, curl, json, mbstring + ini-values: "post_max_size=256M, max_execution_time=60, date.timezone=America/New_York" + + - name: Check PHP version + run: php -v + + - name: Run apt-get update + run: sudo apt-get update + + - name: Install System Dependencies + run: sudo apt-get install -y apache2 snmp snmpd rrdtool fping + + - name: Start SNMPD Agent and Test + run: | + sudo systemctl start snmpd + sudo snmpwalk -c public -v2c -On localhost .1.3.6.1.2.1.1 + + - name: Setup Permissions + run: | + sudo chown -R www-data:runner ${{ github.workspace }}/cacti + sudo find ${{ github.workspace }}/cacti -type d -exec chmod 775 {} \; + sudo find ${{ github.workspace }}/cacti -type f -exec chmod 664 {} \; + sudo chmod +x ${{ github.workspace }}/cacti/cmd.php + sudo chmod +x ${{ github.workspace }}/cacti/poller.php + + - name: Create MySQL Config + run: | + echo -e "[client]\nuser = root\npassword = cactiroot\nhost = 127.0.0.1\n" > ~/.my.cnf + cat ~/.my.cnf + + - name: Initialize Cacti Database + env: + MYSQL_AUTH_USR: '--defaults-file=~/.my.cnf' + run: | + mysql $MYSQL_AUTH_USR -e 'CREATE DATABASE IF NOT EXISTS cacti;' + mysql $MYSQL_AUTH_USR -e "CREATE USER IF NOT EXISTS 'cactiuser'@'localhost' IDENTIFIED BY 'cactiuser';" + mysql $MYSQL_AUTH_USR -e "GRANT ALL PRIVILEGES ON cacti.* TO 'cactiuser'@'localhost';" + mysql $MYSQL_AUTH_USR -e "GRANT SELECT ON mysql.time_zone_name TO 'cactiuser'@'localhost';" + mysql $MYSQL_AUTH_USR -e "FLUSH PRIVILEGES;" + mysql $MYSQL_AUTH_USR cacti < ${{ github.workspace }}/cacti/cacti.sql + mysql $MYSQL_AUTH_USR -e "INSERT INTO settings (name, value) VALUES ('path_php_binary', '/usr/bin/php')" cacti + + - name: Validate composer files + run: | + cd ${{ github.workspace }}/cacti + if [ -f composer.json ]; then + composer validate --strict || true + fi + + - name: Install Composer Dependencies + run: | + cd ${{ github.workspace }}/cacti + if [ -f composer.json ]; then + sudo composer install --prefer-dist --no-progress + fi + + - name: Create Cacti config.php + run: | + cat ${{ github.workspace }}/cacti/include/config.php.dist | \ + sed -r "s/localhost/127.0.0.1/g" | \ + sed -r "s/'cacti'/'cacti'/g" | \ + sed -r "s/'cactiuser'/'cactiuser'/g" | \ + sed -r "s/'cactiuser'/'cactiuser'/g" > ${{ github.workspace }}/cacti/include/config.php + sudo chmod 664 ${{ github.workspace }}/cacti/include/config.php + + - name: Configure Apache + run: | + cat << 'EOF' | sed 's#GITHUB_WORKSPACE#${{ github.workspace }}#g' > /tmp/cacti.conf + + ServerAdmin webmaster@localhost + DocumentRoot GITHUB_WORKSPACE/cacti + + + Options Indexes FollowSymLinks + AllowOverride All + Require all granted + + + ErrorLog ${APACHE_LOG_DIR}/error.log + CustomLog ${APACHE_LOG_DIR}/access.log combined + + EOF + sudo cp /tmp/cacti.conf /etc/apache2/sites-available/000-default.conf + sudo systemctl restart apache2 + + - name: Install Cacti via CLI + run: | + cd ${{ github.workspace }}/cacti + sudo php cli/install_cacti.php --accept-eula --install --force + + - name: Install mikrotik Plugin + run: | + cd ${{ github.workspace }}/cacti + sudo php cli/plugin_manage.php --plugin=mikrotik --install --enable + +# - name: import mikrotik Plugin Sample Data +# run: | +# cd ${{ github.workspace }}/cacti/plugins/mikrotik +# sudo php cli_import.php --filename=.github/workflows/mikrotik_sample_data.xml +# if [ $? -ne 0 ]; then +# echo "Failed to import Thold sample data" +# exit 1 +# fi + + - name: Check PHP Syntax for Plugin + run: | + cd ${{ github.workspace }}/cacti/plugins/mikrotik + if find . -name '*.php' -exec php -l {} 2>&1 \; | grep -iv 'no syntax errors detected'; then + echo "Syntax errors found!" + exit 1 + fi + + - name: Remove the plugins directory exclusion from the .phpstan.neon + run: sed '/plugins/d' -i .phpstan.neon + working-directory: ${{ github.workspace }}/cacti + + - name: Mark composer scripts executable + run: sudo chmod +x ${{ github.workspace }}/cacti/include/vendor/bin/* + + - name: Run Linter on base code + run: composer run-script lint ${{ github.workspace }}/cacti/plugins/mikrotik + working-directory: ${{ github.workspace }}/cacti + + - name: Checking coding standards on base code + run: composer run-script phpcsfixer ${{ github.workspace }}/cacti/plugins/mikrotik + working-directory: ${{ github.workspace }}/cacti + +# - name: Run PHPStan at Level 6 on base code outside of Composer due to technical issues +# run: ./include/vendor/bin/phpstan analyze --level 6 ${{ github.workspace }}/cacti/plugins/mikrotik +# working-directory: ${{ github.workspace }}/cacti + + - name: Run Cacti Poller + run: | + cd ${{ github.workspace }}/cacti + sudo php poller.php --poller=1 --force --debug + if ! grep -q "SYSTEM STATS" log/cacti.log; then + echo "Cacti poller did not finish successfully" + cat log/cacti.log + exit 1 + fi + + - name: View Cacti Logs + if: always() + run: | + if [ -f ${{ github.workspace }}/cacti/log/cacti.log ]; then + echo "=== Cacti Log ===" + sudo cat ${{ github.workspace }}/cacti/log/cacti.log + fi diff --git a/RouterOS/examples/example1.php b/RouterOS/examples/example1.php index 6ec2bd0..2cf86be 100644 --- a/RouterOS/examples/example1.php +++ b/RouterOS/examples/example1.php @@ -7,15 +7,12 @@ $API->debug = true; if ($API->connect('111.111.111.111', 'LOGIN', 'PASSWORD')) { + $API->write('/interface/getall'); - $API->write('/interface/getall'); + $READ = $API->read(false); + $ARRAY = $API->parseResponse($READ); - $READ = $API->read(false); - $ARRAY = $API->parseResponse($READ); - - print_r($ARRAY); - - $API->disconnect(); + print_r($ARRAY); + $API->disconnect(); } - diff --git a/RouterOS/examples/example2.php b/RouterOS/examples/example2.php index 1e204d2..4aa3f31 100644 --- a/RouterOS/examples/example2.php +++ b/RouterOS/examples/example2.php @@ -7,16 +7,13 @@ $API->debug = true; if ($API->connect('111.111.111.111', 'LOGIN', 'PASSWORD')) { + $API->write('/interface/wireless/registration-table/print',false); + $API->write('=stats='); - $API->write('/interface/wireless/registration-table/print',false); - $API->write('=stats='); + $READ = $API->read(false); + $ARRAY = $API->parseResponse($READ); - $READ = $API->read(false); - $ARRAY = $API->parseResponse($READ); - - print_r($ARRAY); - - $API->disconnect(); + print_r($ARRAY); + $API->disconnect(); } - diff --git a/RouterOS/examples/example3.php b/RouterOS/examples/example3.php index b4e911e..aefa03a 100644 --- a/RouterOS/examples/example3.php +++ b/RouterOS/examples/example3.php @@ -1,6 +1,6 @@ debug = true; if ($API->connect('111.111.111.111', 'LOGIN', 'PASSWORD')) { - - $API->comm('/ppp/secret/add', array( - 'name' => 'user', - 'password' => 'pass', - 'remote-address' => '172.16.1.10', - 'comment' => '{new VPN user}', - 'service' => 'pptp', - )); - - $API->disconnect(); - + $API->comm('/ppp/secret/add', [ + 'name' => 'user', + 'password' => 'pass', + 'remote-address' => '172.16.1.10', + 'comment' => '{new VPN user}', + 'service' => 'pptp', + ]); + + $API->disconnect(); } - diff --git a/RouterOS/examples/example4.php b/RouterOS/examples/example4.php index 05c1f07..88f6e46 100644 --- a/RouterOS/examples/example4.php +++ b/RouterOS/examples/example4.php @@ -1,6 +1,6 @@ debug = true; if ($API->connect('111.111.111.111', 'LOGIN', 'PASSWORD')) { + $ARRAY = $API->comm('/interface/wireless/registration-table/print', [ + '.proplist' => '.id', + '?mac-address' => '00:0E:BB:DD:FF:FF', + ]); - $ARRAY = $API->comm("/interface/wireless/registration-table/print", array( - ".proplist"=> ".id", - "?mac-address" => "00:0E:BB:DD:FF:FF", - )); - - print_r($ARRAY); - - $API->disconnect(); + print_r($ARRAY); + $API->disconnect(); } - diff --git a/RouterOS/examples/example5.php b/RouterOS/examples/example5.php index 70a12ec..0105ef3 100644 --- a/RouterOS/examples/example5.php +++ b/RouterOS/examples/example5.php @@ -1,6 +1,6 @@ debug = true; if ($API->connect('111.111.111.111', 'LOGIN', 'PASSWORD')) { + $ARRAY = $API->comm('/ip/dhcp-server/lease/print', [ + 'count-only' => '', + '~active-address' => '1.1.', + ]); - $ARRAY = $API->comm("/ip/dhcp-server/lease/print", array( - "count-only"=> "", - "~active-address" => "1.1.", - )); - - print_r($ARRAY); - - $API->disconnect(); + print_r($ARRAY); + $API->disconnect(); } - diff --git a/RouterOS/examples/example6.php b/RouterOS/examples/example6.php index a106f30..c608ef9 100644 --- a/RouterOS/examples/example6.php +++ b/RouterOS/examples/example6.php @@ -10,26 +10,26 @@ $API = new RouterosAPI(); if ($API->connect('111.111.111.111', 'LOGIN', 'PASSWORD')) { - # Get all current hosts - $API->write('/ip/dns/static/print'); - $ips = $API->read(); + // Get all current hosts + $API->write('/ip/dns/static/print'); + $ips = $API->read(); - # delete them all ! - foreach($ips as $num => $ip_data) { - $API->write('/ip/dns/static/remove', false); - $API->write('=.id=' . $ip_data['.id'], true); - } + // delete them all ! + foreach ($ips as $num => $ip_data) { + $API->write('/ip/dns/static/remove', false); + $API->write('=.id=' . $ip_data['.id'], true); + } - #add some new - $API->comm('/ip/dns/static/add', array( - 'name' => 'jefkeklak', - 'address' => '1.2.3.4', - 'ttl' => '1m' - )); + // add some new + $API->comm('/ip/dns/static/add', [ + 'name' => 'jefkeklak', + 'address' => '1.2.3.4', + 'ttl' => '1m' + ]); - #show me what you got - $API->write('/ip/dns/static/print'); - $ips = $API->read(); - var_dump($ips); - $API->disconnect(); + // show me what you got + $API->write('/ip/dns/static/print'); + $ips = $API->read(); + var_dump($ips); + $API->disconnect(); } diff --git a/RouterOS/routeros_api.class.php b/RouterOS/routeros_api.class.php index 28e242d..2707add 100644 --- a/RouterOS/routeros_api.class.php +++ b/RouterOS/routeros_api.class.php @@ -1,5 +1,5 @@ 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://' : '' ); - $this->debug('Connection attempt #' . $ATTEMPT . ' to ' . $PROTOCOL . $ip . ':' . $this->port . '...'); - $this->socket = @fsockopen($PROTOCOL . $ip, $this->port, $this->error_no, $this->error_str, $this->timeout); - if ($this->socket) { - socket_set_timeout($this->socket, $this->timeout); - - // Try new post-v6.43 API first - $this->write('/login', false); - $this->write('=name=' . $login, false); - $this->write('=password=' . $password); - $RESPONSE = $this->read(false); - if (isset($RESPONSE[0]) && $RESPONSE[0] == '!done') { - $this->connected = true; - break; - } - - // Try legacy pre-v6.43 API next - $this->write('/login'); - $RESPONSE = $this->read(false); - if (isset($RESPONSE[0]) && $RESPONSE[0] == '!done') { - $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(); - } + 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) { + print $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://' : ''); + $this->debug('Connection attempt #' . $ATTEMPT . ' to ' . $PROTOCOL . $ip . ':' . $this->port . '...'); + $this->socket = @fsockopen($PROTOCOL . $ip, $this->port, $this->error_no, $this->error_str, $this->timeout); + + if ($this->socket) { + socket_set_timeout($this->socket, $this->timeout); + + // Try new post-v6.43 API first + $this->write('/login', false); + $this->write('=name=' . $login, false); + $this->write('=password=' . $password); + $RESPONSE = $this->read(false); + + if (isset($RESPONSE[0]) && $RESPONSE[0] == '!done') { + $this->connected = true; + + break; + } + + // Try legacy pre-v6.43 API next + $this->write('/login'); + $RESPONSE = $this->read(false); + + if (isset($RESPONSE[0]) && $RESPONSE[0] == '!done') { + $MATCHES = []; + + 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 = []; + $CURRENT = null; + $singlevalue = null; + + foreach ($response as $x) { + if (in_array($x, ['!fatal', '!re', '!trap'], true)) { + if ($x == '!re') { + $CURRENT = & $PARSED[]; + } else { + $CURRENT = & $PARSED[$x][]; + } + } elseif ($x != '!done') { + $MATCHES = []; + + if (preg_match_all('/[^=]+/i', $x, $MATCHES)) { + if ($MATCHES[0][0] == 'ret') { + $singlevalue = $MATCHES[0][1]; + } + $CURRENT[$MATCHES[0][0]] = ($MATCHES[0][1] ?? ''); + } + } + } + + if (empty($PARSED) && !is_null($singlevalue)) { + $PARSED = $singlevalue; + } + + return $PARSED; + } else { + return []; + } + } + + /** + * 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 = []; + $CURRENT = null; + $singlevalue = null; + + foreach ($response as $x) { + if (in_array($x, ['!fatal', '!re', '!trap'], true)) { + if ($x == '!re') { + $CURRENT = & $PARSED[]; + } else { + $CURRENT = & $PARSED[$x][]; + } + } elseif ($x != '!done') { + $MATCHES = []; + + if (preg_match_all('/[^=]+/i', $x, $MATCHES)) { + if ($MATCHES[0][0] == 'ret') { + $singlevalue = $MATCHES[0][1]; + } + $CURRENT[$MATCHES[0][0]] = ($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 []; + } + } + + /** + * 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 = []; + $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 = []) { + $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(); + } } - diff --git a/locales/index.php b/locales/index.php index 5c8c08f..d503d49 100644 --- a/locales/index.php +++ b/locales/index.php @@ -1,3 +1,2 @@ __('Present', 'mikrotik'), 1 => __('Unknown', 'mikrotik'), 2 => __('Running', 'mikrotik'), 3 => __('Warning', 'mikrotik'), 4 => __('Testing', 'mikrotik'), 5 => __('Down', 'mikrotik') -); +]; switch(get_request_var('action')) { -case 'devices': - mikrotik_devices(); - break; -case 'trees': - mikrotik_trees(); - break; -case 'queues': - mikrotik_queues(); - break; -case 'interfaces': - mikrotik_interfaces(); - break; -case 'dhcp': - mikrotik_dhcp(); - break; -case 'dns': - mikrotik_dns(false); - break; -case 'dnsexport': - mikrotik_dns(true); - break; -case 'list': - mikrotik_list(false); - break; -case 'listexport': - mikrotik_list(true); - break; -case 'wireless_aps': - mikrotik_wireless_aps(); - break; -case 'wireless_regs': - mikrotik_wireless_regs(); - break; -case 'users': - mikrotik_users(); - break; -case 'graphs': - mikrotik_view_graphs(); - break; + case 'devices': + mikrotik_devices(); + + break; + case 'trees': + mikrotik_trees(); + + break; + case 'queues': + mikrotik_queues(); + + break; + case 'interfaces': + mikrotik_interfaces(); + + break; + case 'dhcp': + mikrotik_dhcp(); + + break; + case 'dns': + mikrotik_dns(false); + + break; + case 'dnsexport': + mikrotik_dns(true); + + break; + case 'list': + mikrotik_list(false); + + break; + case 'listexport': + mikrotik_list(true); + + break; + case 'wireless_aps': + mikrotik_wireless_aps(); + + break; + case 'wireless_regs': + mikrotik_wireless_regs(); + + break; + case 'users': + mikrotik_users(); + + break; + case 'graphs': + mikrotik_view_graphs(); + + break; } if (strpos(get_request_var('action'), 'export') === false) { @@ -95,8 +108,9 @@ function mikrotik_get_network($mask) { $octets = explode('.', $mask); $output = ''; + if (cacti_sizeof($octets)) { - foreach($octets as $octet) { + foreach ($octets as $octet) { $output .= decbin($octet); } @@ -107,45 +121,45 @@ function mikrotik_get_network($mask) { } function mikrotik_users_exist() { - return db_fetch_cell("SELECT COUNT(*) FROM plugin_mikrotik_users"); + return db_fetch_cell('SELECT COUNT(*) FROM plugin_mikrotik_users'); } function mikrotik_queues_exist() { - return db_fetch_cell("SELECT COUNT(*) FROM plugin_mikrotik_queues"); + return db_fetch_cell('SELECT COUNT(*) FROM plugin_mikrotik_queues'); } function mikrotik_queue_trees_exist() { - return db_fetch_cell("SELECT COUNT(*) FROM plugin_mikrotik_trees"); + return db_fetch_cell('SELECT COUNT(*) FROM plugin_mikrotik_trees'); } function mikrotik_interfaces_exist() { - return db_fetch_cell("SELECT COUNT(*) FROM plugin_mikrotik_interfaces"); + return db_fetch_cell('SELECT COUNT(*) FROM plugin_mikrotik_interfaces'); } function mikrotik_lists_exist() { - return db_fetch_cell("SELECT COUNT(*) FROM plugin_mikrotik_lists"); + return db_fetch_cell('SELECT COUNT(*) FROM plugin_mikrotik_lists'); } function mikrotik_dns_exist() { - return db_fetch_cell("SELECT COUNT(*) FROM plugin_mikrotik_dns"); + return db_fetch_cell('SELECT COUNT(*) FROM plugin_mikrotik_dns'); } function mikrotik_dhcp_exist() { - return db_fetch_cell("SELECT COUNT(*) FROM plugin_mikrotik_dhcp"); + return db_fetch_cell('SELECT COUNT(*) FROM plugin_mikrotik_dhcp'); } function mikrotik_wireless_aps_exist() { - return db_fetch_cell("SELECT COUNT(*) FROM plugin_mikrotik_wireless_aps"); + return db_fetch_cell('SELECT COUNT(*) FROM plugin_mikrotik_wireless_aps'); } function mikrotik_wregs_exist() { - return db_fetch_cell("SELECT COUNT(*) FROM plugin_mikrotik_wireless_registrations"); + return db_fetch_cell('SELECT COUNT(*) FROM plugin_mikrotik_wireless_registrations'); } function mikrotik_tabs() { global $config; - /* present a tabbed interface */ + // present a tabbed interface $tabs['devices'] = __('Devices', 'mikrotik'); if (mikrotik_users_exist()) { @@ -186,17 +200,17 @@ function mikrotik_tabs() { $tabs['graphs'] = __('Graphs', 'mikrotik'); - /* set the default tab */ + // set the default tab $current_tab = get_request_var('action'); print "