Skip to content

Commit bb3e52a

Browse files
Merge pull request #8 from EvilFreelancer/streamrefactor
Stream refactoring
2 parents 6dc436a + 0a60e8c commit bb3e52a

16 files changed

+1332
-140
lines changed

phpunit.xml

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,4 +16,11 @@
1616
<directory suffix=".php">./tests/</directory>
1717
</testsuite>
1818
</testsuites>
19+
<php>
20+
<env name="ROS_HOST" value="127.0.0.1"/>
21+
<env name="ROS_USER" value="admin"/>
22+
<env name="ROS_PASS" value="admin"/>
23+
<env name="ROS_PORT_MODERN" value="18728"/>
24+
<env name="ROS_PORT_LEGACY" value="28728"/>
25+
</php>
1926
</phpunit>

src/APIConnector.php

Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
<?php
2+
3+
namespace RouterOS;
4+
5+
use RouterOS\Interfaces\StreamInterface;
6+
7+
/**
8+
* Class APIConnector
9+
*
10+
* Implement middle level dialog with router by masking word dialog implementation to client class
11+
*
12+
* @package RouterOS
13+
* @since 0.9
14+
*/
15+
class APIConnector
16+
{
17+
/**
18+
* @var StreamInterface $stream The stream used to communicate with the router
19+
*/
20+
protected $stream;
21+
22+
/**
23+
* Constructor
24+
*
25+
* @param StreamInterface $stream
26+
*/
27+
28+
public function __construct(StreamInterface $stream)
29+
{
30+
$this->stream = $stream;
31+
}
32+
33+
/**
34+
* Reads a WORD from the stream
35+
*
36+
* WORDs are part of SENTENCE. Each WORD has to be encoded in certain way - length of the WORD followed by WORD content.
37+
* Length of the WORD should be given as count of bytes that are going to be sent
38+
*
39+
* @return string The word content, en empty string for end of SENTENCE
40+
*/
41+
public function readWord(): string
42+
{
43+
// Get length of next word
44+
$length = APILengthCoDec::decodeLength($this->stream);
45+
return ($length > 0) ? $this->stream->read($length) : '';
46+
}
47+
48+
/**
49+
* Write word to stream
50+
*
51+
* @param string $word
52+
* @return int return number of written bytes
53+
*/
54+
public function writeWord(string $word): int
55+
{
56+
$encodedLength = APILengthCoDec::encodeLength(strlen($word));
57+
return $this->stream->write($encodedLength . $word);
58+
}
59+
}

src/APILengthCoDec.php

