commit
f01b325c8a
8 changed files with 596 additions and 0 deletions
-
3.gitignore
-
32composer.json
-
38examples/ip_address_print.php
-
368src/Client.php
-
54src/Config.php
-
17src/Excetions/Exception.php
-
13src/Interfaces/ClientInterface.php
-
71src/Query.php
@ -0,0 +1,3 @@ |
|||||
|
/.idea/ |
||||
|
/vendor/ |
||||
|
/composer.lock |
||||
@ -0,0 +1,32 @@ |
|||||
|
{ |
||||
|
"name": "evilfreelancer/routeros-api-php", |
||||
|
"type": "library", |
||||
|
"description": "Mikrotik RouterOS API client for your PHP applications", |
||||
|
"keywords": [ |
||||
|
"socket-client", |
||||
|
"psr-4", |
||||
|
"routeros", |
||||
|
"mikrotik" |
||||
|
], |
||||
|
"license": "MIT", |
||||
|
"autoload": { |
||||
|
"psr-4": { |
||||
|
"RouterOS\\": "./src/" |
||||
|
} |
||||
|
}, |
||||
|
"authors": [ |
||||
|
{ |
||||
|
"name": "Paul Rock", |
||||
|
"email": "paul@drteam.rocks", |
||||
|
"homepage": "http://drteam.rocks/", |
||||
|
"role": "Developer" |
||||
|
} |
||||
|
], |
||||
|
"require": { |
||||
|
"php": "^7.0", |
||||
|
"ext-sockets": "*" |
||||
|
}, |
||||
|
"require-dev": { |
||||
|
"phpunit/phpunit": "^6.0" |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,38 @@ |
|||||
|
<?php |
||||
|
require_once __DIR__ . '/../vendor/autoload.php'; |
||||
|
|
||||
|
error_reporting(E_ALL); |
||||
|
|
||||
|
use \RouterOS\Config; |
||||
|
use \RouterOS\Client; |
||||
|
use \RouterOS\Query; |
||||
|
|
||||
|
/** |
||||
|
* Set the params |
||||
|
*/ |
||||
|
$config = new Config(); |
||||
|
$config->host = '192.168.1.104'; |
||||
|
$config->user = 'admin'; |
||||
|
$config->pass = 'admin'; |
||||
|
|
||||
|
/** |
||||
|
* Initiate client with parameters |
||||
|
*/ |
||||
|
$client = new Client($config); |
||||
|
|
||||
|
/** |
||||
|
* Build query |
||||
|
*/ |
||||
|
$query = new Query('/ip/address/print'); |
||||
|
|
||||
|
/** |
||||
|
* Send query to socket server |
||||
|
*/ |
||||
|
$request = $client->write($query); |
||||
|
var_dump($request); |
||||
|
|
||||
|
/** |
||||
|
* Read answer from server |
||||
|
*/ |
||||
|
$response = $client->read(); |
||||
|
var_dump($response); |
||||
@ -0,0 +1,368 @@ |
|||||
|
<?php |
||||
|
|
||||
|
namespace RouterOS; |
||||
|
|
||||
|
use RouterOS\Exceptions\Exception; |
||||
|
|
||||
|
class Client implements Interfaces\ClientInterface |
||||
|
{ |
||||
|
/** |
||||
|
* Socket resource |
||||
|
* @var resource|null |
||||
|
*/ |
||||
|
private static $_socket; |
||||
|
|
||||
|
/** |
||||
|
* Code of error |
||||
|
* @var int |
||||
|
*/ |
||||
|
private $_socket_err_num; |
||||
|
|
||||
|
/** |
||||
|
* Description of socket error |
||||
|
* @var string |
||||
|
*/ |
||||
|
private $_socket_err_str; |
||||
|
|
||||
|
/** |
||||
|
* Configuration of connection |
||||
|
* @var Config |
||||
|
*/ |
||||
|
private $_config; |
||||
|
|
||||
|
/** |
||||
|
* Client constructor. |
||||
|
* @param Config $config |
||||
|
*/ |
||||
|
public function __construct(Config $config) |
||||
|
{ |
||||
|
$this->_config = $config; |
||||
|
$this->connect(); |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* Convert ordinary string to hex string |
||||
|
* |
||||
|
* @param string $string |
||||
|
* @return string |
||||
|
*/ |
||||
|
public function encodeLength(string $string): string |
||||
|
{ |
||||
|
// Yeah, that's insane, but was more ugly, so you need read this post if you interesting a details:
|
||||
|
// https://wiki.mikrotik.com/wiki/Manual:API#API_words
|
||||
|
switch (true) { |
||||
|
case ($string < 0x80): |
||||
|
$string = \chr($string); |
||||
|
break; |
||||
|
case ($string < 0x4000): |
||||
|
$string |= 0x8000; |
||||
|
$string = \chr(($string >> 8) & 0xFF) |
||||
|
. \chr($string & 0xFF); |
||||
|
break; |
||||
|
case ($string < 0x200000): |
||||
|
$string |= 0xC00000; |
||||
|
$string = \chr(($string >> 16) & 0xFF) |
||||
|
. \chr(($string >> 8) & 0xFF) |
||||
|
. \chr($string & 0xFF); |
||||
|
break; |
||||
|
case ($string < 0x10000000): |
||||
|
$string |= 0xE0000000; |
||||
|
$string = \chr(($string >> 24) & 0xFF) |
||||
|
. \chr(($string >> 16) & 0xFF) |
||||
|
. \chr(($string >> 8) & 0xFF) |
||||
|
. \chr($string & 0xFF); |
||||
|
break; |
||||
|
case ($string >= 0x10000000): |
||||
|
$string = \chr(0xF0) |
||||
|
. \chr(($string >> 24) & 0xFF) |
||||
|
. \chr(($string >> 16) & 0xFF) |
||||
|
. \chr(($string >> 8) & 0xFF) |
||||
|
. \chr($string & 0xFF); |
||||
|
break; |
||||
|
} |
||||
|
|
||||
|
return $string; |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* Send write query to RouterOS (with or without tag) |
||||
|
* |
||||
|
* @param Query $query |
||||
|
* @param string|null $tag |
||||
|
* @return $this |
||||
|
*/ |
||||
|
public function write(Query $query, string $tag = null): self |
||||
|
{ |
||||
|
print_r($query); |
||||
|
|
||||
|
// Send commands via loop to router
|
||||
|
foreach ($query->getQuery() as $command) { |
||||
|
$command = trim($command); |
||||
|
fwrite(self::$_socket, $this->encodeLength(\strlen($command)) . $command); |
||||
|
} |
||||
|
|
||||
|
// If tag is not empty, send to socket
|
||||
|
if (null !== $tag) { |
||||
|
fwrite(self::$_socket, $this->encodeLength(\strlen('.tag=' . $tag)) . '.tag=' . $tag); |
||||
|
} |
||||
|
|
||||
|
// Write zero-terminator
|
||||
|
fwrite(self::$_socket, \chr(0)); |
||||
|
|
||||
|
return $this; |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* Read answer from server after query was executed |
||||
|
* |
||||
|
* @param bool $parse |
||||
|
* @return array|string |
||||
|
*/ |
||||
|
public function read($parse = true) |
||||
|
{ |
||||
|
// By default response is empty
|
||||
|
$response = []; |
||||
|
|
||||
|
// Not done by default
|
||||
|
$done = false; |
||||
|
|
||||
|
// Read answer from socket in loop
|
||||
|
while (true) { |
||||
|
// Read the first byte of input which gives us some or all of the length
|
||||
|
// of the remaining reply.
|
||||
|
$byte = \ord(fread(self::$_socket, 1)); |
||||
|
|
||||
|
// 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(self::$_socket, 1)); |
||||
|
} else { |
||||
|
if (($byte & 224) === 192) { |
||||
|
$length = (($byte & 31) << 8) + \ord(fread(self::$_socket, 1)); |
||||
|
$length = ($length << 8) + \ord(fread(self::$_socket, 1)); |
||||
|
} else { |
||||
|
if (($byte & 240) === 224) { |
||||
|
$length = (($byte & 15) << 8) + \ord(fread(self::$_socket, 1)); |
||||
|
$length = ($length << 8) + \ord(fread(self::$_socket, 1)); |
||||
|
$length = ($length << 8) + \ord(fread(self::$_socket, 1)); |
||||
|
} else { |
||||
|
$length = \ord(fread(self::$_socket, 1)); |
||||
|
$length = ($length << 8) + \ord(fread(self::$_socket, 1)); |
||||
|
$length = ($length << 8) + \ord(fread(self::$_socket, 1)); |
||||
|
$length = ($length << 8) + \ord(fread(self::$_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(self::$_socket, $toread); |
||||
|
$retlen = \strlen($_); |
||||
|
} |
||||
|
$response[] = $_; |
||||
|
} |
||||
|
|
||||
|
// If we get a !done, make a note of it.
|
||||
|
if ($_ === '!done') { |
||||
|
$done = true; |
||||
|
} |
||||
|
|
||||
|
// Get status about latest operation
|
||||
|
$status = stream_get_meta_data(self::$_socket); |
||||
|
|
||||
|
// If we do not have unread bytes from socket or <-same and is done, then exit from loop
|
||||
|
if ((!$status['unread_bytes']) || (!$status['unread_bytes'] && $done)) { |
||||
|
break; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
// Parse results and return
|
||||
|
return $parse ? $this->parseResponse($response) : $response; |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* Parse response from Router OS |
||||
|
* |
||||
|
* @param array $response Response data |
||||
|
* @return array Array with parsed data |
||||
|
*/ |
||||
|
public function parseResponse(array $response): array |
||||
|
{ |
||||
|
$parsed = []; |
||||
|
$current = null; |
||||
|
$single = 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 = []; |
||||
|
if (preg_match_all('/[^=]+/i', $x, $matches)) { |
||||
|
if ($matches[0][0] === 'ret') { |
||||
|
$single = $matches[0][1]; |
||||
|
} |
||||
|
$current[$matches[0][0]] = $matches[0][1] ?? ''; |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
if (empty($parsed) && null !== $single) { |
||||
|
$parsed[] = $single; |
||||
|
} |
||||
|
|
||||
|
return $parsed; |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* Authorization logic |
||||
|
* |
||||
|
* @return bool |
||||
|
*/ |
||||
|
public function login(): bool |
||||
|
{ |
||||
|
// For the first we need get hash with salt
|
||||
|
$query = new Query('/login'); |
||||
|
$response = $this->write($query)->read(); |
||||
|
|
||||
|
// Now need use this hash for authorization
|
||||
|
$query = new Query('/login'); |
||||
|
$query->add('=name=' . $this->_config->user); |
||||
|
$query->add('=response=00' . md5(\chr(0) . $this->_config->pass . pack('H*', $response[0]))); |
||||
|
|
||||
|
// Execute query and get response
|
||||
|
$response = $this->write($query)->read(false); |
||||
|
|
||||
|
// Return true if we have only one line from server and this line is !done
|
||||
|
return isset($response[0]) && $response[0] === '!done'; |
||||
|
|
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* Connect to socket server |
||||
|
* |
||||
|
* @return bool |
||||
|
*/ |
||||
|
public function connect(): bool |
||||
|
{ |
||||
|
// Few attempts in loop
|
||||
|
for ($attempt = 1; $attempt <= $this->_config->attempts; $attempt++) { |
||||
|
|
||||
|
// Initiate socket session
|
||||
|
$this->openSocket(); |
||||
|
|
||||
|
// If socket is active
|
||||
|
if ($this->getSocket()) { |
||||
|
|
||||
|
echo 'z'; |
||||
|
|
||||
|
// If we logged in then exit from loop
|
||||
|
if ($this->login()) { |
||||
|
break; |
||||
|
} |
||||
|
|
||||
|
// Else close socket and start from begin
|
||||
|
$this->closeSocket(); |
||||
|
} |
||||
|
|
||||
|
// Sleep some time between tries
|
||||
|
sleep($this->_config->delay); |
||||
|
} |
||||
|
|
||||
|
// Return status of connection
|
||||
|
return true; |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* Save socket resource to static variable |
||||
|
* |
||||
|
* @param resource|null $socket |
||||
|
* @return bool |
||||
|
*/ |
||||
|
private function setSocket($socket): bool |
||||
|
{ |
||||
|
if (\is_resource($socket)) { |
||||
|
self::$_socket = $socket; |
||||
|
return true; |
||||
|
} |
||||
|
return false; |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* Return socket resource if is exist |
||||
|
* |
||||
|
* @return bool|resource |
||||
|
*/ |
||||
|
public function getSocket() |
||||
|
{ |
||||
|
return \is_resource(self::$_socket) |
||||
|
? self::$_socket |
||||
|
: false; |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* Initiate socket session |
||||
|
* |
||||
|
* @return bool |
||||
|
*/ |
||||
|
private function openSocket(): bool |
||||
|
{ |
||||
|
// Connect to server
|
||||
|
$socket = false; |
||||
|
|
||||
|
// Default: Context for ssl
|
||||
|
$context = stream_context_create(['ssl' => ['ciphers' => 'ADH:ALL', 'verify_peer' => false, 'verify_peer_name' => false]]); |
||||
|
|
||||
|
// Default: Proto tcp:// but for ssl we need ssl://
|
||||
|
$proto = $this->_config->ssl ? 'ssl://' : ''; |
||||
|
|
||||
|
try { |
||||
|
// Initiate socket client
|
||||
|
$socket = stream_socket_client( |
||||
|
$proto . $this->_config->host . ':' . $this->_config->port, |
||||
|
$this->_socket_err_num, |
||||
|
$this->_socket_err_str, |
||||
|
$this->_config->timeout, |
||||
|
STREAM_CLIENT_CONNECT, |
||||
|
$context |
||||
|
); |
||||
|
// Throw error is socket is not initiated
|
||||
|
if (false === $socket) { |
||||
|
throw new Exception('stream_socket_client() failed: reason: ' . socket_strerror(socket_last_error())); |
||||
|
} |
||||
|
|
||||
|
} catch (Exception $e) { |
||||
|
// __construct
|
||||
|
} |
||||
|
|
||||
|
// Save socket to static variable
|
||||
|
return $this->setSocket($socket); |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* Close socket session |
||||
|
* |
||||
|
* @return bool |
||||
|
*/ |
||||
|
public function closeSocket(): bool |
||||
|
{ |
||||
|
fclose(self::$_socket); |
||||
|
return true; |
||||
|
} |
||||
|
|
||||
|
} |
||||
@ -0,0 +1,54 @@ |
|||||
|
<?php |
||||
|
|
||||
|
namespace RouterOS; |
||||
|
|
||||
|
class Config |
||||
|
{ |
||||
|
/** |
||||
|
* Address of Mikrotik Router |
||||
|
* @var string |
||||
|
*/ |
||||
|
public $host; |
||||
|
|
||||
|
/** |
||||
|
* Account's username |
||||
|
* @var string |
||||
|
*/ |
||||
|
public $user; |
||||
|
|
||||
|
/** |
||||
|
* Password |
||||
|
* @var string |
||||
|
*/ |
||||
|
public $pass; |
||||
|
|
||||
|
/** |
||||
|
* Number of port for access |
||||
|
* @var int |
||||
|
*/ |
||||
|
public $port = Client::PORT; |
||||
|
|
||||
|
/** |
||||
|
* Enable ssl support |
||||
|
* @var bool |
||||
|
*/ |
||||
|
public $ssl = Client::SSL; |
||||
|
|
||||
|
/** |
||||
|
* Default timeout |
||||
|
* @var int |
||||
|
*/ |
||||
|
public $timeout = Client::TIMEOUT; |
||||
|
|
||||
|
/** |
||||
|
* Count of attempts |
||||
|
* @var int |
||||
|
*/ |
||||
|
public $attempts = Client::ATTEMPTS; |
||||
|
|
||||
|
/** |
||||
|
* Delay between attempts |
||||
|
* @var int |
||||
|
*/ |
||||
|
public $delay = Client::ATTEMPTS_DELAY; |
||||
|
} |
||||
@ -0,0 +1,17 @@ |
|||||
|
<?php |
||||
|
|
||||
|
namespace RouterOS\Exceptions; |
||||
|
|
||||
|
class Exception extends \Exception |
||||
|
{ |
||||
|
public function __construct(string $message = '', int $code = 0, \Throwable $previous = null) |
||||
|
{ |
||||
|
parent::__construct($message, $code, $previous); |
||||
|
|
||||
|
error_log( |
||||
|
'Uncaught Error: ' . $this->getMessage() . ' in ' . $this->getFile() . ':' . $this->getLine() . "\n" |
||||
|
. "Stack trace:\n" . $this->getTraceAsString() . "\n" |
||||
|
. ' thrown in ' . $this->getFile() . ' on line ' . $this->getLine() |
||||
|
); |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,13 @@ |
|||||
|
<?php |
||||
|
|
||||
|
namespace RouterOS\Interfaces; |
||||
|
|
||||
|
interface ClientInterface |
||||
|
{ |
||||
|
const PORT = 8728; |
||||
|
const PORT_SSL = 8729; |
||||
|
const SSL = false; |
||||
|
const TIMEOUT = 1; |
||||
|
const ATTEMPTS = 10; |
||||
|
const ATTEMPTS_DELAY = 1; |
||||
|
} |
||||
@ -0,0 +1,71 @@ |
|||||
|
<?php |
||||
|
|
||||
|
namespace RouterOS; |
||||
|
|
||||
|
class Query |
||||
|
{ |
||||
|
|
||||
|
/** |
||||
|
* Array of query attributes |
||||
|
* @var array |
||||
|
*/ |
||||
|
private $_attributes = []; |
||||
|
|
||||
|
/** |
||||
|
* Endpoint of query |
||||
|
* @var string |
||||
|
*/ |
||||
|
private $_endpoint; |
||||
|
|
||||
|
/** |
||||
|
* Query constructor. |
||||
|
* |
||||
|
* @param string $endpoint Path of endpoint |
||||
|
*/ |
||||
|
public function __construct(string $endpoint) |
||||
|
{ |
||||
|
$this->_endpoint = $endpoint; |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* Append to array yet another attribute of query |
||||
|
* |
||||
|
* @param string $word |
||||
|
* @return $this |
||||
|
*/ |
||||
|
public function add(string $word): self |
||||
|
{ |
||||
|
$this->_attributes[] = $word; |
||||
|
return $this; |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* @return array |
||||
|
*/ |
||||
|
public function getAttributes(): array |
||||
|
{ |
||||
|
return $this->_attributes; |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* @return string |
||||
|
*/ |
||||
|
public function getEndpoint(): string |
||||
|
{ |
||||
|
return $this->_endpoint; |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* Build body of query |
||||
|
* |
||||
|
* @return array |
||||
|
*/ |
||||
|
public function getQuery(): array |
||||
|
{ |
||||
|
$endpoint = $this->getEndpoint(); |
||||
|
$attributes = $this->getAttributes(); |
||||
|
array_unshift($attributes, $endpoint); |
||||
|
|
||||
|
return $attributes; |
||||
|
} |
||||
|
} |
||||
Write
Preview
Loading…
Cancel
Save
Reference in new issue