#!/usr/bin/env python3 """Minimal RouterOS API client used by production import scripts.""" from __future__ import annotations import binascii import hashlib import os import socket from typing import Any def encode_len(n: int) -> bytes: if n < 0x80: return bytes([n]) if n < 0x4000: n |= 0x8000 return bytes([(n >> 8) & 0xFF, n & 0xFF]) if n < 0x200000: n |= 0xC00000 return bytes([(n >> 16) & 0xFF, (n >> 8) & 0xFF, n & 0xFF]) n |= 0xE0000000 return bytes([(n >> 24) & 0xFF, (n >> 16) & 0xFF, (n >> 8) & 0xFF, n & 0xFF]) def write_sentence(sock: socket.socket, words: list[str]) -> None: for word in words: b = word.encode("utf-8") sock.sendall(encode_len(len(b)) + b) sock.sendall(b"\x00") def read_len(sock: socket.socket) -> int: c = sock.recv(1) if not c: raise ConnectionError("closed") n = c[0] if (n & 0x80) == 0: return n if (n & 0xC0) == 0x80: n &= ~0xC0 b = sock.recv(1) return (n << 8) + b[0] if (n & 0xE0) == 0xC0: n &= ~0xE0 b = sock.recv(2) return (n << 16) + (b[0] << 8) + b[1] raise RuntimeError("len too big") def read_word(sock: socket.socket) -> str: n = read_len(sock) if n == 0: return "" data = b"" while len(data) < n: chunk = sock.recv(n - len(data)) if not chunk: raise ConnectionError("closed") data += chunk return data.decode("utf-8", "replace") def read_sentence(sock: socket.socket) -> list[str]: words = [] while True: w = read_word(sock) if w == "": break words.append(w) return words def talk(sock: socket.socket, words: list[str]) -> list[list[str]]: write_sentence(sock, words) out = [] while True: s = read_sentence(sock) if not s: break out.append(s) if s[0] in ("!done", "!trap", "!fatal"): break return out def sentence_dict(s: list[str]) -> dict[str, str]: d: dict[str, str] = {} for w in s: if w.startswith("=") and "=" in w[1:]: k, v = w[1:].split("=", 1) d[k] = v elif w.startswith("=.id="): d[".id"] = w[5:] return d def replies_to_rows(replies: list[list[str]]) -> list[dict[str, str]]: rows = [] for s in replies: if s and s[0] == "!re": rows.append(sentence_dict(s)) return rows def trap_msg(replies: list[list[str]]) -> str: for s in replies: for w in s: if w.startswith("=message="): return w[9:] return str(replies) def ok(replies: list[list[str]]) -> bool: return any(s and s[0] == "!done" for s in replies) and not any( s and s[0] == "!trap" for s in replies ) def connect( host: str | None = None, user: str | None = None, password: str | None = None, port: int = 8728, timeout: int = 20, ) -> socket.socket: host = host or os.environ.get("MK_HOST", "10.15.15.1") user = user or os.environ.get("MK_USER", "admin") password = password if password is not None else os.environ.get("MK_PASS", "") sock = socket.create_connection((host, port), timeout) sock.settimeout(timeout) r = talk(sock, ["/login", f"=name={user}", f"=password={password}"]) chal = None for s in r: for w in s: if w.startswith("=ret="): chal = w[5:] if chal: h = hashlib.md5() h.update(b"\x00" + password.encode() + binascii.unhexlify(chal)) r = talk(sock, ["/login", f"=name={user}", "=response=00" + h.hexdigest()]) if not ok(r): sock.close() raise RuntimeError("MikroTik login failed: " + trap_msg(r)) return sock