Lines changed: 196 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,196 @@
1+
<?php
2+
3+
namespace RouterOS;
4+
5+
use RouterOS\Interfaces\StreamInterface;
6+
use RouterOS\Helpers\BinaryStringHelper;
7+
8+
/**
9+
* class APILengthCoDec
10+
*
11+
* Coder / Decoder for length field in mikrotik API communication protocol
12+
*
13+
* @package RouterOS
14+
* @since 0.9
15+
*/
16+
class APILengthCoDec
17+
{
18+
/**
19+
* Encode string to length of string
20+
*
21+
* @param int|float $length
22+
* @return string
23+
*/
24+
public static function encodeLength($length): string
25+
{
26+
// Encode the length :
27+
// - if length <= 0x7F (binary : 01111111 => 7 bits set to 1)
28+
// - encode length with one byte
29+
// - set the byte to length value, as length maximal value is 7 bits set to 1, the most significant bit is always 0
30+
// - end
31+
// - length <= 0x3FFF (binary : 00111111 11111111 => 14 bits set to 1)
32+
// - encode length with two bytes
33+
// - set length value to 0x8000 (=> 10000000 00000000)
34+
// - add length : as length maximumal value is 14 bits to 1, this does not modify the 2 most significance bits (10)
35+
// - end
36+
// => minimal encoded value is 10000000 10000000
37+
// - length <= 0x1FFFFF (binary : 00011111 11111111 11111111 => 21 bits set to 1)
38+
// - encode length with three bytes
39+
// - set length value to 0xC00000 (binary : 11000000 00000000 00000000)
40+
// - add length : as length maximal vlaue is 21 bits to 1, this does not modify the 3 most significance bits (110)
41+
// - end
42+
// => minimal encoded value is 11000000 01000000 00000000
43+
// - length <= 0x0FFFFFFF (binary : 00001111 11111111 11111111 11111111 => 28 bits set to 1)
44+
// - encode length with four bytes
45+
// - set length value to 0xE0000000 (binary : 11100000 00000000 00000000 00000000)
46+
// - add length : as length maximal vlaue is 28 bits to 1, this does not modify the 4 most significance bits (1110)
47+
// - end
48+
// => minimal encoded value is 11100000 00100000 00000000 00000000
49+
// - length <= 0x7FFFFFFFFF (binary : 00000111 11111111 11111111 11111111 11111111 => 35 bits set to 1)
50+
// - encode length with five bytes
51+
// - set length value to 0xF000000000 (binary : 11110000 00000000 00000000 00000000 00000000)
52+
// - add length : as length maximal vlaue is 35 bits to 1, this does not modify the 5 most significance bits (11110)
53+
// - end
54+
// - length > 0x7FFFFFFFFF : not supported
55+
56+
if ($length < 0) {
57+
throw new \DomainException("Length of word could not to be negative ($length)");
58+
}
59+
60+
if ($length <= 0x7F) {
61+
return BinaryStringHelper::IntegerToNBOBinaryString($length);
62+
}
63+
64+
if ($length <= 0x3FFF) {
65+
return BinaryStringHelper::IntegerToNBOBinaryString(0x8000 + $length);
66+
}
67+
68+
if ($length <= 0x1FFFFF) {
69+
return BinaryStringHelper::IntegerToNBOBinaryString(0xC00000 + $length);
70+
}
71+
72+
if ($length <= 0x0FFFFFFF) {
73+
return BinaryStringHelper::IntegerToNBOBinaryString(0xE0000000 + $length);
74+
}
75+
76+
// https://wiki.mikrotik.com/wiki/Manual:API#API_words
77+
// If len >= 0x10000000 then 0xF0 and len as four bytes
78+
return BinaryStringHelper::IntegerToNBOBinaryString(0xF000000000 + $length);
79+
}
80+
81+
// Decode length of data when reading :
82+
// The 5 firsts bits of the first byte specify how the length is encoded.
83+
// The position of the first 0 value bit, starting from the most significant postion.
84+
// - 0xxxxxxx => The 7 remainings bits of the first byte is the length :
85+
// => min value of length is 0x00
86+
// => max value of length is 0x7F (127 bytes)
87+
// - 10xxxxxx => The 6 remainings bits of the first byte plus the next byte represent the lenght
88+
// NOTE : the next byte MUST be at least 0x80 !!
89+
// => min value of length is 0x80
90+
// => max value of length is 0x3FFF (16,383 bytes, near 16 KB)
91+
// - 110xxxxx => The 5 remainings bits of th first byte and the two next bytes represent the length
92+
// => max value of length is 0x1FFFFF (2,097,151 bytes, near 2 MB)
93+
// - 1110xxxx => The 4 remainings bits of the first byte and the three next bytes represent the length
94+
// => max value of length is 0xFFFFFFF (268,435,455 bytes, near 270 MB)
95+
// - 11110xxx => The 3 remainings bits of the first byte and the four next bytes represent the length
96+
// => max value of length is 0x7FFFFFFF (2,147,483,647 byes, 2GB)
97+
// - 11111xxx => This byte is not a length-encoded word but a control byte.
98+
// => Extracted from Mikrotik API doc :
99+
// it is a reserved control byte.
100+
// After receiving unknown control byte API client cannot proceed, because it cannot know how to interpret following bytes
101+
// Currently control bytes are not used
102+
103+
public static function decodeLength(StreamInterface $stream): int
104+
{
105+
// if (false === is_resource($stream)) {
106+
// throw new \InvalidArgumentException(
107+
// sprintf(
108+
// 'Argument must be a stream resource type. %s given.',
109+
// gettype($stream)
110+
// )
111+
// );
112+
// }
113+
114+
// Read first byte
115+
$firstByte = ord($stream->read(1));
116+
117+
// If first byte is not set, length is the value of the byte
118+
if (0 === ($firstByte & 0x80)) {
119+
return $firstByte;
120+
}
121+
122+
// if 10xxxxxx, length is 2 bytes encoded
123+
if (0x80 === ($firstByte & 0xC0)) {
124+
// Set 2 most significands bits to 0
125+
$result = $firstByte & 0x3F;
126+
127+
// shift left 8 bits to have 2 bytes
128+
$result <<= 8;
129+
130+
// read next byte and use it as least significant
131+
$result |= ord($stream->read(1));
132+
return $result;
133+
}
134+
135+
// if 110xxxxx, length is 3 bytes encoded
136+
if (0xC0 === ($firstByte & 0xE0)) {
137+
// Set 3 most significands bits to 0
138+
$result = $firstByte & 0x1F;
139+
140+
// shift left 16 bits to have 3 bytes
141+
$result <<= 16;
142+
143+
// read next 2 bytes as value and use it as least significant position
144+
$result |= (ord($stream->read(1)) << 8);
145+
$result |= ord($stream->read(1));
146+
return $result;
147+
}
148+
149+
// if 1110xxxx, length is 4 bytes encoded
150+
if (0xE0 === ($firstByte & 0xF0)) {
151+
// Set 4 most significance bits to 0
152+
$result = $firstByte & 0x0F;
153+
154+
// shift left 24 bits to have 4 bytes
155+
$result <<= 24;
156+
157+
// read next 3 bytes as value and use it as least significant position
158+
$result |= (ord($stream->read(1)) << 16);
159+
$result |= (ord($stream->read(1)) << 8);
160+
$result |= ord($stream->read(1));
161+
return $result;
162+
}
163+
164+
// if 11110xxx, length is 5 bytes encoded
165+
if (0xF0 === ($firstByte & 0xF8)) {
166+
// Not possible on 32 bits systems
167+
if (PHP_INT_SIZE < 8) {
168+
// Cannot be done on 32 bits systems
169+
// PHP5 windows versions of php, even on 64 bits systems was impacted
170+
// see : https://stackoverflow.com/questions/27865340/php-int-size-returns-4-but-my-operating-system-is-64-bit
171+
// How can we test it ?
172+
173+
// @codeCoverageIgnoreStart
174+
throw new \OverflowException("Your system is using 32 bits integers, cannot decode this value ($firstByte) on this system");
175+
// @codeCoverageIgnoreEnd
176+
}
177+
178+
// Set 5 most significance bits to 0
179+
$result = $firstByte & 0x07;
180+
181+
// shift left 232 bits to have 5 bytes
182+
$result <<= 32;
183+
184+
// read next 4 bytes as value and use it as least significant position
185+
$result |= (ord($stream->read(1)) << 24);
186+
$result |= (ord($stream->read(1)) << 16);
187+
$result |= (ord($stream->read(1)) << 8);
188+
$result |= ord($stream->read(1));
189+
return $result;
190+
}
191+
192+
// Now the only solution is 5 most significance bits are set to 1 (11111xxx)
193+
// This is a control word, not implemented by Mikrotik for the moment
194+
throw new \UnexpectedValueException('Control Word found');
195+
}
196+
}

0 commit comments

Comments
 (0)