Manual:API: Difference between revisions

From MikroTik Wiki
Jump to navigation Jump to search
(java API example is slighly incorrect and is not working)
 
m (Protected "API": will be in manual [edit=sysop:move=sysop])
(10 intermediate revisions by the same user not shown)
Line 1: Line 1:
This document describes the operation of MikroTik RouterOS API for RouterOS3. The API (application programming interface) is a way to create your own versions of Winbox. This guide will help you make simplified, or translated conrol applications for RouterOS v3.  
This document describes the operation of MikroTik RouterOS API for RouterOS3. The API (application programming interface) is a way to create your own versions of Winbox. This guide will help you make simplified, or translated control applications for RouterOS v3.  




Line 12: Line 12:


* Protocol stream is formatted as a sequence of words.
* Protocol stream is formatted as a sequence of words.
* Each word is encoded as lenght, followed by that many bytes of content.
* Each word is encoded as length, followed by that many bytes of content.
* Words are grouped into sentences. End of sentence is terminated by zero length word.
* Words are grouped into sentences. End of sentence is terminated by zero length word.
* Lenght is encoded as follows:
* Length is encoded as follows:
{| border="1"
{| border="1"
!Value of length !! Number of bytes !! Encoding
!Value of length !! Number of bytes !! Encoding
Line 43: Line 43:
* If first byte of word is '''>= 0xF8''', then it is a reserved control byte. After receiving unknown control byte API client cannot proceed, because it cannot know how to interpret following bytes.
* If first byte of word is '''>= 0xF8''', then it is a reserved control byte. After receiving unknown control byte API client cannot proceed, because it cannot know how to interpret following bytes.
* Currently control bytes are not used.
* Currently control bytes are not used.


== Short description of API sentences ==
== Short description of API sentences ==
Line 66: Line 65:
  =disable-running-check=yes
  =disable-running-check=yes
** Command argument should begin with '=' followed by name of argument, followed by another '=', followed by value of argument.
** Command argument should begin with '=' followed by name of argument, followed by another '=', followed by value of argument.
** Agrument value can be empty and can contain '='.
** There are API specific arguments, such as <tt>.id</tt>. Names of API specific arguments begin with dot.
** Argument value can be empty and can contain '='.
** Command sentence can have parameters that are specific to and processed by API protocol. These parameters should begin with '.' followed by name of parameter, followed by '=', followed by value of parameter.
** Currently the only such parameter is 'tag'.
** Order of arguments and API parameters is not important and cannot be relied on
** First word of reply begins with '!'.
** First word of reply begins with '!'.
** Each command generates at least one reply (if connection does not get terminated).
** Each command generates at least one reply (if connection does not get terminated).
Line 98: Line 101:
* In case of error, reply contains <tt>=ret=''error message''</tt>.
* In case of error, reply contains <tt>=ret=''error message''</tt>.
* In case of successful login client can start to issue commands.
* In case of successful login client can start to issue commands.
== Tags ==
* It is possible to run several commands simultaneously, without waiting for previous one to complete. If API client is doing this and needs to differentiate command responses, it can use 'tag' API parameter in command sentences.
* If you include 'tag' parameter with non-empty value in command sentence, then 'tag' parameter with exactly the same value will be included in all responses generated by this command.
* If you do not include 'tag' parameter or it's value is empty, then all responses for this command will not have 'tag' parameter.
== Command description ==
* <tt>/cancel</tt>
** optional argument: <tt>=tag=<i>tag of command to cancel</i></tt>, without it cancels all running commands
** does not cancel itself
** all canceled commands are interruped and in the usual case generate '!trap' and '!done' responses
** please note that <tt>/cancel</tt> is separate command and can have it's own unique '.tag' parameter, that is not related to '=tag' argument of this command
* <tt>listen</tt>
** <tt>listen</tt> command is avaliable where console print command is available, but it does not have expected effect everywhere (i.e. may not work)
** <tt>!re</tt> sentences are generated as something changes in particular item list
** when item is deleted or dissapears in any other way, the '!re' sentence includes value '=.dead=yes'
** This command does not terminate. To terminate it use <tt>/cancel</tt> command.
* <tt>getall</tt>
** <tt>getall</tt> command is available where console print command is available
** replies contain <tt>=.id=''Item internal number''</tt> property.


== Command examples ==
== Command examples ==


=== <tt>/system/package/getall</tt> ===
=== <tt>/system/package/getall</tt> ===
* Note that replies contain <tt>=.id=''Item internal number''</tt> property, that is not available in console. Names of properties that are API specific begin with dot.
* <tt>getall</tt> command is avaliable where console print command is available


{{bapi}}
{{bapi}}
Line 140: Line 165:


=== <tt>/user/active/listen</tt> ===
=== <tt>/user/active/listen</tt> ===
* <tt>listen</tt> command is avaliable where console print command is available, but it does not have expected effect everywhere (i.e. may not work)
* <tt>!re</tt> sentences are generated as something changes in particular item list
* this command does not terminate


{{bapi}}
{{bapi}}
Line 162: Line 184:
| ... more !re sentences ...
| ... more !re sentences ...
|-
|-
{{eapi}}
=== <tt>/cancel</tt>, simultaneous commands ===
{{bapi}}
{{apic|/login}}
{{apic|}}
{{apis|!done}}
{{apis|1==ret=856780b7411eefd3abadee2058c149a3}}
{{apis|}}
{{apic|/login}}
{{apic|1==name=admin}}
{{apic|1==response=005062f7a5ef124d34675bf3e81f56c556}}
{{apic|}}
{{apis|!done}}
{{apis|}}
{{apih|first start listening for interface changes (tag is 2)}}
{{apic|/interface/listen}}
{{apic|1=.tag=2}}
{{apic|}}
{{apih|disable interface (tag is 3)}}
{{apic|/interface/set}}
{{apic|1==disabled=yes}}
{{apic|1==.id=ether1}}
{{apic|1=.tag=3}}
{{apic|}}
{{apih|this is done for disable command (tag 3)}}
{{apis|!done}}
{{apis|1=.tag=3}}
{{apis|}}
{{apih|enable interface (tag is 4)}}
{{apic|/interface/set}}
{{apic|1==disabled=no}}
{{apic|1==.id=ether1}}
{{apic|1=.tag=4}}
{{apic|}}
{{apih|this update is generated by change made by first set command (tag 3)}}
{{apis|!re}}
{{apis|1==.id=*1}}
{{apis|1==disabled=yes}}
{{apis|1==dynamic=no}}
{{apis|1==running=no}}
{{apis|1==name=ether1}}
{{apis|1==mtu=1500}}
{{apis|1==type=ether}}
{{apis|1=.tag=2}}
{{apis|}}
{{apih|this is done for enable command (tag 4)}}
{{apis|!done}}
{{apis|1=.tag=4}}
{{apis|}}
{{apih|get interface list (tag is 5)}}
{{apic|/interface/getall}}
{{apic|1=.tag=5}}
{{apic|}}
{{apih|this update is generated by change made by second set command (tag 4)}}
{{apis|!re}}
{{apis|1==.id=*1}}
{{apis|1==disabled=no}}
{{apis|1==dynamic=no}}
{{apis|1==running=yes}}
{{apis|1==name=ether1}}
{{apis|1==mtu=1500}}
{{apis|1==type=ether}}
{{apis|1=.tag=2}}
{{apis|}}
{{apih|these are replies to getall command (tag 5)}}
{{apis|!re}}
{{apis|1==.id=*1}}
{{apis|1==disabled=no}}
{{apis|1==dynamic=no}}
{{apis|1==running=yes}}
{{apis|1==name=ether1}}
{{apis|1==mtu=1500}}
{{apis|1==type=ether}}
{{apis|1=.tag=5}}
{{apis|}}
{{apis|!re}}
{{apis|1==.id=*2}}
{{apis|1==disabled=no}}
{{apis|1==dynamic=no}}
{{apis|1==running=yes}}
{{apis|1==name=ether2}}
{{apis|1==mtu=1500}}
{{apis|1==type=ether}}
{{apis|1=.tag=5}}
{{apis|}}
{{apih|here interface getall ends (tag 5)}}
{{apis|!done}}
{{apis|1=.tag=5}}
{{apis|}}
{{apih|stop listening - request to cancel command with tag 2, cancel itself uses tag 7}}
{{apic|/cancel}}
{{apic|1==tag=2}}
{{apic|1=.tag=7}}
{{apic|}}
{{apih|listen command is interrupted (tag 2)}}
{{apis|!trap}}
{{apis|1==category=2}}
{{apis|1==message=interrupted}}
{{apis|1=.tag=2}}
{{apis|}}
{{apih|cancel command is finished (tag 7)}}
{{apis|!done}}
{{apis|1=.tag=7}}
{{apis|}}
{{apih|listen command is finished (tag 2)}}
{{apis|!done}}
{{apis|1=.tag=2}}
{{apis|}}
{{eapi}}
{{eapi}}


Line 169: Line 301:
* usage: api.py ''ip-address'' ''username'' ''password''
* usage: api.py ''ip-address'' ''username'' ''password''
* after that type words from keyboard, terminating them with newline
* after that type words from keyboard, terminating them with newline
* Since empty word terminates sentence, you should press enter '''twice''' after last word before sentence will be sent to router.


<pre>
<pre>
#!/usr/bin/python
#!/usr/bin/python
import sys, posix, time, md5, binascii, socket
 
import sys, posix, time, md5, binascii, socket, select


class ApiRos:
class ApiRos:
Line 178: Line 312:
     def __init__(self, sk):
     def __init__(self, sk):
         self.sk = sk
         self.sk = sk
        self.currenttag = 0
       
     def login(self, username, pwd):
     def login(self, username, pwd):
         for repl, attrs in self.say(["/login"]):
         for repl, attrs in self.talk(["/login"]):
             chal = binascii.unhexlify(attrs['=ret'])
             chal = binascii.unhexlify(attrs['=ret'])
         md = md5.new()
         md = md5.new()
Line 185: Line 321:
         md.update(pwd)
         md.update(pwd)
         md.update(chal)
         md.update(chal)
         None in self.say(["/login", "=name=" + username, "=response=00" + binascii.hexlify(md.digest())])
         self.talk(["/login", "=name=" + username,
                  "=response=00" + binascii.hexlify(md.digest())])


     def say(self, words):
     def talk(self, words):
         if self.writeWords(words) == 0: return
         if self.writeSentence(words) == 0: return
        r = []
         while 1:
         while 1:
             i = self.readWords()
             i = self.readSentence();
             repl = i.next()
             if len(i) == 0: continue
            reply = i[0]
             attrs = {}
             attrs = {}
             for w in i:
             for w in i[1:]:
                 j = w.find('=', 1)
                 j = w.find('=', 1)
                 if (j == -1):
                 if (j == -1):
Line 199: Line 338:
                 else:
                 else:
                     attrs[w[:j]] = w[j+1:]
                     attrs[w[:j]] = w[j+1:]
             yield (repl, attrs)
             r.append((reply, attrs))
             if repl == '!done': return
             if reply == '!done': return r
     def writeWords(self, words):
 
     def writeSentence(self, words):
         ret = 0
         ret = 0
         for w in words:
         for w in words:
Line 208: Line 348:
         self.writeWord('')
         self.writeWord('')
         return ret
         return ret
     def readWords(self):
 
         while 1:
     def readSentence(self):
            w = self.readWord()
         r = []
            if (w != ''): break
         while 1:
         while 1:
            yield w
             w = self.readWord()
             w = self.readWord()
             if w == '': return
             if w == '': return r
            r.append(w)
           
     def writeWord(self, w):
     def writeWord(self, w):
         print "<<< " + w
         print "<<< " + w
         self.writeLen(len(w))
         self.writeLen(len(w))
         self.writeStr(w)
         self.writeStr(w)
     def readWord(self):
     def readWord(self):
         ret = self.readStr(self.readLen())
         ret = self.readStr(self.readLen())
         print ">>> " + ret
         print ">>> " + ret
         return ret
         return ret
     def writeLen(self, l):
     def writeLen(self, l):
         if l < 0x80:
         if l < 0x80:
Line 248: Line 390:
             self.writeStr(chr((l >> 8) & 0xFF))
             self.writeStr(chr((l >> 8) & 0xFF))
             self.writeStr(chr(l & 0xFF))
             self.writeStr(chr(l & 0xFF))
     def readLen(self):               
     def readLen(self):               
         c = ord(self.readStr(1))     
         c = ord(self.readStr(1))     
Line 279: Line 422:
             c += ord(self.readStr(1))     
             c += ord(self.readStr(1))     
         return c                     
         return c                     
     def writeStr(self, str):         
     def writeStr(self, str):         
         n = 0;                       
         n = 0;                       
Line 285: Line 429:
             if r == 0: raise RuntimeError, "connection closed by remote end"
             if r == 0: raise RuntimeError, "connection closed by remote end"
             n += r                   
             n += r                   
     def readStr(self, length):       
     def readStr(self, length):       
         ret = ''                     
         ret = ''                     
Line 290: Line 435:
             s = self.sk.recv(length - len(ret))
             s = self.sk.recv(length - len(ret))
             if s == '': raise RuntimeError, "connection closed by remote end"
             if s == '': raise RuntimeError, "connection closed by remote end"
             ret += s              
             ret += s
         return ret                
         return ret
                                   
 
def getParagraph():               
def main():
    for l in iter(sys.stdin.readline, '\n'):
        yield l[:-1]               
                                   
def main():                        
     s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
     s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
     s.connect((sys.argv[1], 8728))   
     s.connect((sys.argv[1], 8728))   
     apiros = ApiRos(s);             
     apiros = ApiRos(s);             
     apiros.login(sys.argv[2], sys.argv[3]);
     apiros.login(sys.argv[2], sys.argv[3]);
     while 1:                      
 
         for repl, attrs in apiros.say(getParagraph()):
    inputsentence = []
             print repl             
 
             print attrs           
     while 1:
             print ''              
         r = select.select([s, sys.stdin], [], [], None)
                                   
        if s in r[0]:
if __name__ == '__main__':        
            # something to read in socket, read sentence
     main()                        
            x = apiros.readSentence()
 
        if sys.stdin in r[0]:
            # read line from input and strip off newline
            l = sys.stdin.readline()
            l = l[:-1]
 
             # if empty line, send sentence and start with new
             # otherwise append to input sentence
             if l == '':
                apiros.writeSentence(inputsentence)
                inputsentence = []
            else:
                inputsentence.append(l)
 
if __name__ == '__main__':
     main()
</pre>
 
Example run:
 
<pre>
debian@localhost:~/api-test$ ./api.py 10.0.0.1 admin ''
<<< /login
<<<
>>> !done
>>> =ret=93b438ec9b80057c06dd9fe67d56aa9a
>>>
<<< /login
<<< =name=admin
<<< =response=00e134102a9d330dd7b1849fedfea3cb57
<<<
>>> !done
>>>
/user/getall
 
<<< /user/getall
<<<
>>> !re
>>> =.id=*1
>>> =disabled=no
>>> =name=admin
>>> =group=full
>>> =address=0.0.0.0/0
>>> =netmask=0.0.0.0
>>>
>>> !done
>>>
</pre>
</pre>

Revision as of 14:07, 18 February 2008

This document describes the operation of MikroTik RouterOS API for RouterOS3. The API (application programming interface) is a way to create your own versions of Winbox. This guide will help you make simplified, or translated control applications for RouterOS v3.


Enabling and Connecting

  • Default API port is 8728. By default API is disabled
  • Enable API
/ip service enable api
  • Now you can initiate TCP connection to the API port of the router

Protocol

  • Protocol stream is formatted as a sequence of words.
  • Each word is encoded as length, followed by that many bytes of content.
  • Words are grouped into sentences. End of sentence is terminated by zero length word.
  • Length is encoded as follows:
Value of length Number of bytes Encoding
0 <= len <= 0x7F 1 len, lowest byte
0x80 <= len <= 0x3FFF 2 len | 0x8000, two lower bytes
0x4000 <= len <= 0x1FFFFF 3 len | 0xC00000, three lower bytes
0x200000 <= len <= 0xFFFFFFF 4 len | 0xE0000000
len >= 0x10000000 5 0xF0 and len as four bytes
  • Although this scheme allows encoding of length up to 0x7FFFFFFFF, only four byte length is supported.
  • Bytes of len are sent most significant first (network order).
  • If first byte of word is >= 0xF8, then it is a reserved control byte. After receiving unknown control byte API client cannot proceed, because it cannot know how to interpret following bytes.
  • Currently control bytes are not used.

Short description of API sentences

  • Empty sentences are ignored.
  • Sentences are processed after receiving terminating zero length word.
  • There is a limit on number and size of sentences client can send before it has logged in.
  • Commands
    • First word is name of command. Examples:
 /login
 /ip/address/getall
 /user/active/listen
 /interface/vlan/remove
 /system/reboot
    • Names of commands closely follow console, with spaces replaced by '/'. There are commands that are specific to API, like getall or login.
    • Name of command should begin with '/'.
    • Next, command arguments can be specified. Examples:
=address=10.0.0.1
=name=iu=c3Eeg
=disable-running-check=yes
    • Command argument should begin with '=' followed by name of argument, followed by another '=', followed by value of argument.
    • There are API specific arguments, such as .id. Names of API specific arguments begin with dot.
    • Argument value can be empty and can contain '='.
    • Command sentence can have parameters that are specific to and processed by API protocol. These parameters should begin with '.' followed by name of parameter, followed by '=', followed by value of parameter.
    • Currently the only such parameter is 'tag'.
    • Order of arguments and API parameters is not important and cannot be relied on
    • First word of reply begins with '!'.
    • Each command generates at least one reply (if connection does not get terminated).
    • Last reply for every command is reply that has first word !done.
    • Errors and exceptional conditions begin with !trap.
    • Data replies begin with !re


Initial login

Template:Apic

Template:Apic

Template:Apis

Template:Apis

Template:Apis

Template:Apic

Template:Apic

Template:Apic

Template:Apic

Template:Apis

Template:Apis

  • First, clients sends /login command.
  • Note that each command and response ends with an empty word.
  • Reply contains =ret=challenge argument.
  • Client sends second /login command, with =name=username and =response=response.
  • In case of error, reply contains =ret=error message.
  • In case of successful login client can start to issue commands.

Tags

  • It is possible to run several commands simultaneously, without waiting for previous one to complete. If API client is doing this and needs to differentiate command responses, it can use 'tag' API parameter in command sentences.
  • If you include 'tag' parameter with non-empty value in command sentence, then 'tag' parameter with exactly the same value will be included in all responses generated by this command.
  • If you do not include 'tag' parameter or it's value is empty, then all responses for this command will not have 'tag' parameter.

Command description

  • /cancel
    • optional argument: =tag=tag of command to cancel, without it cancels all running commands
    • does not cancel itself
    • all canceled commands are interruped and in the usual case generate '!trap' and '!done' responses
    • please note that /cancel is separate command and can have it's own unique '.tag' parameter, that is not related to '=tag' argument of this command
  • listen
    • listen command is avaliable where console print command is available, but it does not have expected effect everywhere (i.e. may not work)
    • !re sentences are generated as something changes in particular item list
    • when item is deleted or dissapears in any other way, the '!re' sentence includes value '=.dead=yes'
    • This command does not terminate. To terminate it use /cancel command.
  • getall
    • getall command is available where console print command is available
    • replies contain =.id=Item internal number property.

Command examples

/system/package/getall

Template:Bapi Template:Apic Template:Apic Template:Apis Template:Apis Template:Apis Template:Apis Template:Apis Template:Apis Template:Apis Template:Apis Template:Apis Template:Apis Template:Apis Template:Apis Template:Apis Template:Apis Template:Apis Template:Apis |- | ... more !re sentences ... |- Template:Apis Template:Apis Template:Apis Template:Apis Template:Apis Template:Apis Template:Apis Template:Apis Template:Apis Template:Apis Template:Eapi

/user/active/listen

Template:Bapi Template:Apic Template:Apic Template:Apis Template:Apis Template:Apis Template:Apis Template:Apis Template:Apis Template:Apis Template:Apis Template:Apis Template:Apis Template:Apis Template:Apis |- | ... more !re sentences ... |- Template:Eapi

/cancel, simultaneous commands

Template:Bapi Template:Apic Template:Apic Template:Apis Template:Apis Template:Apis Template:Apic Template:Apic Template:Apic Template:Apic Template:Apis Template:Apis Template:Apih Template:Apic Template:Apic Template:Apic Template:Apih Template:Apic Template:Apic Template:Apic Template:Apic Template:Apic Template:Apih Template:Apis Template:Apis Template:Apis Template:Apih Template:Apic Template:Apic Template:Apic Template:Apic Template:Apic Template:Apih Template:Apis Template:Apis Template:Apis Template:Apis Template:Apis Template:Apis Template:Apis Template:Apis Template:Apis Template:Apis Template:Apih Template:Apis Template:Apis Template:Apis Template:Apih Template:Apic Template:Apic Template:Apic Template:Apih Template:Apis Template:Apis Template:Apis Template:Apis Template:Apis Template:Apis Template:Apis Template:Apis Template:Apis Template:Apis Template:Apih Template:Apis Template:Apis Template:Apis Template:Apis Template:Apis Template:Apis Template:Apis Template:Apis Template:Apis Template:Apis Template:Apis Template:Apis Template:Apis Template:Apis Template:Apis Template:Apis Template:Apis Template:Apis Template:Apis Template:Apis Template:Apih Template:Apis Template:Apis Template:Apis Template:Apih Template:Apic Template:Apic Template:Apic Template:Apic Template:Apih Template:Apis Template:Apis Template:Apis Template:Apis Template:Apis Template:Apih Template:Apis Template:Apis Template:Apis Template:Apih Template:Apis Template:Apis Template:Apis Template:Eapi

Example client

  • this is simple API client in Python
  • usage: api.py ip-address username password
  • after that type words from keyboard, terminating them with newline
  • Since empty word terminates sentence, you should press enter twice after last word before sentence will be sent to router.
#!/usr/bin/python

import sys, posix, time, md5, binascii, socket, select

class ApiRos:
    "Routeros api"
    def __init__(self, sk):
        self.sk = sk
        self.currenttag = 0
        
    def login(self, username, pwd):
        for repl, attrs in self.talk(["/login"]):
            chal = binascii.unhexlify(attrs['=ret'])
        md = md5.new()
        md.update('\x00')
        md.update(pwd)
        md.update(chal)
        self.talk(["/login", "=name=" + username,
                   "=response=00" + binascii.hexlify(md.digest())])

    def talk(self, words):
        if self.writeSentence(words) == 0: return
        r = []
        while 1:
            i = self.readSentence();
            if len(i) == 0: continue
            reply = i[0]
            attrs = {}
            for w in i[1:]:
                j = w.find('=', 1)
                if (j == -1):
                    attrs[w] = ''
                else:
                    attrs[w[:j]] = w[j+1:]
            r.append((reply, attrs))
            if reply == '!done': return r

    def writeSentence(self, words):
        ret = 0
        for w in words:
            self.writeWord(w)
            ret += 1
        self.writeWord('')
        return ret

    def readSentence(self):
        r = []
        while 1:
            w = self.readWord()
            if w == '': return r
            r.append(w)
            
    def writeWord(self, w):
        print "<<< " + w
        self.writeLen(len(w))
        self.writeStr(w)

    def readWord(self):
        ret = self.readStr(self.readLen())
        print ">>> " + ret
        return ret

    def writeLen(self, l):
        if l < 0x80:
            self.writeStr(chr(l))
        elif l < 0x4000:
            l |= 0x8000
            self.writeStr(chr((l >> 8) & 0xFF))
            self.writeStr(chr(l & 0xFF))
        elif l < 0x200000:
            l |= 0xC00000
            self.writeStr(chr((l >> 16) & 0xFF))
            self.writeStr(chr((l >> 8) & 0xFF))
            self.writeStr(chr(l & 0xFF))
        elif l < 0x10000000:        
            l |= 0xE0000000         
            self.writeStr(chr((l >> 24) & 0xFF))
            self.writeStr(chr((l >> 16) & 0xFF))
            self.writeStr(chr((l >> 8) & 0xFF))
            self.writeStr(chr(l & 0xFF))
        else:                       
            self.writeStr(chr(0xF0))
            self.writeStr(chr((l >> 24) & 0xFF))
            self.writeStr(chr((l >> 16) & 0xFF))
            self.writeStr(chr((l >> 8) & 0xFF))
            self.writeStr(chr(l & 0xFF))

    def readLen(self):              
        c = ord(self.readStr(1))    
        if (c & 0x80) == 0x00:      
            pass                    
        elif (c & 0xC0) == 0x80:    
            c &= ~0xC0              
            c <<= 8                 
            c += ord(self.readStr(1))    
        elif (c & 0xE0) == 0xC0:    
            c &= ~0xE0              
            c <<= 8                 
            c += ord(self.readStr(1))    
            c <<= 8                 
            c += ord(self.readStr(1))    
        elif (c & 0xF0) == 0xE0:    
            c &= ~0xF0              
            c <<= 8                 
            c += ord(self.readStr(1))    
            c <<= 8                 
            c += ord(self.readStr(1))    
            c <<= 8                 
            c += ord(self.readStr(1))    
        elif (c & 0xF8) == 0xF0:    
            c = ord(self.readStr(1))     
            c <<= 8                 
            c += ord(self.readStr(1))    
            c <<= 8                 
            c += ord(self.readStr(1))    
            c <<= 8                 
            c += ord(self.readStr(1))    
        return c                    

    def writeStr(self, str):        
        n = 0;                      
        while n < len(str):         
            r = self.sk.send(str[n:])
            if r == 0: raise RuntimeError, "connection closed by remote end"
            n += r                  

    def readStr(self, length):      
        ret = ''                    
        while len(ret) < length:    
            s = self.sk.recv(length - len(ret))
            if s == '': raise RuntimeError, "connection closed by remote end"
            ret += s
        return ret

def main():
    s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
    s.connect((sys.argv[1], 8728))  
    apiros = ApiRos(s);             
    apiros.login(sys.argv[2], sys.argv[3]);

    inputsentence = []

    while 1:
        r = select.select([s, sys.stdin], [], [], None)
        if s in r[0]:
            # something to read in socket, read sentence
            x = apiros.readSentence()

        if sys.stdin in r[0]:
            # read line from input and strip off newline
            l = sys.stdin.readline()
            l = l[:-1]

            # if empty line, send sentence and start with new
            # otherwise append to input sentence
            if l == '':
                apiros.writeSentence(inputsentence)
                inputsentence = []
            else:
                inputsentence.append(l)

if __name__ == '__main__':
    main()

Example run:

debian@localhost:~/api-test$ ./api.py 10.0.0.1 admin ''
<<< /login
<<< 
>>> !done
>>> =ret=93b438ec9b80057c06dd9fe67d56aa9a
>>> 
<<< /login
<<< =name=admin
<<< =response=00e134102a9d330dd7b1849fedfea3cb57
<<< 
>>> !done
>>> 
/user/getall

<<< /user/getall
<<< 
>>> !re
>>> =.id=*1
>>> =disabled=no
>>> =name=admin
>>> =group=full
>>> =address=0.0.0.0/0
>>> =netmask=0.0.0.0
>>> 
>>> !done
>>>