import socket, hashlib, re, time 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(self): 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.decode('utf-8', errors='replace') def _read_sentence(self): words = [] while True: w = self._read_word() 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]; rest = sentence[1:] if reply == '!done': break elif reply == '!re': d = {} for w in rest: if '=' in w: parts = w.split('=', 2) d[parts[1]] = parts[2] if len(parts) >= 3 else '' else: d[w] = '' responses.append(d) elif reply == '!trap': responses.append(('!trap', rest)) elif reply == '!fatal': raise Exception(f"Fatal: {rest}") else: responses.append((reply, rest)) return responses 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') # Get router clock resp = api.cmd('/system/clock/print') if resp: print(f"Router time: {resp[0].get('date')} {resp[0].get('time')}") # Check scheduler run counts print("\n=== Scheduler run counts ===") for item in api.cmd('/system/scheduler/print'): name = item['name'] if name in ["Log", "tunnelbroker", "backup-relay"]: continue rc = item.get('run-count', '?') disabled = item.get('disabled', 'false') print(f" {name}: run={rc} disabled={disabled}") # Check hotspot users with expired comments print("\n=== Users with past expiry dates ===") today_str = resp[0].get('date', '') if resp else '' today_parts = today_str.split('/') # Convert to comparable format: 20260707 month_map = {'jan':1,'feb':2,'mar':3,'apr':4,'may':5,'jun':6,'jul':7,'aug':8,'sep':9,'oct':10,'nov':11,'dec':12} users = list(api.cmd('/ip/hotspot/user/print')) expired_count = 0 for user in users: comment = user.get('comment', '') name = user.get('name', '') profile = user.get('profile', '?') if not comment or comment == '---': continue # Parse comment: format is "mon/day/year hh:mm:ss" parts = comment.split(' ') if len(parts) >= 2: date_part = parts[0] time_part = parts[1] dp = date_part.split('/') if len(dp) == 3: try: exp_month = month_map.get(dp[0].lower(), 0) exp_day = int(dp[1]) exp_year = int(dp[2]) exp_dateint = exp_year * 10000 + exp_month * 100 + exp_day today_p = today_str.split('/') cur_month = month_map.get(today_p[0].lower(), 0) cur_day = int(today_p[1]) cur_year = int(today_p[2]) cur_dateint = cur_year * 10000 + cur_month * 100 + cur_day if exp_dateint < cur_dateint: print(f" EXPIRED: {name} (profile={profile}, comment={comment})") expired_count += 1 elif exp_dateint == cur_dateint: # Check time too tp = time_part.split(':') if len(tp) >= 2: exp_minutes = int(tp[0]) * 60 + int(tp[1]) cur_time = resp[0].get('time', '0:00:00').split(':') cur_minutes = int(cur_time[0]) * 60 + int(cur_time[1]) if exp_minutes < cur_minutes: print(f" EXPIRED today: {name} (profile={profile}, comment={comment})") expired_count += 1 except (ValueError, KeyError): pass print(f"\nTotal expired users found: {expired_count}") if expired_count > 0: print("Next scheduler run should clean these up.") else: print("No expired users pending cleanup.") api.close()