Skip to content

Commit

Permalink
reverted back to customized fork of librouteros
Browse files Browse the repository at this point in the history
  • Loading branch information
tomaae committed Mar 18, 2020
1 parent 1010a8a commit 0a58db4
Show file tree
Hide file tree
Showing 8 changed files with 621 additions and 29 deletions.
60 changes: 60 additions & 0 deletions custom_components/mikrotik_router/librouteros_custom/__init__.py
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 custom_components/mikrotik_router/librouteros_custom/api.py
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,
))
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 custom_components/mikrotik_router/librouteros_custom/exceptions.py
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 custom_components/mikrotik_router/librouteros_custom/login.py
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}))
Loading

0 comments on commit 0a58db4

Please sign in to comment.