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