Browse Source

initial release

tags/0.1
pasha 7 years ago
commit
f01b325c8a
  1. 3
      .gitignore
  2. 32
      composer.json
  3. 38
      examples/ip_address_print.php
  4. 368
      src/Client.php
  5. 54
      src/Config.php
  6. 17
      src/Excetions/Exception.php
  7. 13
      src/Interfaces/ClientInterface.php
  8. 71
      src/Query.php

3
.gitignore

@ -0,0 +1,3 @@
/.idea/
/vendor/
/composer.lock

32
composer.json

@ -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"
}
}

38
examples/ip_address_print.php

@ -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);

368
src/Client.php

@ -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;
}
}

54
src/Config.php

@ -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;
}

17
src/Excetions/Exception.php

@ -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()
);
}
}

13
src/Interfaces/ClientInterface.php

@ -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;
}

71
src/Query.php

@ -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;
}
}
Loading…
Cancel
Save