diff --git a/custom_components/mikrotik_router/librouteros_custom/__init__.py b/custom_components/mikrotik_router/librouteros_custom/__init__.py deleted file mode 100644 index b345228..0000000 --- a/custom_components/mikrotik_router/librouteros_custom/__init__.py +++ /dev/null @@ -1,60 +0,0 @@ -# -*- 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) diff --git a/custom_components/mikrotik_router/librouteros_custom/api.py b/custom_components/mikrotik_router/librouteros_custom/api.py deleted file mode 100644 index e8c4a17..0000000 --- a/custom_components/mikrotik_router/librouteros_custom/api.py +++ /dev/null @@ -1,136 +0,0 @@ -# -*- 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, - )) diff --git a/custom_components/mikrotik_router/librouteros_custom/connections.py b/custom_components/mikrotik_router/librouteros_custom/connections.py deleted file mode 100644 index 40cd66b..0000000 --- a/custom_components/mikrotik_router/librouteros_custom/connections.py +++ /dev/null @@ -1,37 +0,0 @@ -# -*- 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() diff --git a/custom_components/mikrotik_router/librouteros_custom/exceptions.py b/custom_components/mikrotik_router/librouteros_custom/exceptions.py deleted file mode 100644 index 9eea29b..0000000 --- a/custom_components/mikrotik_router/librouteros_custom/exceptions.py +++ /dev/null @@ -1,52 +0,0 @@ -# -*- 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) diff --git a/custom_components/mikrotik_router/librouteros_custom/login.py b/custom_components/mikrotik_router/librouteros_custom/login.py deleted file mode 100644 index c674cac..0000000 --- a/custom_components/mikrotik_router/librouteros_custom/login.py +++ /dev/null @@ -1,26 +0,0 @@ -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})) diff --git a/custom_components/mikrotik_router/librouteros_custom/protocol.py b/custom_components/mikrotik_router/librouteros_custom/protocol.py deleted file mode 100644 index 8c31a2b..0000000 --- a/custom_components/mikrotik_router/librouteros_custom/protocol.py +++ /dev/null @@ -1,209 +0,0 @@ -# -*- coding: UTF-8 -*- - -from struct import pack, unpack -from logging import getLogger, NullHandler - -from .exceptions import ( - ProtocolError, - FatalError, -) - -LOGGER = getLogger('librouteros') -LOGGER.addHandler(NullHandler()) - - -def parse_word(word): - """ - Split given attribute word to key, value pair. - - Values are casted to python equivalents. - - :param word: API word. - :returns: Key, value pair. - """ - mapping = {'yes': True, 'true': True, 'no': False, 'false': False} - _, key, value = word.split('=', 2) - try: - value = int(value) - except ValueError: - value = mapping.get(value, value) - return (key, value) - - -def cast_to_api(value): - """Cast python equivalent to API.""" - mapping = {True: 'yes', False: 'no'} - # this is necesary because 1 == True, 0 == False - if type(value) == int: - value = str(value) - else: - value = mapping.get(value, str(value)) - return value - - -def compose_word(key, value): - """ - Create a attribute word from key, value pair. - Values are casted to api equivalents. - """ - return '={}={}'.format(key, cast_to_api(value)) - - -class Encoder: - - def encodeSentence(self, *words): - """ - Encode given sentence in API format. - - :param words: Words to endoce. - :returns: Encoded sentence. - """ - encoded = map(self.encodeWord, words) - encoded = b''.join(encoded) - # append EOS (end of sentence) byte - encoded += b'\x00' - return encoded - - def encodeWord(self, word): - """ - Encode word in API format. - - :param word: Word to encode. - :returns: Encoded word. - """ - #pylint: disable=no-member - encoded_word = word.encode(encoding=self.encoding, errors='strict') - return Encoder.encodeLength(len(word)) + encoded_word - - @staticmethod - def encodeLength(length): - """ - Encode given length in mikrotik format. - - :param length: Integer < 268435456. - :returns: Encoded length. - """ - if length < 128: - ored_length = length - offset = -1 - elif length < 16384: - ored_length = length | 0x8000 - offset = -2 - elif length < 2097152: - ored_length = length | 0xC00000 - offset = -3 - elif length < 268435456: - ored_length = length | 0xE0000000 - offset = -4 - else: - raise ProtocolError('Unable to encode length of {}'.format(length)) - - return pack('!I', ored_length)[offset:] - - -class Decoder: - - @staticmethod - def determineLength(length): - """ - Given first read byte, determine how many more bytes - needs to be known in order to get fully encoded length. - - :param length: First read byte. - :return: How many bytes to read. - """ - integer = ord(length) - - #pylint: disable=no-else-return - if integer < 128: - return 0 - elif integer < 192: - return 1 - elif integer < 224: - return 2 - elif integer < 240: - return 3 - - raise ProtocolError('Unknown controll byte {}'.format(length)) - - @staticmethod - def decodeLength(length): - """ - Decode length based on given bytes. - - :param length: Bytes string to decode. - :return: Decoded length. - """ - bytes_length = len(length) - - if bytes_length < 2: - offset = b'\x00\x00\x00' - xor = 0 - elif bytes_length < 3: - offset = b'\x00\x00' - xor = 0x8000 - elif bytes_length < 4: - offset = b'\x00' - xor = 0xC00000 - elif bytes_length < 5: - offset = b'' - xor = 0xE0000000 - else: - raise ProtocolError('Unable to decode length of {}'.format(length)) - - decoded = unpack('!I', (offset + length))[0] - decoded ^= xor - return decoded - - -class ApiProtocol(Encoder, Decoder): - - def __init__(self, transport, encoding): - self.transport = transport - self.encoding = encoding - - @staticmethod - def log(direction_string, *sentence): - for word in sentence: - LOGGER.debug('{0} {1!r}'.format(direction_string, word)) - - LOGGER.debug('{0} EOS'.format(direction_string)) - - def writeSentence(self, cmd, *words): - """ - Write encoded sentence. - - :param cmd: Command word. - :param words: Aditional words. - """ - encoded = self.encodeSentence(cmd, *words) - self.log('<---', cmd, *words) - self.transport.write(encoded) - - def readSentence(self): - """ - Read every word untill empty word (NULL byte) is received. - - :return: Reply word, tuple with read words. - """ - sentence = tuple(word for word in iter(self.readWord, '')) - self.log('--->', *sentence) - reply_word, words = sentence[0], sentence[1:] - if reply_word == '!fatal': - self.transport.close() - raise FatalError(words[0]) - return reply_word, words - - def readWord(self): - byte = self.transport.read(1) - # Early return check for null byte - if byte == b'\x00': - return '' - to_read = self.determineLength(byte) - byte += self.transport.read(to_read) - length = self.decodeLength(byte) - word = self.transport.read(length) - return word.decode(encoding=self.encoding, errors='strict') - - def close(self): - self.transport.close() diff --git a/custom_components/mikrotik_router/librouteros_custom/query.py b/custom_components/mikrotik_router/librouteros_custom/query.py deleted file mode 100644 index c3f9bf2..0000000 --- a/custom_components/mikrotik_router/librouteros_custom/query.py +++ /dev/null @@ -1,64 +0,0 @@ -# -*- coding: UTF-8 -*- -from itertools import chain -from .protocol import ( - cast_to_api, -) - - -class Key: - - def __init__(self, name): - self.name = name - - def __eq__(self, other): - yield '?={}={}'.format(self, cast_to_api(other)) - - def __ne__(self, other): - yield from self == other - yield '?#!' - - def __lt__(self, other): - yield '?<{}={}'.format(self, cast_to_api(other)) - - def __gt__(self, other): - yield '?>{}={}'.format(self, cast_to_api(other)) - - def __str__(self): - return str(self.name) - - -class Query: - - def __init__(self, path, keys, api): - self.path = path - self.keys = keys - self.api = api - self.query = tuple() - - def where(self, *args): - self.query = tuple(chain.from_iterable(args)) - return self - - def __iter__(self): - keys = ','.join(str(key) for key in self.keys) - keys = '=.proplist={}'.format(keys) - cmd = str(self.path.join('print')) - return iter(self.api.rawCmd(cmd, keys, *self.query)) - - -def And(left, right, *rest): - #pylint: disable=invalid-name - yield from left - yield from right - yield from chain.from_iterable(rest) - yield '?#&' - yield from ('?#&', ) * len(rest) - - -def Or(left, right, *rest): - #pylint: disable=invalid-name - yield from left - yield from right - yield from chain.from_iterable(rest) - yield '?#|' - yield from ('?#|', ) * len(rest) diff --git a/custom_components/mikrotik_router/manifest.json b/custom_components/mikrotik_router/manifest.json index 1909aab..e9bfac1 100644 --- a/custom_components/mikrotik_router/manifest.json +++ b/custom_components/mikrotik_router/manifest.json @@ -4,7 +4,9 @@ "config_flow": true, "documentation": "https://github.com/tomaae/homeassistant-mikrotik_router", "dependencies": [], - "requirements": [], + "requirements": [ + "librouteros==3.0.0" + ], "codeowners": [ "@tomaae" ] diff --git a/custom_components/mikrotik_router/mikrotikapi.py b/custom_components/mikrotik_router/mikrotikapi.py index b52c314..73056ce 100644 --- a/custom_components/mikrotik_router/mikrotikapi.py +++ b/custom_components/mikrotik_router/mikrotikapi.py @@ -2,10 +2,7 @@ import ssl import logging -import os -import sys import time -import importlib from threading import Lock from .exceptions import ApiEntryNotFound from .const import ( @@ -13,12 +10,7 @@ from .const import ( DEFAULT_ENCODING, ) -MODULE_PATH = os.path.join(os.path.dirname(__file__), "librouteros_custom", "__init__.py") -MODULE_NAME = "librouteros_custom" -spec = importlib.util.spec_from_file_location(MODULE_NAME, MODULE_PATH) -librouteros_custom = importlib.util.module_from_spec(spec) -sys.modules[spec.name] = librouteros_custom -spec.loader.exec_module(librouteros_custom) +import librouteros _LOGGER = logging.getLogger(__name__) @@ -84,13 +76,13 @@ class MikrotikAPI: kwargs["ssl_wrapper"] = self._ssl_wrapper self.lock.acquire() try: - self._connection = librouteros_custom.connect(self._host, self._username, self._password, **kwargs) + self._connection = librouteros.connect(self._host, self._username, self._password, **kwargs) except ( - librouteros_custom.exceptions.TrapError, - librouteros_custom.exceptions.MultiTrapError, - librouteros_custom.exceptions.ConnectionClosed, - librouteros_custom.exceptions.ProtocolError, - librouteros_custom.exceptions.FatalError, + librouteros.exceptions.TrapError, + librouteros.exceptions.MultiTrapError, + librouteros.exceptions.ConnectionClosed, + librouteros.exceptions.ProtocolError, + librouteros.exceptions.FatalError, ssl.SSLError, BrokenPipeError, OSError @@ -147,16 +139,16 @@ class MikrotikAPI: try: response = self._connection.path(path) _LOGGER.debug("API response (%s): %s", path, response) - except librouteros_custom.exceptions.ConnectionClosed: + except librouteros.exceptions.ConnectionClosed: _LOGGER.error("Mikrotik %s connection closed", self._host) self.disconnect() self.lock.release() return None except ( - librouteros_custom.exceptions.TrapError, - librouteros_custom.exceptions.MultiTrapError, - librouteros_custom.exceptions.ProtocolError, - librouteros_custom.exceptions.FatalError, + librouteros.exceptions.TrapError, + librouteros.exceptions.MultiTrapError, + librouteros.exceptions.ProtocolError, + librouteros.exceptions.FatalError, ssl.SSLError, BrokenPipeError, OSError, @@ -174,7 +166,7 @@ class MikrotikAPI: try: tuple(response) - except librouteros_custom.exceptions.ConnectionClosed as api_error: + except librouteros.exceptions.ConnectionClosed as api_error: _LOGGER.error("Mikrotik %s error while path %s", self._host, api_error) self.disconnect() self.lock.release() @@ -221,16 +213,16 @@ class MikrotikAPI: self.lock.acquire() try: response.update(**params) - except librouteros_custom.exceptions.ConnectionClosed: + except librouteros.exceptions.ConnectionClosed: _LOGGER.error("Mikrotik %s connection closed", self._host) self.disconnect() self.lock.release() return False except ( - librouteros_custom.exceptions.TrapError, - librouteros_custom.exceptions.MultiTrapError, - librouteros_custom.exceptions.ProtocolError, - librouteros_custom.exceptions.FatalError, + librouteros.exceptions.TrapError, + librouteros.exceptions.MultiTrapError, + librouteros.exceptions.ProtocolError, + librouteros.exceptions.FatalError, ssl.SSLError, BrokenPipeError, OSError, @@ -282,16 +274,16 @@ class MikrotikAPI: try: run = response('run', **{'.id': tmp['.id']}) tuple(run) - except librouteros_custom.exceptions.ConnectionClosed: + except librouteros.exceptions.ConnectionClosed: _LOGGER.error("Mikrotik %s connection closed", self._host) self.disconnect() self.lock.release() return False except ( - librouteros_custom.exceptions.TrapError, - librouteros_custom.exceptions.MultiTrapError, - librouteros_custom.exceptions.ProtocolError, - librouteros_custom.exceptions.FatalError, + librouteros.exceptions.TrapError, + librouteros.exceptions.MultiTrapError, + librouteros.exceptions.ProtocolError, + librouteros.exceptions.FatalError, ssl.SSLError, BrokenPipeError, OSError, @@ -336,16 +328,16 @@ class MikrotikAPI: try: traffic = response('monitor-traffic', **args) _LOGGER.debug("API response (%s): %s", "/interface/monitor-traffic", traffic) - except librouteros_custom.exceptions.ConnectionClosed: + except librouteros.exceptions.ConnectionClosed: _LOGGER.error("Mikrotik %s connection closed", self._host) self.disconnect() self.lock.release() return None except ( - librouteros_custom.exceptions.TrapError, - librouteros_custom.exceptions.MultiTrapError, - librouteros_custom.exceptions.ProtocolError, - librouteros_custom.exceptions.FatalError, + librouteros.exceptions.TrapError, + librouteros.exceptions.MultiTrapError, + librouteros.exceptions.ProtocolError, + librouteros.exceptions.FatalError, ssl.SSLError, BrokenPipeError, OSError,