PHP API bug - read() fails on large replies - this fix OK?

I have been using the PHP API and found an interesting problem. When the read() function is called and the response is greater than 127 characters, it all goes horribly wrong.

This is because the original coder failed to take into account the reply length criteria as defined on http://wiki.mikrotik.com/wiki/API. It took me an hour or so to get my head around how the length works and therefore how to fix the PHP code. Unfortunately, my account doesn’t have the ability to edit the API page on the wiki and so I can’t update the page to make the description more clear. However…

I have changed the read() function in the PHP class as follows:

	function read($parse = true) {

		$RESPONSE = array();

		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;
			
			echo "$BYTE\n";

			// 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->parse_response($RESPONSE);

		return $RESPONSE;

	}

Note that this incorporates the changes I suggested in a previous post (regarding the ‘!done’ reply). Please can somebody with better PHP skills than me have a look and see if it can be optimised? If there’s no response within a few days, I’ll make the changes on the wiki.

Hi,
I can confirm that your implementation of read method runs very nice. The original wiki code was buggy, some commands does not executes at all.
I used moly’s fix http://forum.mikrotik.com/t/php-api-bug/25484/4 But it locks for few seconds on every command. With this implementation is all fine. I would only inicialize $receiveddone = false at the beggining of method call to get rid of php notice.
Thank you!

Hi

this works well but there is another problem with this implementation.

Problem is when you are sending “listen” command (ie: /interface/wireless/registration-table/listen ).

As listen never ends, there is no !done command, and from main program read method just freezes,

You are quite right - as it exists at the moment, the class expects an immediate response to a command and doesn’t work at all well when the response may never come or may come at some point in the future.

Nick.

Solution for listen command

Make new method called listen and comment those two lines, and it will work

         if ($_ == "!done")
            $receiveddone=true;

Pawel Cieplinski

well, listen commands have !done, when you send in cancel, also, listen (and all commands without the end) should have their tag set =tag=unendingStory

so you can use /cancel with =tag=unendingStory to stop the command

then you will have your !done for the command

Ok Janis, but what is the point in listening and cancelling ? If you want to react just after event occurs than you need to modify this API to be able to read without !done message.

Yes, you are right about using tags, but to use tags, this PHP API is absolutly useless.

BTW why is not possible to use command “/export” or eg. “/system/export” or any other export via API? The behavior of script is quite weird, sometimes ends immediately, sometimes returns zeroes, sometimes waits and returns nothing. In any case without useful response. After this query routerboard is often loaded at 100% (probably permanently) and you have to restart it, otherwise system become unstable and very slow. I want to get automatically some actual configurations from my RBs using API, but without any success.

you are able to listen and while listening you are receiving data. as it is endless command it can only be canceled. Once canceled, it will return !done

but all the data will be received previously.

this is basics how API works:

API client --->command ---> router
APi client <---data !done<---router

with endless commands

API client --->command listen ---> router
APi client <---data                <---router
APi client <---data                <---router
.. 
APi client <---data                <---router
APi client <---data                <---router
API client ---> /cancel ----------->router
APi client <---data !done<---        router

so you are able to run several commands simultaneously. it is just a matter of implementation, when you are able to view data. So that is good idea to tag commands, because each ret will have its tag assigned, so you can see what command is returning what.

I use this for reading the length. I derived it from the python example on the API wiki.

The $this->read(X) call goes to a method that reads X bytes off the socket buffer.

	private function readLength()
	{
		/* Get the value of the first byte */		
		$lengthDecoded = ord($this->read(1));
		
		/* Check if we are a single byte */
		if (((int)$lengthDecoded & 0x80) == 0x00)
		{
			
			/* Single byte length */
			return $lengthDecoded;
			
		}
		/* Check if we are a 2 byte length */
		else if (((int)$lengthDecoded & 0xC0) == 0x80)
		{
			
			/* Logical AND the length with the inverse of the encoding flag, producing the original value for the lower byte */
			$lengthDecoded = $lengthDecoded & (~0xC0);
			/* Shift the value 8 bytes */
			$lengthDecoded = (int)$lengthDecoded << 8;
			/* Add the next value onto the working value */
			$lengthDecoded += ord($this->read(1));
			
			return $lengthDecoded;
			
		}
		/* Check if we are a 3 byte length */
		else if (((int)$lengthDecoded & 0xE0) == 0xC0)
		{
			
			/* Logical AND the length with the inverse of the encoding flag, producing the original value for the lower byte */
			$lengthDecoded = $lengthDecoded & (~0xE0);
			/* Shift the value 8 bytes */
			$lengthDecoded = (int)$lengthDecoded << 8;
			/* Add the next value onto the working value */
			$lengthDecoded += ord($this->read(1));
			/* Shift the value 8 bytes */
			$lengthDecoded = (int)$lengthDecoded << 8;
			/* Add the next value onto the working value */
			$lengthDecoded += ord($this->read(1));		
			
			return $lengthDecoded;
			
		}
		/* Check if we are a 4 byte length */
		else if (((int)$lengthDecoded & 0xF0) == 0xE0)
		{
			
			/* Logical AND the length with the inverse of the encoding flag, producing the original value for the lower byte */
			$lengthDecoded = $lengthDecoded & (~0xF0);
			/* Shift the value 8 bytes */
			$lengthDecoded = (int)$lengthDecoded << 8;
			/* Add the next value onto the working value */
			$lengthDecoded += ord($this->read(1));
			/* Shift the value 8 bytes */
			$lengthDecoded = (int)$lengthDecoded << 8;
			/* Add the next value onto the working value */
			$lengthDecoded += ord($this->read(1));		
			/* Shift the value 8 bytes */
			$lengthDecoded = (int)$lengthDecoded << 8;
			/* Add the next value onto the working value */
			$lengthDecoded += ord($this->read(1));				
			
			return $lengthDecoded;
			
		}
		else
		{
			
			throw new RosWordTooLongException("First byte of length was: 0x".dechex($lengthDecoded));
			
		}
	
		
	}

