├── .gitignore ├── .travis.yml ├── CHANGELOG.rst ├── LICENSE ├── README.rst ├── setup.py ├── tests ├── __init__.py └── test_api.py └── tikapy ├── __init__.py └── api └── __init__.py /.gitignore: -------------------------------------------------------------------------------- 1 | /.idea/ 2 | __pycache__/ 3 | *.pyc 4 | /dist/ 5 | /build/ 6 | /tikapy.egg-info/ 7 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: python 2 | python: 3 | - "3.4" 4 | script: python setup.py test 5 | branches: 6 | only: 7 | - master 8 | -------------------------------------------------------------------------------- /CHANGELOG.rst: -------------------------------------------------------------------------------- 1 | Change Log 2 | ========== 3 | 4 | All notable changes to this project will be documented in this file. 5 | This project adheres to `Semantic Versioning`_. 6 | 7 | `Unreleased`_ 8 | ------------- 9 | 10 | Changed 11 | ~~~~~~~ 12 | 13 | - TLS certificates are now checked against system CA store, and matched against 14 | the provided hostname. 15 | 16 | `0.2.1`_ - 2015-06-11 17 | --------------------- 18 | 19 | Fixed 20 | ~~~~~ 21 | 22 | - README formatting 23 | 24 | 25 | `0.2.0`_ - 2015-06-11 26 | --------------------- 27 | 28 | Added 29 | ~~~~~ 30 | 31 | - `#2`_ Added first tests. 32 | `@andre-luiz-dos-santos`_ 33 | 34 | Changed 35 | ~~~~~~~ 36 | 37 | - Make write\_sock more efficient, not sending byte by byte anymore. 38 | `@andre-luiz-dos-santos`_ 39 | 40 | Fixed 41 | ~~~~~ 42 | 43 | - Encoding of word length when sending words longer than 127. 44 | - Error handling during SSL connection setup. 45 | 46 | Removed 47 | ~~~~~~~ 48 | 49 | - Python 3.2/3.3 compatibility. 50 | 51 | `0.1.2`_ - 2015-06-01 52 | --------------------- 53 | 54 | Changed 55 | ~~~~~~~ 56 | 57 | - `#1`_ Return ID when adding new records. 58 | `@andre-luiz-dos-santos`_ 59 | 60 | Fixed 61 | ~~~~~ 62 | 63 | - Wrong LICENSE in setup.py 64 | 65 | 0.1.1 - 2015-05-08 66 | ------------------ 67 | 68 | Added 69 | ~~~~~ 70 | 71 | - initial public release 72 | 73 | .. _Semantic Versioning: http://semver.org/ 74 | .. _Unreleased: https://github.com/vshn/tikapy/compare/v0.2.1...HEAD 75 | .. _0.2.1: https://github.com/vshn/tikapy/compare/v0.2.0...v0.2.1 76 | .. _0.2.0: https://github.com/vshn/tikapy/compare/v0.1.2...v0.2.0 77 | .. _0.1.2: https://github.com/vshn/tikapy/compare/v0.1.1...v0.1.2 78 | .. _#1: https://github.com/vshn/tikapy/pull/1 79 | .. _#2: https://github.com/vshn/tikapy/pull/2 80 | .. _@andre-luiz-dos-santos: https://github.com/andre-luiz-dos-santos 81 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2015, VSHN AG, info@vshn.ch 2 | All rights reserved. 3 | 4 | Redistribution and use in source and binary forms, with or without 5 | modification, are permitted provided that the following conditions are met: 6 | 7 | * Redistributions of source code must retain the above copyright notice, this 8 | list of conditions and the following disclaimer. 9 | 10 | * Redistributions in binary form must reproduce the above copyright notice, 11 | this list of conditions and the following disclaimer in the documentation 12 | and/or other materials provided with the distribution. 13 | 14 | * Neither the name of the copyright holder nor the names of its 15 | contributors may be used to endorse or promote products derived from 16 | this software without specific prior written permission. 17 | 18 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 19 | AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 20 | IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 21 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE 22 | FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 23 | DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR 24 | SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER 25 | CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, 26 | OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 27 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 28 | -------------------------------------------------------------------------------- /README.rst: -------------------------------------------------------------------------------- 1 | tikapy 2 | ====== 3 | 4 | tikapy is a simple API client for MikroTik RouterOS written in python3. 5 | 6 | |travis_ci| 7 | 8 | 9 | Installation 10 | ------------ 11 | 12 | .. code-block:: bash 13 | 14 | $ pip install tikapy 15 | 16 | Examples 17 | -------- 18 | 19 | .. code-block:: python 20 | 21 | #!/usr/bin/python3 22 | 23 | from tikapy import TikapySslClient 24 | from pprint import pprint 25 | 26 | client = TikapySslClient('10.140.66.11', 8729) 27 | 28 | client.login('api-test', 'api123') 29 | pprint(client.talk(['/routing/ospf/neighbor/getall'])) 30 | 31 | 32 | .. |travis_ci| image:: https://api.travis-ci.org/vshn/tikapy.svg?branch=master 33 | :target: https://travis-ci.org/vshn/tikapy 34 | :alt: Travis CI build status (master) 35 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | """ 2 | tikapy setup module. 3 | """ 4 | 5 | from setuptools import setup, find_packages 6 | from codecs import open 7 | from os import path 8 | 9 | here = path.abspath(path.dirname(__file__)) 10 | 11 | setup( 12 | name='tikapy', 13 | version='0.2.1', 14 | description='A python client for the MikroTik RouterOS API', 15 | url='https://github.com/vshn/tikapy', 16 | test_suite="tests", 17 | author='Andre Keller', 18 | author_email='andre.keller@vshn.ch', 19 | # BSD 3-Clause License: 20 | # - http://opensource.org/licenses/BSD-3-Clause 21 | license='BSD', 22 | 23 | # See https://pypi.python.org/pypi?%3Aaction=list_classifiers 24 | classifiers=[ 25 | 'Development Status :: 3 - Alpha', 26 | 'Intended Audience :: Developers', 27 | 'Topic :: Software Development :: Build Tools', 28 | 'License :: OSI Approved :: BSD License', 29 | 'Programming Language :: Python :: 3', 30 | 'Programming Language :: Python :: 3.4', 31 | ], 32 | 33 | packages=[ 34 | 'tikapy', 35 | 'tikapy.api', 36 | ] 37 | 38 | ) 39 | -------------------------------------------------------------------------------- /tests/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vshn/tikapy/a02235d091fbd2a45628f5077127beb97c52141e/tests/__init__.py -------------------------------------------------------------------------------- /tests/test_api.py: -------------------------------------------------------------------------------- 1 | from unittest import TestCase 2 | from unittest.mock import Mock 3 | import tikapy 4 | 5 | 6 | class TestWrites(TestCase): 7 | """ 8 | Test the write functions. 9 | """ 10 | 11 | WORDS = [ 12 | # word length < 128 13 | ('', chr(0)), 14 | ('abc', chr(3) + 'abc'), 15 | ('12345', chr(5) + '12345'), 16 | ('b' * 127, chr(127) + ('b' * 127)), 17 | # word length < 16384 18 | ('c' * 128, chr(0x80) + chr(128) + ('c' * 128)), 19 | ] 20 | 21 | def test_write_word(self): 22 | """ 23 | Call 'write_word' and check what's sent to the socket. 24 | """ 25 | for inp, out in self.WORDS: 26 | with self.subTest(size=len(inp), word=inp[:15]): 27 | sock = Mock() 28 | api = tikapy.ApiRos(sock) 29 | sock.send.side_effect = lambda b: len(b) 30 | api.write_word(inp) 31 | self.assertEqual( 32 | b''.join(c[0][0] for c in sock.sendall.call_args_list), 33 | bytes(out, 'latin-1')) 34 | self.assertLessEqual(sock.sendall.call_count, 5) 35 | -------------------------------------------------------------------------------- /tikapy/__init__.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python3 2 | 3 | # 4 | # Copyright (c) 2015, VSHN AG, info@vshn.ch 5 | # Licensed under "BSD 3-Clause". See LICENSE file. 6 | # 7 | # Authors: 8 | # - Andre Keller 9 | # 10 | 11 | """ 12 | MikroTik RouterOS Python API Clients 13 | """ 14 | 15 | import logging 16 | import socket 17 | import ssl 18 | from .api import ApiError, ApiRos, ApiUnrecoverableError 19 | 20 | LOG = logging.getLogger(__name__) 21 | 22 | 23 | class ClientError(Exception): 24 | """ 25 | Exception returned when a API client interaction fails. 26 | """ 27 | pass 28 | 29 | 30 | class TikapyBaseClient(): 31 | """ 32 | Base class for functions shared between the SSL and non-SSL API client 33 | """ 34 | 35 | def __init__(self): 36 | """ 37 | Constructor. Initialize instance variables. 38 | """ 39 | self._address = None 40 | self._port = None 41 | self._base_sock = None 42 | self._sock = None 43 | self._api = None 44 | 45 | @property 46 | def address(self): 47 | """ 48 | Address of the remote API. 49 | 50 | :return: string - address of the remote API. 51 | """ 52 | return self._address 53 | 54 | @address.setter 55 | def address(self, value): 56 | """ 57 | Address of the remote API. 58 | """ 59 | self._address = value 60 | 61 | @property 62 | def port(self): 63 | """ 64 | Port of the remote API. 65 | :return: 66 | """ 67 | return self._port 68 | 69 | @port.setter 70 | def port(self, value): 71 | """ 72 | Port of the remote API. 73 | 74 | :raises: ValueError - if invalid port number is specified 75 | """ 76 | try: 77 | if not 0 < value < 65536: 78 | raise ValueError('%d is not a valid port number' % value) 79 | self._port = value 80 | except ValueError as exc: 81 | raise ValueError('invalid port number specified') from exc 82 | 83 | def __del__(self): 84 | """ 85 | Destructor. Tries to disconnect socket if it is still open. 86 | """ 87 | self.disconnect() 88 | 89 | def disconnect(self): 90 | """ 91 | Disconnect/closes open sockets. 92 | """ 93 | try: 94 | if self._sock: 95 | self._sock.close() 96 | except socket.error: 97 | pass 98 | try: 99 | if self._base_sock: 100 | self._base_sock.close() 101 | except socket.error: 102 | pass 103 | 104 | def _connect_socket(self): 105 | """ 106 | Connect the base socket. 107 | 108 | If self.address is a hostname, this function will loop through 109 | all available addresses until it can establish a connection. 110 | 111 | :raises: ClientError - if address/port has not been set 112 | - if no connection to remote socket 113 | could be established. 114 | """ 115 | if not self.address: 116 | raise ClientError('address has not been set') 117 | if not self.port: 118 | raise ClientError('address has not been set') 119 | 120 | for family, socktype, proto, _, sockaddr in \ 121 | socket.getaddrinfo(self.address, 122 | self.port, 123 | socket.AF_UNSPEC, 124 | socket.SOCK_STREAM): 125 | 126 | try: 127 | self._base_sock = socket.socket(family, socktype, proto) 128 | except socket.error: 129 | self._base_sock = None 130 | continue 131 | 132 | try: 133 | self._base_sock.connect(sockaddr) 134 | except socket.error: 135 | self._base_sock.close() 136 | self._base_sock = None 137 | continue 138 | break 139 | 140 | if self._base_sock is None: 141 | LOG.error('could not open socket') 142 | raise ClientError('could not open socket') 143 | 144 | def _connect(self): 145 | """ 146 | Connects the socket and stores the result in self._sock. 147 | 148 | This is meant to be sub-classed if a socket needs to be wrapped, 149 | f.e. with an SSL handler. 150 | """ 151 | self._connect_socket() 152 | self._sock = self._base_sock 153 | 154 | def login(self, user, password): 155 | """ 156 | Connects to the API and tries to login the user. 157 | 158 | :param user: Username for API connections 159 | :param password: Password for API connections 160 | :raises: ClientError - if login failed 161 | """ 162 | self._connect() 163 | self._api = ApiRos(self._sock) 164 | try: 165 | self._api.login(user, password) 166 | except (ApiError, ApiUnrecoverableError) as exc: 167 | raise ClientError('could not login') from exc 168 | 169 | def talk(self, words): 170 | """ 171 | Send command sequence to the API. 172 | 173 | :param words: List of command sequences to send to the API 174 | :returns: dict containing response or ID. 175 | :raises: ClientError - If client could not talk to remote API. 176 | ValueError - On invalid input. 177 | """ 178 | if isinstance(words, list) and all(isinstance(x, str) for x in words): 179 | try: 180 | return self.tik_to_json(self._api.talk(words)) 181 | except (ApiError, ApiUnrecoverableError) as exc: 182 | raise ClientError('could not talk to api') from exc 183 | raise ValueError('words needs to be a list of strings') 184 | 185 | @staticmethod 186 | def tik_to_json(tikoutput): 187 | """ 188 | Converts MikroTik RouterOS output to python dict / JSON. 189 | 190 | :param tikoutput: 191 | :return: dict containing response or ID. 192 | """ 193 | try: 194 | if tikoutput[0][0] == '!done': 195 | return tikoutput[0][1]['ret'] 196 | except (IndexError, KeyError): 197 | pass 198 | try: 199 | return { 200 | d['.id'][1:]: d for d in ([x[1] for x in tikoutput]) 201 | if '.id' in d.keys()} 202 | except (TypeError, IndexError) as exc: 203 | raise ClientError('unable to convert api output to json') from exc 204 | 205 | 206 | class TikapyClient(TikapyBaseClient): 207 | """ 208 | RouterOS API Client. 209 | """ 210 | 211 | def __init__(self, address, port=8728): 212 | """ 213 | Initialize client. 214 | 215 | :param address: Remote device address (maybe a hostname) 216 | :param port: Remote device port (defaults to 8728) 217 | """ 218 | super().__init__() 219 | self.address = address 220 | self.port = port 221 | 222 | 223 | class TikapySslClient(TikapyBaseClient): 224 | """ 225 | RouterOS SSL API Client. 226 | """ 227 | 228 | def __init__(self, address, port=8729, verify_cert=True, 229 | verify_addr=True): 230 | """ 231 | Initialize client. 232 | 233 | :param address: Remote device address (maybe a hostname) 234 | :param port: Remote device port (defaults to 8728) 235 | :param verify_cert: Verify device certificate against system CAs 236 | :param verify_addr: Verify provided address against certificate 237 | """ 238 | super().__init__() 239 | self.address = address 240 | self.port = port 241 | self.verify_cert = verify_cert 242 | self.verify_addr = verify_addr 243 | 244 | def _connect(self): 245 | """ 246 | Connects a ssl socket. 247 | """ 248 | self._connect_socket() 249 | try: 250 | ctx = ssl.create_default_context() 251 | if not self.verify_cert: 252 | ctx.verify_mode = ssl.CERT_OPTIONAL 253 | if not self.verify_addr: 254 | ctx.check_hostname = False 255 | self._sock = ctx.wrap_socket(self._base_sock, 256 | server_hostname=self.address) 257 | except ssl.SSLError: 258 | LOG.error('could not establish SSL connection') 259 | raise ClientError('could not establish SSL connection') 260 | -------------------------------------------------------------------------------- /tikapy/api/__init__.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python3 2 | 3 | # 4 | # Copyright (c) 2015, VSHN AG, info@vshn.ch 5 | # Licensed under "BSD 3-Clause". See LICENSE file. 6 | # 7 | # Authors: 8 | # - Andre Keller 9 | # 10 | 11 | """ 12 | MikroTik Router OS Python API 13 | """ 14 | 15 | import binascii 16 | import hashlib 17 | import logging 18 | 19 | LOG = logging.getLogger(__name__) 20 | 21 | 22 | class ApiError(Exception): 23 | """ 24 | Exception returned when API call fails. 25 | 26 | (!trap event) 27 | """ 28 | pass 29 | 30 | 31 | class ApiUnrecoverableError(Exception): 32 | """ 33 | Exception returned when API call fails in an unrecovarable manner. 34 | 35 | (!fatal event) 36 | """ 37 | pass 38 | 39 | 40 | class ApiRos: 41 | """ 42 | MikroTik Router OS Python API base class 43 | 44 | For a basic understanding of this code, its important to read through 45 | http://wiki.mikrotik.com/wiki/Manual:API. 46 | 47 | Within MikroTik API 'words' and 'sentences' have a very specific meaning 48 | """ 49 | 50 | def __init__(self, sock): 51 | """ 52 | Initialize base class. 53 | 54 | Args: 55 | sock - Socket (should already be opened and connected) 56 | """ 57 | self.sock = sock 58 | self.currenttag = 0 59 | 60 | def login(self, username, password): 61 | """ 62 | Perform API login 63 | 64 | Args: 65 | username - Username used to login 66 | password - Password used to login 67 | """ 68 | 69 | # request login 70 | # Mikrotik answers with a challenge in the 'ret' attribute 71 | # 'ret' attribute accessible as attrs['ret'] 72 | _, attrs = self.talk(["/login"])[0] 73 | 74 | # Prepare response for challenge-response login 75 | # response is MD5 of 0-char + plaintext-password + challange 76 | response = hashlib.md5() 77 | response.update(b'\x00') 78 | response.update(password.encode('UTF-8')) 79 | response.update(binascii.unhexlify((attrs['ret']).encode('UTF-8'))) 80 | response = "00" + binascii.hexlify(response.digest()).decode('UTF-8') 81 | 82 | # send response & login request 83 | self.talk(["/login", 84 | "=name=%s" % username, 85 | "=response=%s" % response]) 86 | 87 | def talk(self, words): 88 | """ 89 | Communicate with the API 90 | 91 | Args: 92 | words - List of API words to send 93 | """ 94 | if not words: 95 | return 96 | 97 | # Write sentence to API 98 | self.write_sentence(words) 99 | 100 | replies = [] 101 | 102 | # Wait for reply 103 | while True: 104 | # read sentence 105 | sentence = self.read_sentence() 106 | 107 | # empty sentences are ignored 108 | if len(sentence) == 0: 109 | continue 110 | 111 | # extract first word from sentence. 112 | # this indicates the type of reply: 113 | # - !re 114 | # Replay 115 | # - !done 116 | # Acknowledgement 117 | # - !trap 118 | # API Error 119 | # - !fatal 120 | # Unrecoverable API Error 121 | reply = sentence.pop(0) 122 | 123 | attrs = {} 124 | # extract attributes from the words replied by the API 125 | for word in sentence: 126 | # try to determine if there is a second equal sign in the 127 | # word. 128 | try: 129 | second_eq_pos = word.index('=', 1) 130 | except IndexError: 131 | attrs[word[1:]] = '' 132 | else: 133 | attrs[word[1:second_eq_pos]] = word[second_eq_pos + 1:] 134 | 135 | replies.append((reply, attrs)) 136 | if reply == '!done': 137 | if replies[0][0] == '!trap': 138 | raise ApiError(replies[0][1]) 139 | if replies[0][0] == '!fatal': 140 | self.sock.close() 141 | raise ApiUnrecoverableError(replies[0][1]) 142 | return replies 143 | 144 | def write_sentence(self, words): 145 | """ 146 | writes a sentence word by word to API socket. 147 | 148 | Ensures sentence is terminated with a zero-length word. 149 | 150 | Args: 151 | words - List of API words to send 152 | """ 153 | for word in words: 154 | self.write_word(word) 155 | # write zero-length word to indicate end of sentence. 156 | self.write_word('') 157 | 158 | def read_sentence(self): 159 | """ 160 | reads sentence word by word from API socket. 161 | 162 | API uses zero-length word to terminate sentence, so words are read 163 | until zero-length word is received. 164 | 165 | Returns: 166 | words - List of API words read from socket 167 | """ 168 | words = [] 169 | while True: 170 | word = self.read_word() 171 | if not word: 172 | return words 173 | words.append(word) 174 | 175 | def write_word(self, word): 176 | """ 177 | writes word to API socket 178 | 179 | The MikroTik API expects the length of the word to be sent over the 180 | wire using a special encoding followed by the word itself. 181 | 182 | See http://wiki.mikrotik.com/wiki/Manual:API#API_words for details. 183 | 184 | Args: 185 | word 186 | """ 187 | 188 | length = len(word) 189 | LOG.debug("<<< %s", word) 190 | # word length < 128 191 | if length < 0x80: 192 | self.write_sock(chr(length)) 193 | # word length < 16384 194 | elif length < 0x4000: 195 | length |= 0x8000 196 | self.write_sock(chr((length >> 8) & 0xFF)) 197 | self.write_sock(chr(length & 0xFF)) 198 | # word length < 2097152 199 | elif length < 0x200000: 200 | length |= 0xC00000 201 | self.write_sock(chr((length >> 16) & 0xFF)) 202 | self.write_sock(chr((length >> 8) & 0xFF)) 203 | self.write_sock(chr(length & 0xFF)) 204 | # word length < 268435456 205 | elif length < 0x10000000: 206 | length |= 0xE0000000 207 | self.write_sock(chr((length >> 24) & 0xFF)) 208 | self.write_sock(chr((length >> 16) & 0xFF)) 209 | self.write_sock(chr((length >> 8) & 0xFF)) 210 | self.write_sock(chr(length & 0xFF)) 211 | # word length < 549755813888 212 | elif length < 0x8000000000: 213 | self.write_sock(chr(0xF0)) 214 | self.write_sock(chr((length >> 24) & 0xFF)) 215 | self.write_sock(chr((length >> 16) & 0xFF)) 216 | self.write_sock(chr((length >> 8) & 0xFF)) 217 | self.write_sock(chr(length & 0xFF)) 218 | else: 219 | raise ApiUnrecoverableError("word-length exceeded") 220 | self.write_sock(word) 221 | 222 | def read_word(self): 223 | """ 224 | read word from API socket 225 | 226 | The MikroTik API sends the length of the word to be received over the 227 | wire using a special encoding followed by the word itself. 228 | 229 | This function will first determine the length, and then read the 230 | word from the socket. 231 | 232 | See http://wiki.mikrotik.com/wiki/Manual:API#API_words for details. 233 | 234 | """ 235 | # value of first byte determines how many bytes the encoded length 236 | # of the words will have. 237 | 238 | # we read the first char from the socket and determine its ASCII code. 239 | # (ASCII code is used to encode the length. Char "a" == 65 f.e. 240 | length = ord(self.read_sock(1)) 241 | 242 | # if most significant bit is 0 243 | # -> length < 128, no additional bytes need to be read 244 | if (length & 0x80) == 0x00: 245 | pass 246 | # if the two most significant bits are 10 247 | # -> length is >= 128, but < 16384 248 | elif (length & 0xC0) == 0x80: 249 | # unmask and shift the second lowest byte 250 | length &= ~0xC0 251 | length <<= 8 252 | # read the lowest byte 253 | length += ord(self.read_sock(1)) 254 | # if the three most significant bits are 110 255 | # -> length is >= 16384, but < 2097152 256 | elif (length & 0xE0) == 0xC0: 257 | # unmask and shift the third lowest byte 258 | length &= ~0xE0 259 | length <<= 8 260 | # read and shift second lowest byte 261 | length += ord(self.read_sock(1)) 262 | length <<= 8 263 | # read lowest byte 264 | length += ord(self.read_sock(1)) 265 | # if the four most significant bits are 1110 266 | # length is >= 2097152, but < 268435456 267 | elif (length & 0xF0) == 0xE0: 268 | # unmask and shift the fourth lowest byte 269 | length &= ~0xF0 270 | length <<= 8 271 | # read and shift third lowest byte 272 | length += ord(self.read_sock(1)) 273 | length <<= 8 274 | # read and shift second lowest byte 275 | length += ord(self.read_sock(1)) 276 | length <<= 8 277 | # read lowest byte 278 | length += ord(self.read_sock(1)) 279 | # if the five most significant bits are 11110 280 | # length is >= 268435456, but < 4294967296 281 | elif (length & 0xF8) == 0xF0: 282 | # read and shift fourth lowest byte 283 | length = ord(self.read_sock(1)) 284 | length <<= 8 285 | # read and shift third lowest byte 286 | length += ord(self.read_sock(1)) 287 | length <<= 8 288 | # read and shift second lowest byte 289 | length += ord(self.read_sock(1)) 290 | length <<= 8 291 | # read lowest byte 292 | length += ord(self.read_sock(1)) 293 | else: 294 | raise ApiUnrecoverableError("unknown control byte received") 295 | 296 | # read actual word from socket, using length determined above 297 | ret = self.read_sock(length) 298 | LOG.debug(">>> %s", ret) 299 | return ret 300 | 301 | def write_sock(self, string): 302 | """ 303 | write string to API socket 304 | 305 | Args: 306 | string - String to send 307 | """ 308 | try: 309 | self.sock.sendall(bytes(string, 'latin-1')) 310 | except OSError as exc: 311 | raise ApiUnrecoverableError("could not send to socket") from exc 312 | 313 | def read_sock(self, length): 314 | """ 315 | read string with specified length from API socket 316 | 317 | Args: 318 | length - Number of chars to read from socket 319 | Returns: 320 | string - String as read from socket 321 | """ 322 | string = '' 323 | while len(string) < length: 324 | # read data from socket with a maximum buffer size of 4k 325 | chunk = self.sock.recv(min(length - len(string), 4096)) 326 | if not chunk: 327 | raise ApiUnrecoverableError("could not read from socket") 328 | string = string + chunk.decode('latin-1', 'replace') 329 | return string 330 | --------------------------------------------------------------------------------