Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,13 +4,13 @@ Changelog for RouterOS-api
## 0.19.1 (unreleased)
----------------------

- Nothing changed yet.
- Fix decoding non utf-8 characters. Allow setting fallback encoding.


## 0.19.0 (2025-03-06)
----------------------

- Add support for `!empty` reply word ([#103](https://github.com/socialwifi/RouterOS-api/pull/103))
- Add support for `!empty` reply word ([#103](https://github.com/socialwifi/RouterOS-api/pull/103)).


## 0.18.1 (2024-05-28)
Expand Down
29 changes: 28 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -19,9 +19,10 @@ Python API to RouterBoard devices produced by [MikroTik](https://mikrotik.com/)

import routeros_api

connection = routeros_api.RouterOsApiPool('IP', username='admin', password='')
connection = routeros_api.RouterOsApiPool('IP', username='admin', password='', plaintext_login=True)
api = connection.get_api()
```
Use `plaintext_login=True` option when connecting to RouterOS version 6.43 and newer.

#### Connect Options

Expand All @@ -31,6 +32,7 @@ routeros_api.RouterOsApiPool(
username='admin',
password='',
port=8728,
plaintext_login=True,
use_ssl=False,
ssl_verify=True,
ssl_verify_hostname=True,
Expand Down Expand Up @@ -90,6 +92,31 @@ It is highly recommended only to use this option with SSL enabled.
routeros_api.RouterOsApiPool(host, username='admin', password='', plaintext_login=True)
```

#### Handling non UTF-8 characters

The API does not assume any particular encoding, so non utf-8 characters will
not be shown correctly.
A default encoding can be specified if needed. For some cases the `latin-1`
encoding will be the best one to use, for others - `windows-1250` will work
better.
Ref. https://forum.mikrotik.com/viewtopic.php?t=106053#p528460

It is possible to use specific encoding by defining custom default structure
like this:

```python
import collections
import routeros_api
from routeros_api.api_structure import StringField

connection = routeros_api.RouterOsApiPool('ip', username='admin', password='password', plaintext_login=True)
api = connection.get_api()
# This part here is important:
default_structure = collections.defaultdict(lambda: StringField(encoding='windows-1250'))
api.get_resource('/system/identity', structure=default_structure).get()
```


### Execute Commands

Call this with a resource and parameters as name/value pairs.
Expand Down
3 changes: 2 additions & 1 deletion routeros_api/api.py
Original file line number Diff line number Diff line change
Expand Up @@ -103,7 +103,8 @@ def login(self, login, password, plaintext_login):
'login', {'name': login.encode(), 'response': hashed})

def get_resource(self, path, structure=None):
structure = structure or api_structure.default_structure
if structure is None:
structure = api_structure.default_structure
return resource.RouterOsResource(self.communicator, path, structure)

def get_binary_resource(self, path):
Expand Down
7 changes: 5 additions & 2 deletions routeros_api/api_structure.py
Original file line number Diff line number Diff line change
Expand Up @@ -24,11 +24,14 @@ def get_python_value(self, bytes):


class StringField(Field):
def __init__(self, encoding='utf-8'):
self.encoding = encoding

def get_mikrotik_value(self, string):
return string.encode()
return string.encode(encoding=self.encoding, errors='backslashreplace')

def get_python_value(self, bytes):
return bytes.decode()
return bytes.decode(encoding=self.encoding, errors='backslashreplace')


class BytesField(Field):
Expand Down
35 changes: 35 additions & 0 deletions tests/test_resource.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
from routeros_api import resource
from routeros_api.api_communicator import base

STRING_STRUCTURE = {'string': structure.StringField()}
BYTES_STRUCTURE = {'bytes': structure.BytesField()}
BOOLEAN_STRUCTURE = {'boolean': structure.BooleanField()}

Expand All @@ -32,6 +33,40 @@ def test_unknown_resource_set(self):
'/unknown/', 'set', arguments={'x': b'y'}, queries={},
additional_queries=())

def test_string_resource_get(self):
communicator = mock.Mock()
response = base.AsynchronousResponse([{'string': b's'}], command='')
communicator.call.return_value.get.return_value = response
some_resource = resource.RouterOsResource(communicator, '/string', STRING_STRUCTURE)
result = some_resource.get()
self.assertEqual(result, [{'string': 's'}])

def test_string_resource_get_non_utf8_characters(self):
communicator = mock.Mock()
response = base.AsynchronousResponse([{'string': b'test-\xb9\xe6\xbf-test'}], command='')
communicator.call.return_value.get.return_value = response
some_resource = resource.RouterOsResource(communicator, '/string', STRING_STRUCTURE)
result = some_resource.get()
self.assertEqual(result, [{'string': 'test-\\xb9\\xe6\\xbf-test'}])

def test_string_resource_get_windows_1250_characters(self):
string_structure = {'string': structure.StringField(encoding='windows-1250')}
communicator = mock.Mock()
response = base.AsynchronousResponse([{'string': b'test-\xb9\xe6\xbf\x8f-test'}], command='')
communicator.call.return_value.get.return_value = response
some_resource = resource.RouterOsResource(communicator, '/string', string_structure)
result = some_resource.get()
self.assertEqual(result, [{'string': 'test-ąćżŹ-test'}])

def test_string_resource_get_latin_1_characters(self):
string_structure = {'string': structure.StringField(encoding='latin-1')}
communicator = mock.Mock()
response = base.AsynchronousResponse([{'string': b'hap ac\xb2'}], command='')
communicator.call.return_value.get.return_value = response
some_resource = resource.RouterOsResource(communicator, '/string', string_structure)
result = some_resource.get()
self.assertEqual(result, [{'string': 'hap ac²'}])

def test_bytes_resource_get(self):
communicator = mock.Mock()
response = base.AsynchronousResponse([{'bytes': b'y'}], command='')
Expand Down