import socket, hashlib class RouterOSApi: def __init__(self, host, port=8728): self.host = host; self.port = port; self.sock = None def connect(self, username, password): self.sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) self.sock.settimeout(10) self.sock.connect((self.host, self.port)) self._write_sentence('/login', f'=name={username}', f'=password={password}') resp = self._read_response() if any(isinstance(r, tuple) and r[0] == '!trap' for r in resp): chal = None for item in resp: if isinstance(item, dict) and 'ret' in item: chal = item['ret'] if chal and len(chal) == 32: response = '00' + hashlib.md5(b'\x00' + password.encode() + bytes.fromhex(chal)).hexdigest() self._write_sentence('/login', f'=name={username}', f'=response={response}') resp = self._read_response() return self def _encode_length(self, length): if length < 0x80: return bytes([length]) elif length < 0x4000: length |= 0x8000; return bytes([(length>>8)&0xFF, length&0xFF]) elif length < 0x200000: length |= 0xC00000; return bytes([(length>>16)&0xFF, (length>>8)&0xFF, length&0xFF]) elif length < 0x10000000: length |= 0xE0000000; return bytes([(length>>24)&0xFF, (length>>16)&0xFF, (length>>8)&0xFF, length&0xFF]) else: return bytes([0xF0, (length>>24)&0xFF, (length>>16)&0xFF, (length>>8)&0xFF, length&0xFF]) def _write_word(self, word): self.sock.sendall(self._encode_length(len(word)) + word.encode('utf-8')) def _write_sentence(self, cmd, *words): self._write_word(cmd) for w in words: self._write_word(w) self.sock.sendall(b'\x00') def _read_byte(self): return self.sock.recv(1) def _read_word_raw(self): """Read a word and return raw bytes""" first = self._read_byte() if first == b'\x00': return None lb = first[0] if lb & 0x80: if (lb & 0xC0) == 0x80: length = ((lb & 0x3F) << 8) + self._read_byte()[0] elif (lb & 0xE0) == 0xC0: length = ((lb & 0x1F) << 16) + (self._read_byte()[0] << 8) + self._read_byte()[0] elif (lb & 0xF0) == 0xE0: length = ((lb & 0x0F) << 24) + (self._read_byte()[0] << 16) + (self._read_byte()[0] << 8) + self._read_byte()[0] else: length = (self._read_byte()[0] << 24) + (self._read_byte()[0] << 16) + (self._read_byte()[0] << 8) + self._read_byte()[0] else: length = lb data = b'' while len(data) < length: chunk = self.sock.recv(length - len(data)) if not chunk: break data += chunk return data def _read_sentence(self): words = [] while True: w = self._read_word_raw() if w is None: break words.append(w) return words def _read_response(self): responses = [] while True: sentence = self._read_sentence() if not sentence: break reply = sentence[0].decode('utf-8', errors='replace') rest = sentence[1:] if reply == '!done': break elif reply == '!re': d = {} for w in rest: ws = w.decode('utf-8', errors='replace') if '=' in ws: parts = ws.split('=', 2) d[parts[1]] = parts[2] if len(parts) >= 3 else '' else: d[ws] = '' responses.append(d) elif reply == '!trap': responses.append(('!trap', [r.decode('utf-8', errors='replace') for r in rest])) return responses def _read_response_raw(self): """Read and return raw sentences without decoding""" sentences = [] while True: sentence = self._read_sentence() if not sentence: break reply = sentence[0] rest = sentence[1:] if reply == b'!done': break sentences.append((reply, rest)) return sentences def cmd(self, command, **attrs): words = [command] for k, v in attrs.items(): if k.startswith('?'): words.append(f'{k}={v}') else: words.append(f'={k}={v}') self._write_sentence(*words) return self._read_response() def close(self): if self.sock: self.sock.close(); self.sock = None api = RouterOSApi('192.168.100.2') api.connect('mi4', 'm14') # Read raw bytes of Bulanan2 on-event words = [command:= '/system/scheduler/print'] words.append('?name=Bulanan2') api._write_sentence(*words) raw_resp = api._read_response_raw() # Find the on-event field for reply, rest in raw_resp: if reply == b'!re': for w in rest: ws = w.decode('utf-8', errors='replace') if ws.startswith('=on-event='): # Get raw bytes after '=on-event=' prefix = b'=on-event=' idx = w.find(prefix) if idx >= 0: on_event_bytes = w[idx + len(prefix):] print(f"on-event raw bytes length: {len(on_event_bytes)}") # Find all occurrences of :pic in raw bytes search = b':pic' pos = 0 while True: pos = on_event_bytes.find(search, pos) if pos == -1: break context = on_event_bytes[max(0,pos-5):pos+20] print(f"\n ':pic' at byte {pos}") print(f" Hex: {' '.join(f'{b:02x}' for b in context)}") print(f" ASCII: {''.join(chr(b) if 32 <= b < 127 else '?' for b in context)}") # Show what follows each :pic after = on_event_bytes[pos:pos+10] print(f" Next 10 bytes: {' '.join(f'{b:02x}' for b in after)}") pos += 4 api.close()