NAB, thank you for posting this script. I had to make one modification to it as I was getting the following error:

PHP Notice: Undefined variable: receiveddone in routeros_api.class.php on line 322

I had to add an else statement so that the var was valid:

// If we get a !done, make a note of it.
         if ($_ == "!done")
            $receiveddone=true;

         else {
            $receiveddone=false;
         }

I’m using this script to get the equivalent of /ip hotspot active print count-only as I can not get my old 2.9 expect scripts to work quickly enough or properly with 3.0 due to all of the garbage you get when ssh’ing into a router (yes I’ve tried +ct and 80w).

Anyway, hope this helps…

I have problem with connection??? Sometimes connect, sometimes not!!!

Connection attempt #1 to 10.2.2.7:8728…
<<< [6] /login

[5/5 bytes read.
[5, 39] !done
[37/37 bytes read.
[37, 1] =ret=ce97ace24bd5dd4b4d451ca4ceddce40
Connection attempt #2 to 10.2.2.7:8728…
<<< [6] /login
[5/5 bytes read.
[5, 39] !done
[37/37 bytes read.
[37, 1] =ret=0cbc43a8886382dfa87122bccebf8f2c
Connection attempt #3 to 10.2.2.7:8728…
<<< [6] /login
[5/5 bytes read.
[5, 39] !done
[37/37 bytes read.
[37, 1] =ret=d20f103c25896266854f7796e2706af3
Connection attempt #4 to 10.2.2.7:8728…
<<< [6] /login
[5/5 bytes read.
[5, 39] !done
[37/37 bytes read.
[37, 1] =ret=195c5b110900cae2547a27294f2e888b
Connection attempt #5 to 10.2.2.7:8728…
<<< [6] /login
[5/5 bytes read.
[5, 39] !done
[37/37 bytes read.
[37, 1] =ret=9e95e5ad2525e86f2547e3afecf999bf
Error…

Hi there

$API->write('/tool/torch/<pppoe-user>');

returns

Array ( [!trap] => Array ( [0] => Array ( [message] => no such command ) ) )

Is API usable for this operation (torch for pppoe interfaces…)?

there’s no such command - ‘/tool/torch/’
correct syntax is

/tool/torch
=interface=bla-bla-bla

Thanks.


Now I have another problem…

$API->write('/tool/torch',false);
$API->write('=interface=<pppoe-user>',false);
$API->write('/cancel',false);
$API->write('=tag=unendingStory',true);
$READ = $API->read(false);
$ARRAY = $API->parse_response($READ);

Returns…

Array ( [!trap] => Array ( [0] => Array ( [message] => unknown parameter ) ) )

exists in interface list and traffic exist too…

I made few php scripts which works with API but I’m not sure how to deal with “never ending commands”

Best regards and happy Christmas

I think, for ‘/tool/torch’ command, there’s no such parameter - ‘/cancel’ =)

/cancel is because torch is “never ending command”

I’ll see what I can do with it… :frowning:

Best regards

/cancel is just another command. please, read http://wiki.mikrotik.com/wiki/API#Tags

by the way, you may add ‘duration’ parameter - then Torch won’t be ‘never ending’ =)

also, for outputs and testing command i would suggest to use python example from wiki page, that way you see if command you are trying to run is capable to run at all. If it is, you should be able to get same or similar output from every other API implementation. Just that python example is made my MikroTik

Hi erazor999.

I had the same problem. It was fixed when I followed the suggestion found here:

http://forum.mikrotik.com/t/api-login-problem-on-rb133/27084/3

The sleep before read()…