-
-
Notifications
You must be signed in to change notification settings - Fork 63
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
reverted back to customized fork of librouteros
- Loading branch information
Showing
8 changed files
with
621 additions
and
29 deletions.
There are no files selected for viewing
60 changes: 60 additions & 0 deletions
60
custom_components/mikrotik_router/librouteros_custom/__init__.py
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,60 @@ | ||
# -*- coding: UTF-8 -*- | ||
|
||
from socket import create_connection | ||
from collections import ChainMap | ||
|
||
from .exceptions import ( | ||
ConnectionClosed, | ||
FatalError, | ||
) | ||
from .connections import SocketTransport | ||
from .protocol import ApiProtocol | ||
from .login import ( | ||
plain, | ||
token, | ||
) | ||
from .api import Api | ||
|
||
DEFAULTS = { | ||
'timeout': 10, | ||
'port': 8728, | ||
'saddr': '', | ||
'subclass': Api, | ||
'encoding': 'ASCII', | ||
'ssl_wrapper': lambda sock: sock, | ||
'login_method': plain, | ||
} | ||
|
||
|
||
def connect(host, username, password, **kwargs): | ||
""" | ||
Connect and login to routeros device. | ||
Upon success return a Api class. | ||
:param host: Hostname to connecto to. May be ipv4,ipv6,FQDN. | ||
:param username: Username to login with. | ||
:param password: Password to login with. Only ASCII characters allowed. | ||
:param timeout: Socket timeout. Defaults to 10. | ||
:param port: Destination port to be used. Defaults to 8728. | ||
:param saddr: Source address to bind to. | ||
:param subclass: Subclass of Api class. Defaults to Api class from library. | ||
:param ssl_wrapper: Callable (e.g. ssl.SSLContext instance) to wrap socket with. | ||
:param login_method: Callable with login method. | ||
""" | ||
arguments = ChainMap(kwargs, DEFAULTS) | ||
transport = create_transport(host, **arguments) | ||
protocol = ApiProtocol(transport=transport, encoding=arguments['encoding']) | ||
api = arguments['subclass'](protocol=protocol) | ||
|
||
try: | ||
arguments['login_method'](api=api, username=username, password=password) | ||
return api | ||
except (ConnectionClosed, FatalError): | ||
transport.close() | ||
raise | ||
|
||
|
||
def create_transport(host, **kwargs): | ||
sock = create_connection((host, kwargs['port']), kwargs['timeout'], (kwargs['saddr'], 0)) | ||
sock = kwargs['ssl_wrapper'](sock) | ||
return SocketTransport(sock=sock) |
136 changes: 136 additions & 0 deletions
136
custom_components/mikrotik_router/librouteros_custom/api.py
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,136 @@ | ||
# -*- coding: UTF-8 -*- | ||
|
||
from posixpath import join as pjoin | ||
|
||
from .exceptions import TrapError, MultiTrapError | ||
from .protocol import ( | ||
compose_word, | ||
parse_word, | ||
) | ||
from .query import Query | ||
|
||
|
||
class Api: | ||
|
||
def __init__(self, protocol): | ||
self.protocol = protocol | ||
|
||
def __call__(self, cmd, **kwargs): | ||
""" | ||
Call Api with given command. | ||
Yield each row. | ||
:param cmd: Command word. eg. /ip/address/print | ||
:param kwargs: Dictionary with optional arguments. | ||
""" | ||
words = (compose_word(key, value) for key, value in kwargs.items()) | ||
self.protocol.writeSentence(cmd, *words) | ||
yield from self.readResponse() | ||
|
||
def rawCmd(self, cmd, *words): | ||
""" | ||
Call Api with given command and raw words. | ||
End user is responsible to properly format each api word argument. | ||
:param cmd: Command word. eg. /ip/address/print | ||
:param args: Iterable with optional plain api arguments. | ||
""" | ||
self.protocol.writeSentence(cmd, *words) | ||
yield from self.readResponse() | ||
|
||
def readSentence(self): | ||
""" | ||
Read one sentence and parse words. | ||
:returns: Reply word, dict with attribute words. | ||
""" | ||
reply_word, words = self.protocol.readSentence() | ||
words = dict(parse_word(word) for word in words) | ||
return reply_word, words | ||
|
||
def readResponse(self): | ||
""" | ||
Yield each sentence untill !done is received. | ||
:throws TrapError: If one !trap is received. | ||
:throws MultiTrapError: If > 1 !trap is received. | ||
""" | ||
traps = [] | ||
reply_word = None | ||
while reply_word != '!done': | ||
reply_word, words = self.readSentence() | ||
if reply_word == '!trap': | ||
traps.append(TrapError(**words)) | ||
elif reply_word in ('!re', '!done') and words: | ||
yield words | ||
|
||
if len(traps) > 1: | ||
raise MultiTrapError(*traps) | ||
if len(traps) == 1: | ||
raise traps[0] | ||
|
||
def close(self): | ||
self.protocol.close() | ||
|
||
def path(self, *path): | ||
return Path( | ||
path='', | ||
api=self, | ||
).join(*path) | ||
|
||
|
||
class Path: | ||
"""Represents absolute command path.""" | ||
|
||
def __init__(self, path, api): | ||
self.path = path | ||
self.api = api | ||
|
||
def select(self, key, *other): | ||
keys = (key, ) + other | ||
return Query(path=self, keys=keys, api=self.api) | ||
|
||
def __str__(self): | ||
return self.path | ||
|
||
def __repr__(self): | ||
return "<{module}.{cls} {path!r}>".format( | ||
module=self.__class__.__module__, | ||
cls=self.__class__.__name__, | ||
path=self.path, | ||
) | ||
|
||
def __iter__(self): | ||
yield from self('print') | ||
|
||
def __call__(self, cmd, **kwargs): | ||
yield from self.api( | ||
self.join(cmd).path, | ||
**kwargs, | ||
) | ||
|
||
def join(self, *path): | ||
"""Join current path with one or more path strings.""" | ||
return Path( | ||
api=self.api, | ||
path=pjoin('/', self.path, *path).rstrip('/'), | ||
) | ||
|
||
def remove(self, *ids): | ||
ids = ','.join(ids) | ||
tuple(self( | ||
'remove', | ||
**{'.id': ids}, | ||
)) | ||
|
||
def add(self, **kwargs): | ||
ret = self( | ||
'add', | ||
**kwargs, | ||
) | ||
return tuple(ret)[0]['ret'] | ||
|
||
def update(self, **kwargs): | ||
tuple(self( | ||
'set', | ||
**kwargs, | ||
)) |
37 changes: 37 additions & 0 deletions
37
custom_components/mikrotik_router/librouteros_custom/connections.py
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,37 @@ | ||
# -*- coding: UTF-8 -*- | ||
|
||
from .exceptions import ConnectionClosed | ||
|
||
|
||
class SocketTransport: | ||
|
||
def __init__(self, sock): | ||
self.sock = sock | ||
|
||
def write(self, data): | ||
""" | ||
Write given bytes to socket. Loop as long as every byte in | ||
string is written unless exception is raised. | ||
""" | ||
self.sock.sendall(data) | ||
|
||
def read(self, length): | ||
""" | ||
Read as many bytes from socket as specified in length. | ||
Loop as long as every byte is read unless exception is raised. | ||
""" | ||
data = bytearray() | ||
while len(data) != length: | ||
tmp = None | ||
try: | ||
tmp = self.sock.recv((length - len(data))) | ||
except: | ||
raise ConnectionClosed('Socket recv failed.') | ||
|
||
data += tmp | ||
if not data: | ||
raise ConnectionClosed('Connection unexpectedly closed.') | ||
return data | ||
|
||
def close(self): | ||
self.sock.close() |
52 changes: 52 additions & 0 deletions
52
custom_components/mikrotik_router/librouteros_custom/exceptions.py
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,52 @@ | ||
# -*- coding: UTF-8 -*- | ||
|
||
|
||
class LibRouterosError(Exception): | ||
"""Base exception for all other.""" | ||
|
||
|
||
class ConnectionClosed(LibRouterosError): | ||
"""Raised when connection have been closed.""" | ||
|
||
|
||
class ProtocolError(LibRouterosError): | ||
"""Raised when e.g. encoding/decoding fails.""" | ||
|
||
|
||
class FatalError(ProtocolError): | ||
"""Exception raised when !fatal is received.""" | ||
|
||
|
||
class TrapError(ProtocolError): | ||
""" | ||
Exception raised when !trap is received. | ||
:param int category: Optional integer representing category. | ||
:param str message: Error message. | ||
""" | ||
|
||
def __init__(self, message, category=None): | ||
self.category = category | ||
self.message = message | ||
super().__init__() | ||
|
||
def __str__(self): | ||
return str(self.message.replace('\r\n', ',')) | ||
|
||
def __repr__(self): | ||
return '{}({!r})'.format(self.__class__.__name__, str(self)) | ||
|
||
|
||
class MultiTrapError(ProtocolError): | ||
""" | ||
Exception raised when multiple !trap words have been received in one response. | ||
:param traps: TrapError instances. | ||
""" | ||
|
||
def __init__(self, *traps): | ||
self.traps = traps | ||
super().__init__() | ||
|
||
def __str__(self): | ||
return ', '.join(str(trap) for trap in self.traps) |
26 changes: 26 additions & 0 deletions
26
custom_components/mikrotik_router/librouteros_custom/login.py
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,26 @@ | ||
from binascii import unhexlify, hexlify | ||
from hashlib import md5 | ||
|
||
|
||
def encode_password(token, password): | ||
#pylint: disable=redefined-outer-name | ||
token = token.encode('ascii', 'strict') | ||
token = unhexlify(token) | ||
password = password.encode('ascii', 'strict') | ||
hasher = md5() | ||
hasher.update(b'\x00' + password + token) | ||
password = hexlify(hasher.digest()) | ||
return '00' + password.decode('ascii', 'strict') | ||
|
||
|
||
def token(api, username, password): | ||
"""Login using pre routeros 6.43 authorization method.""" | ||
sentence = api('/login') | ||
tok = tuple(sentence)[0]['ret'] | ||
encoded = encode_password(tok, password) | ||
tuple(api('/login', **{'name': username, 'response': encoded})) | ||
|
||
|
||
def plain(api, username, password): | ||
"""Login using post routeros 6.43 authorization method.""" | ||
tuple(api('/login', **{'name': username, 'password': password})) |
Oops, something went wrong.