159 lines
6.8 KiB
Python
159 lines
6.8 KiB
Python
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()
|