Manual:API: Difference between revisions
No edit summary |
(API intro) |
||
(10 intermediate revisions by the same user not shown) | |||
Line 1: | Line 1: | ||
This document describes the operation of MikroTik RouterOS API for RouterOS3. | 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. | ||
== Enabling and Connecting == | == Enabling and Connecting == | ||
* Default API port is 8728. By default API is disabled | * Default API port is 8728. By default API is disabled | ||
* Enable API | * Enable API | ||
/ip service enable api | |||
* Now you can initiate TCP connection to the API port of the router | * Now you can initiate TCP connection to the API port of the router | ||
Line 13: | Line 15: | ||
* 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: | * Lenght is encoded as follows: | ||
{| border="1" | {| border="1" | ||
!Value of length !! Number of bytes !! Encoding | !Value of length !! Number of bytes !! Encoding | ||
Line 19: | Line 20: | ||
| 0 <= len <= 0x7F | | 0 <= len <= 0x7F | ||
| 1 | | 1 | ||
| | | len, lowest byte | ||
|- | |- | ||
| 0x80 <= len <= 0x3FFF | | 0x80 <= len <= 0x3FFF | ||
| 2 | | 2 | ||
| < | | <nowiki>len | 0x8000, two lower bytes</nowiki> | ||
|- | |- | ||
| 0x4000 <= len <= 0x1FFFFF | | 0x4000 <= len <= 0x1FFFFF | ||
| 3 | | 3 | ||
| < | | <nowiki>len | 0xC00000, three lower bytes</nowiki> | ||
|- | |- | ||
| 0x200000 <= len <= 0xFFFFFFF | | 0x200000 <= len <= 0xFFFFFFF | ||
| 4 | | 4 | ||
| < | | <nowiki>len | 0xE0000000</nowiki> | ||
|- | |- | ||
| len >= 0x10000000 | | len >= 0x10000000 | ||
| 5 | | 5 | ||
| | | 0xF0 and len as four bytes | ||
|- | |- | ||
|} | |} | ||
* Although this scheme allows encoding of length up to '''0x7FFFFFFFF''', only four byte length is supported. | * 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). | * 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. | * 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. | |||
Line 51: | Line 52: | ||
* Commands | * Commands | ||
** First word is name of command. Examples: | ** First word is name of command. Examples: | ||
<pre> | |||
/login | /login | ||
/ip/address/getall | /ip/address/getall | ||
Line 56: | Line 58: | ||
/interface/vlan/remove | /interface/vlan/remove | ||
/system/reboot | /system/reboot | ||
** Names of commands closely follow console, with spaces replaced by '/'. There are commands that are specific to API, like | </pre> | ||
** Names of commands closely follow console, with spaces replaced by '/'. There are commands that are specific to API, like <tt>getall</tt> or <tt>login</tt>. | |||
** Name of command should begin with '/'. | ** Name of command should begin with '/'. | ||
** Next, command arguments can be specified. Examples: | ** Next, command arguments can be specified. Examples: | ||
Line 66: | Line 69: | ||
** 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). | ||
** Last reply for every command is reply that has first word | ** Last reply for every command is reply that has first word <tt>!done</tt>. | ||
** Errors and exceptional conditions begin with | ** Errors and exceptional conditions begin with <tt>!trap</tt>. | ||
** Data replies begin with | ** Data replies begin with <tt>!re</tt> | ||
== Initial login == | == Initial login == | ||
<tt> | |||
{| style="width: 500px" | {| style="width: 500px" | ||
{{apic|/login}} | |||
| /login | {{apic|}} | ||
| | {{apis|!done}} | ||
| !done | {{apis|1==ret=ebddd18303a54111e2dea05a92ab46b4}} | ||
| | {{apis|}} | ||
{{apic|/login}} | |||
| | {{apic|1==name=admin}} | ||
| /login | {{apic|1==response=001ea726ed53ae38520c8334f82d44c9f2}} | ||
| | {{apic|}} | ||
{{apis|!done}} | |||
| | {{apis|}} | ||
| | |||
| !done | |||
|} | |} | ||
</tt> | |||
* First, clients sends <tt>/login</tt> command. | |||
* Note that each command and response ends with an empty word. | |||
* Reply contains <tt>=ret=''challenge''</tt> argument. | |||
* Client sends second <tt>/login</tt> command, with <tt>=name=''username''</tt> and <tt>=response=''response''</tt>. | |||
* In case of error, reply contains <tt>=ret=''error message''</tt>. | |||
* In case of successful login client can start to issue commands. | |||
== Command examples == | == Command examples == | ||
=== <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}} | |||
{{apic|/system/package/getall}} | |||
{{apic|}} | |||
{{apis|!re}} | |||
{{apis|1==.id=*5802}} | |||
{{apis|1==disabled=no}} | |||
{{apis|1==name=routeros-x86}} | |||
{{apis|1==version=3.0beta2}} | |||
{{apis|1==build-time=oct/18/2006 16:24:41}} | |||
{{apis|1==scheduled=}} | |||
{{apis|}} | |||
{{apis|!re}} | |||
{{apis|1==.id=*5805}} | |||
{{apis|1==disabled=no}} | |||
{{apis|1==name=system}} | |||
{{apis|1==version=3.0beta2}} | |||
{{apis|1==build-time=oct/18/2006 17:20:46}} | |||
{{apis|1==scheduled=}} | |||
{{apis|}} | |||
|- | |||
| ... more !re sentences ... | |||
|- | |||
{{apis|!re}} | |||
{{apis|1==.id=*5902}} | |||
{{apis|1==disabled=no}} | |||
{{apis|1==name=advanced-tools}} | |||
{{apis|1==version=3.0beta2}} | |||
{{apis|1==build-time=oct/18/2006 17:20:49}} | |||
{{apis|1==scheduled=}} | |||
{{apis|}} | |||
{{apis|!done}} | |||
{{apis|}} | |||
{{eapi}} | |||
=== <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}} | |||
/ | {{apic|/user/active/listen}} | ||
{{apic|}} | |||
{{apis|!re}} | |||
{{apis|1==.id=*68}} | |||
{{apis|1==radius=no}} | |||
{{apis|1==when=oct/24/2006 08:40:42}} | |||
{{apis|1==name=admin}} | |||
{{apis|1==address=0.0.0.0}} | |||
{{apis|1==via=console}} | |||
{{apis|}} | |||
{{apis|!re}} | |||
{{apis|1==.id=*68}} | |||
{{apis|1==.dead=yes}} | |||
{{apis|}} | |||
|- | |||
| ... more !re sentences ... | |||
|- | |||
{{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 | |||
<pre> | |||
#!/usr/bin/python | |||
import sys, posix, time, md5, binascii, socket | |||
class ApiRos: | |||
=. | "Routeros api" | ||
= | def __init__(self, sk): | ||
= | self.sk = sk | ||
def login(self, username, pwd): | |||
= | for repl, attrs in self.say(["/login"]): | ||
= | chal = binascii.unhexlify(attrs['=ret']) | ||
md = md5.new() | |||
md.update('\x00') | |||
md.update(pwd) | |||
md.update(chal) | |||
None in self.say(["/login", "=name=" + username, "=response=00" + binascii.hexlify(md.digest())]) | |||
!done | def say(self, words): | ||
if self.writeWords(words) == 0: return | |||
while 1: | |||
i = self.readWords() | |||
repl = i.next() | |||
attrs = {} | |||
for w in i: | |||
j = w.find('=', 1) | |||
if (j == -1): | |||
attrs[w] = '' | |||
else: | |||
attrs[w[:j]] = w[j+1:] | |||
yield (repl, attrs) | |||
if repl == '!done': return | |||
def writeWords(self, words): | |||
ret = 0 | |||
for w in words: | |||
self.writeWord(w) | |||
ret += 1 | |||
self.writeWord('') | |||
return ret | |||
def readWords(self): | |||
while 1: | |||
w = self.readWord() | |||
if (w != ''): break | |||
while 1: | |||
yield w | |||
w = self.readWord() | |||
if w == '': return | |||
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 getParagraph(): | |||
for l in iter(sys.stdin.readline, '\n'): | |||
yield l[:-1] | |||
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]); | |||
while 1: | |||
for repl, attrs in apiros.say(getParagraph()): | |||
print repl | |||
print attrs | |||
print '' | |||
if __name__ == '__main__': | |||
main() | |||
</pre> | </pre> |
Revision as of 11:18, 19 February 2007
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.
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 lenght, followed by that many bytes of content.
- Words are grouped into sentences. End of sentence is terminated by zero length word.
- Lenght 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.
- Agrument value can be empty and can contain '='.
- 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
- 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.
Command examples
/system/package/getall
- Note that replies contain =.id=Item internal number property, that is not available in console. Names of properties that are API specific begin with dot.
- getall command is avaliable where console print command is available
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
- 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
- this command does not terminate
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
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
#!/usr/bin/python import sys, posix, time, md5, binascii, socket class ApiRos: "Routeros api" def __init__(self, sk): self.sk = sk def login(self, username, pwd): for repl, attrs in self.say(["/login"]): chal = binascii.unhexlify(attrs['=ret']) md = md5.new() md.update('\x00') md.update(pwd) md.update(chal) None in self.say(["/login", "=name=" + username, "=response=00" + binascii.hexlify(md.digest())]) def say(self, words): if self.writeWords(words) == 0: return while 1: i = self.readWords() repl = i.next() attrs = {} for w in i: j = w.find('=', 1) if (j == -1): attrs[w] = '' else: attrs[w[:j]] = w[j+1:] yield (repl, attrs) if repl == '!done': return def writeWords(self, words): ret = 0 for w in words: self.writeWord(w) ret += 1 self.writeWord('') return ret def readWords(self): while 1: w = self.readWord() if (w != ''): break while 1: yield w w = self.readWord() if w == '': return 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 getParagraph(): for l in iter(sys.stdin.readline, '\n'): yield l[:-1] 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]); while 1: for repl, attrs in apiros.say(getParagraph()): print repl print attrs print '' if __name__ == '__main__': main()