From d757b07267e2bc4ef34843aa6b8cb424122df98b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jakub=20Goc=C5=82awski?= Date: Thu, 6 Mar 2025 17:13:49 +0100 Subject: [PATCH 1/2] Update README. --- README.md | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index fc0c11c..b6066a1 100644 --- a/README.md +++ b/README.md @@ -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 @@ -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, From 4ad9cbd41b645c2c99361c303db16fe4a473b850 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jakub=20Goc=C5=82awski?= Date: Thu, 6 Mar 2025 17:13:40 +0100 Subject: [PATCH 2/2] Fix handling non utf-8 characters. --- CHANGELOG.md | 4 ++-- README.md | 25 +++++++++++++++++++++++++ routeros_api/api.py | 3 ++- routeros_api/api_structure.py | 7 +++++-- tests/test_resource.py | 35 +++++++++++++++++++++++++++++++++++ 5 files changed, 69 insertions(+), 5 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index b80e6de..ba52286 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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) diff --git a/README.md b/README.md index b6066a1..2e8652e 100644 --- a/README.md +++ b/README.md @@ -92,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. diff --git a/routeros_api/api.py b/routeros_api/api.py index 7ba3776..1cf1137 100644 --- a/routeros_api/api.py +++ b/routeros_api/api.py @@ -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): diff --git a/routeros_api/api_structure.py b/routeros_api/api_structure.py index 7899956..307a7f8 100644 --- a/routeros_api/api_structure.py +++ b/routeros_api/api_structure.py @@ -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): diff --git a/tests/test_resource.py b/tests/test_resource.py index 21d40c9..fe31b20 100644 --- a/tests/test_resource.py +++ b/tests/test_resource.py @@ -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()} @@ -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='')