From 24511707086ade87c15e7445c5c73d7874f9805e Mon Sep 17 00:00:00 2001 From: player Date: Sat, 15 Aug 2026 00:28:08 +0700 Subject: [PATCH] init: workspace opencode sync --- .gitignore | 5 + TELEGRAM-RESET-BUTTON.md | 160 +++++ baca-file.py | 72 +++ cb-check.php | 7 + cb-debug.php | 5 + cb-wrap.php | 6 + cb-wrap2.php | 10 + cek-dns.py | 86 +++ cek-net.py | 90 +++ cek-rute.py | 81 +++ check-all-notif.py | 111 ++++ check-and-remove-test.py | 113 ++++ check-cb.py | 12 + check-cb2.py | 12 + check-connect.py | 10 + check-disabled.py | 29 + check-disabled2.py | 20 + check-logs.py | 95 +++ check-msr-key.py | 44 ++ check-nadia-container.py | 100 ++++ check-notif-freq.py | 92 +++ check-notif.py | 113 ++++ check-ntp.py | 94 +++ check-reset.py | 76 +++ check-runs.py | 116 ++++ check-scheduler-detail.py | 94 +++ check-script-oneliner.py | 111 ++++ check-telegram-schedulers.py | 89 +++ check-test123.py | 108 ++++ check-tg-thread.py | 17 + check-tg.py | 10 + check-tg2.py | 21 + check-users.py | 80 +++ create-a55-test.py | 110 ++++ create-test-reset.py | 78 +++ debug-bytes.py | 135 +++++ debug-replace.py | 127 ++++ debug-set.py | 30 + debug-set2.py | 62 ++ debug-ssh.py | 27 + deploy-to-somewhere/README.md | 129 ++++ deploy-to-somewhere/deploy.ps1 | 44 ++ deploy-to-somewhere/notify-expire.php | 49 ++ deploy-to-somewhere/routeros_api.class.php | 647 +++++++++++++++++++++ deploy-to-somewhere/setup.sh | 50 ++ deploy-to-somewhere/telegram-cb.php | 127 ++++ dump-sched.py | 74 +++ endtoend-test.py | 82 +++ find-resp-file.py | 73 +++ fix-clock.py | 119 ++++ fix-container.py | 15 + fix-dns.py | 81 +++ fix-final.py | 130 +++++ fix-final2.py | 159 +++++ fix-notif-dedup.py | 128 ++++ fix-notif-dedup2.py | 108 ++++ fix-raw-api.py | 238 ++++++++ fix-router.py | 107 ++++ fix-via-api.py | 41 ++ fix-via-api2.py | 77 +++ fix-via-api3.py | 72 +++ fix-via-api4.py | 69 +++ fix-via-exec.py | 59 ++ fix-via-ssh.py | 131 +++++ force-expire.py | 121 ++++ fpm-inspect.sh | 11 + install-curl.sh | 6 + install-curl2.sh | 4 + kirim-test.py | 81 +++ kirim-test2.py | 85 +++ list-files.py | 73 +++ list-sched.py | 78 +++ mk-user.py | 76 +++ patch-notif.py | 84 +++ patch-notif2.py | 100 ++++ patch-test1.py | 86 +++ patch-test2.py | 81 +++ patch-test3.py | 75 +++ patch-test4.py | 81 +++ patch-test5.py | 81 +++ phpok.php | 2 + poller.sh | 10 + read-script.py | 96 +++ remove-binding-labibah.py | 118 ++++ rm-user.py | 70 +++ send-expired-tmr.py | 79 +++ telegram-cb.php | 129 ++++ tes-domain.py | 84 +++ tes-hulu.py | 89 +++ tes-routing.py | 84 +++ tes-tg.py | 80 +++ test-1min.py | 142 +++++ test-aktif-1menit.py | 146 +++++ test-api-set.py | 37 ++ test-e2e.py | 128 ++++ test-escape.py | 37 ++ test-expiry.py | 141 +++++ test-fix-local.py | 49 ++ test-notif.py | 83 +++ test-replace.py | 22 + test-reset-btn.py | 88 +++ test-short.py | 120 ++++ test-tg-notif.py | 139 +++++ tg-check.py | 103 ++++ tg-test-simple.py | 123 ++++ unbypass-labibah.py | 94 +++ update-scheduler-family-notify.py | 123 ++++ update-scheduler-family.py | 116 ++++ update-scheduler-thread.py | 126 ++++ update-tg-and-check-key.py | 123 ++++ verif-tg.py | 83 +++ verif-tg2.py | 81 +++ verify-after-fix.py | 97 +++ verify-expiry.py | 158 +++++ verify-fix.py | 125 ++++ verify-laporkan.py | 107 ++++ verify-markup.py | 78 +++ verify-notif.py | 82 +++ verify-profiles.py | 103 ++++ verify-reset.py | 73 +++ verify-scheduler.py | 98 ++++ voucher-fix.py | 93 +++ 122 files changed, 10549 insertions(+) create mode 100644 .gitignore create mode 100644 TELEGRAM-RESET-BUTTON.md create mode 100644 baca-file.py create mode 100644 cb-check.php create mode 100644 cb-debug.php create mode 100644 cb-wrap.php create mode 100644 cb-wrap2.php create mode 100644 cek-dns.py create mode 100644 cek-net.py create mode 100644 cek-rute.py create mode 100644 check-all-notif.py create mode 100644 check-and-remove-test.py create mode 100644 check-cb.py create mode 100644 check-cb2.py create mode 100644 check-connect.py create mode 100644 check-disabled.py create mode 100644 check-disabled2.py create mode 100644 check-logs.py create mode 100644 check-msr-key.py create mode 100644 check-nadia-container.py create mode 100644 check-notif-freq.py create mode 100644 check-notif.py create mode 100644 check-ntp.py create mode 100644 check-reset.py create mode 100644 check-runs.py create mode 100644 check-scheduler-detail.py create mode 100644 check-script-oneliner.py create mode 100644 check-telegram-schedulers.py create mode 100644 check-test123.py create mode 100644 check-tg-thread.py create mode 100644 check-tg.py create mode 100644 check-tg2.py create mode 100644 check-users.py create mode 100644 create-a55-test.py create mode 100644 create-test-reset.py create mode 100644 debug-bytes.py create mode 100644 debug-replace.py create mode 100644 debug-set.py create mode 100644 debug-set2.py create mode 100644 debug-ssh.py create mode 100644 deploy-to-somewhere/README.md create mode 100644 deploy-to-somewhere/deploy.ps1 create mode 100644 deploy-to-somewhere/notify-expire.php create mode 100644 deploy-to-somewhere/routeros_api.class.php create mode 100644 deploy-to-somewhere/setup.sh create mode 100644 deploy-to-somewhere/telegram-cb.php create mode 100644 dump-sched.py create mode 100644 endtoend-test.py create mode 100644 find-resp-file.py create mode 100644 fix-clock.py create mode 100644 fix-container.py create mode 100644 fix-dns.py create mode 100644 fix-final.py create mode 100644 fix-final2.py create mode 100644 fix-notif-dedup.py create mode 100644 fix-notif-dedup2.py create mode 100644 fix-raw-api.py create mode 100644 fix-router.py create mode 100644 fix-via-api.py create mode 100644 fix-via-api2.py create mode 100644 fix-via-api3.py create mode 100644 fix-via-api4.py create mode 100644 fix-via-exec.py create mode 100644 fix-via-ssh.py create mode 100644 force-expire.py create mode 100644 fpm-inspect.sh create mode 100644 install-curl.sh create mode 100644 install-curl2.sh create mode 100644 kirim-test.py create mode 100644 kirim-test2.py create mode 100644 list-files.py create mode 100644 list-sched.py create mode 100644 mk-user.py create mode 100644 patch-notif.py create mode 100644 patch-notif2.py create mode 100644 patch-test1.py create mode 100644 patch-test2.py create mode 100644 patch-test3.py create mode 100644 patch-test4.py create mode 100644 patch-test5.py create mode 100644 phpok.php create mode 100644 poller.sh create mode 100644 read-script.py create mode 100644 remove-binding-labibah.py create mode 100644 rm-user.py create mode 100644 send-expired-tmr.py create mode 100644 telegram-cb.php create mode 100644 tes-domain.py create mode 100644 tes-hulu.py create mode 100644 tes-routing.py create mode 100644 tes-tg.py create mode 100644 test-1min.py create mode 100644 test-aktif-1menit.py create mode 100644 test-api-set.py create mode 100644 test-e2e.py create mode 100644 test-escape.py create mode 100644 test-expiry.py create mode 100644 test-fix-local.py create mode 100644 test-notif.py create mode 100644 test-replace.py create mode 100644 test-reset-btn.py create mode 100644 test-short.py create mode 100644 test-tg-notif.py create mode 100644 tg-check.py create mode 100644 tg-test-simple.py create mode 100644 unbypass-labibah.py create mode 100644 update-scheduler-family-notify.py create mode 100644 update-scheduler-family.py create mode 100644 update-scheduler-thread.py create mode 100644 update-tg-and-check-key.py create mode 100644 verif-tg.py create mode 100644 verif-tg2.py create mode 100644 verify-after-fix.py create mode 100644 verify-expiry.py create mode 100644 verify-fix.py create mode 100644 verify-laporkan.py create mode 100644 verify-markup.py create mode 100644 verify-notif.py create mode 100644 verify-profiles.py create mode 100644 verify-reset.py create mode 100644 verify-scheduler.py create mode 100644 voucher-fix.py diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..cec1def --- /dev/null +++ b/.gitignore @@ -0,0 +1,5 @@ +๏ปฟ*.log +__pycache__/ +*.pyc.DS_Store +node_modules/.env + diff --git a/TELEGRAM-RESET-BUTTON.md b/TELEGRAM-RESET-BUTTON.md new file mode 100644 index 0000000..167b0e7 --- /dev/null +++ b/TELEGRAM-RESET-BUTTON.md @@ -0,0 +1,160 @@ +# Telegram Reset Button untuk Notifikasi Expired (MSR Hotspot) + +> Ringkasan perubahan arsitektur notifikasi expired hotspot & tombol "๐Ÿ”„ Reset" via Telegram. +> Tanggal: 1 Agu 2026 ยท RouterOS v6.49.6 ยท Mikhmon (Docker) + +--- + +## 1. Masalah Awal + +1. Notifikasi "User X expired" sebelumnya dikirim oleh `notify-expire.php` di server Mikhmon, yang menampilkan inline keyboard **๐Ÿ”„ Reset** dengan `callback_data=reset_`. +2. Router (MikroTik) tidak bisa menjangkau path lama (`192.168.100.6`, sudah mati). Scheduler di router mengirim notifikasi ke URL Telegram lama yang tidak lagi berfungsi. +3. Akibatnya pesan expired terkirim **tanpa tombol Reset**, dan callback reset tidak berfungsi. + +## 2. Solusi Baru (Arsitektur) + +``` +[RouterOS Scheduler] --/tool/fetch sendMessage--> [Telegram Bot] --Kirim ke Grup (thread "Notif Expire") + ^ +[User klik ๐Ÿ”„ Reset] --> callback_query --> [Bot] --> poller getUpdates + | + v + [Host rl] cron tiap menit --> msr-poller.sh --> docker start mikhmon + | + v + telegram-cb.php (di container) --> RouterOS API reset user + | + v + Telegram: "โœ… {user} has been reset" +``` + +### Komponen +| Komponen | Lokasi | Peran | +|---|---|---| +| Scheduler RouterOS | Router 192.168.100.2 | Kirim pesan expired + `reply_markup` tombol Reset langsung ke Telegram | +| Telegram Bot | `5115036608:AAGey...` | Terima/teruskan pesan & callback | +| Cron host | `/usr/local/bin/msr-poller.sh` di `rl` | Setiap menit: `docker start mikhmon` + curl `telegram-cb.php` | +| `telegram-cb.php` | `/var/www/html/` di container `mikhmon` | getUpdates โ†’ reset user di RouterOS โ†’ jawab callback + kirim konfirmasi | +| `lib/routeros_api.class.php` | `/var/www/html/lib/` | Koneksi RouterOS API dari PHP | + +## 3. Perubahan yang Dilakukan + +### 3.1 Scheduler RouterOS (Family, Bulanan2, Bulanan4) +`on-event` scheduler sekarang melakukan `/tool/fetch` ke Telegram `sendMessage` dengan: +- `chat_id=-1002554941429`, `message_thread_id=42` (thread "Notif Expire") +- `text=" . $name . "+expired` +- `reply_markup` = JSON inline keyboard **URL-encoded**: + +``` +%7B%22inline_keyboard%22%3A%5B%5B%7B%22text%22%3A%22%F0%9F%94%84%20Reset%22%2C%22callback_data%22%3A%22reset_" . $name . "%22%7D%5D%5D%7D +``` + +yang setara dengan: +```json +{"inline_keyboard":[[{"text":"๐Ÿ”ด Reset","callback_data":"reset_"}]]} +``` + +Contoh cuplikan on-event Family: +``` +tool fetch url=("https://api.telegram.org/bot5115036608:AAGey.../sendMessage?chat_id=-1002554941429&message_thread_id=42&text=" . $name . "+expired&reply_markup=" . "%7B%22inline_keyboard%22%3A...%22reset_" . $name . "%22%7D%5D%5D%7D") mode=https http-method=get keep-result=no +``` + +**Penting:** deduplikasi (`:if ($curlim = "")`) tetap dipertahankan agar setiap user hanya dikirim sekali. + +### 3.2 Container Mikhmon โ€” Install php81-curl +Container PHP 8.1 (Alpine) awalnya tanpa ekstensi cURL โ†’ `telegram-cb.php` error HTTP 500. +```sh +docker exec -u root mikhmon apk add --no-cache php81-curl +``` + +### 3.3 Perbaikan include path `telegram-cb.php` +Dari `dirname(__DIR__) . '/lib/routeros_api.class.php'` โ†’ `__DIR__ . '/lib/routeros_api.class.php'` (2 tempat), karena file berada langsung di `/var/www/html/`. + +### 3.4 Poller Host (atasi sablier) +Container `mikhmon` dikelola **sablier** (traefik plugin) yang mematikannya setelah **5 menit idle** โ€” cron di dalam container tidak bisa diandalkan. Solusi: cron di **host** yang men-starter container lalu memanggil callback. + +`/usr/local/bin/msr-poller.sh`: +```bash +#!/bin/bash +CB_KEY="msr-2026-reset" +URL="http://172.18.0.15/telegram-cb.php?key=${CB_KEY}" +docker start mikhmon >/dev/null 2>&1 +sleep 1 +curl -s -m 30 "$URL" >/dev/null 2>&1 +``` + +Crontab host (`crontab -l`): +``` +* * * * * /usr/local/bin/msr-poller.sh >/dev/null 2>&1 +``` + +Catatan: +- `docker start` idempotent (kalau sudah jalan, no-op) โ†’ otomatis menjaga container hidup. +- Polling maksimal latency ~1 menit setelah klik tombol. + +### 3.5 Callback Handler `telegram-cb.php` +- Auth via `?key=msr-2026-reset` (403 kalau salah). +- `getUpdates` (hanya `callback_query`). +- Untuk `data` berawalan `reset_`: + 1. Cari user di `/ip/hotspot/user/print?name=` + 2. `set limit-uptime=0, comment=''` + `reset-counters` + 3. Hapus scheduler bernama `` (jika ada) + 4. `answerCallbackQuery` (`โœ… {user} reset!`) + 5. `sendMessage` ke chat/thread: `โœ… *{user}* has been reset\nReady to use again` +- User tidak ditemukan โ†’ `answerCallbackQuery` alert `User {user} not found` (tanpa pesan). +- Meng-konfirmasi offset via `getUpdates?offset=` setelah diproses. + +## 4. Verifikasi + +| Item | Hasil | +|---|---| +| Pesan expired + tombol Reset muncul di grup | โœ… Bubble punya `reply-markup-button` "Reset" | +| Klik tombol โ†’ callback `reset_` sampai ke bot | โœ… `data: "reset_TMR-1"` via getUpdates | +| Poller cron memproses callback | โœ… `processed: 1` | +| User di-reset di router | โœ… `limit-uptime` 10s โ†’ None (unlimited), counters reset | +| Scheduler user dihapus | โœ… | +| Konfirmasi bot di grup | โœ… `msr_net: TMR-1 has been reset` (09:11) | + +Test user `TMR-1` dibuat khusus lalu dihapus setelah verifikasi. + +## 5. Operasional & Troubleshooting + +### Cek status +- Poller: `ssh rl "cat /usr/local/bin/msr-poller.sh; crontab -l | grep poller"` +- Container: `ssh rl "docker ps --filter name=mikhmon --format '{{.Status}}'"` +- Callback pending: `curl 'http://172.18.0.15/telegram-cb.php?key=msr-2026-reset'` โ†’ harus `processed: 0` / `no updates` +- Webhook tidak terpasang (poller getUpdates aman): `getWebhookInfo` โ†’ `url: ""`, `allowed_updates:["callback_query"]` + +### Gejala umum +- **HTTP 500 dari telegram-cb.php** โ†’ cek cURL: `docker exec mikhmon php -r 'var_dump(function_exists("curl_init"));'` +- **Container mati** โ†’ sablier mematikannya setelah 5m idle; poller harusnya men-starter. Cek log poller/cron. +- **Callback tidak terproses** โ†’ JANGAN baca `getUpdates` manual (maju offset & poller kehilangan callback). Biarkan poller cron yang memproses. +- **Tombol tidak muncul** โ†’ `reply_markup` tidak ter-encode dengan benar; cek `verify-markup.py` & on-event scheduler. + +## 6. Kredensial & Referensi + +- Router: `192.168.100.2`, API `remote.vpnmurahjogja.my.id:33206`, login `mi4`/`m14` +- Bot: `5115036608:AAGeyXVq5eNvdAkaCQHU9NfH16uAAiSKiuk` +- Chat/Grup: `-1002554941429`, thread `42` ("Notif Expire") +- Container: `mikhmon` (animegasan/mikhmon:v3-latest) di host `rl` (`root@100.100.31.46`, ssh alias `rl`, key `~/.ssh/pl`) +- Callback secret: `msr-2026-reset` +- Container IP: `172.18.0.15` + +## 7. Skrip Bantu (folder `C:\Users\Admin\dev\mikhmon-fix\`) + +| Skrip | Fungsi | +|---|---| +| `patch-notif2.py` | Patch on-event scheduler โ†’ tambah `reply_markup` Reset button | +| `verify-markup.py` | Verifikasi on-event berisi `reply_markup` & `reset_` | +| `verify-notif.py` | Verifikasi isi scheduler (api.telegram.org, tidak ada URL lama) | +| `dump-sched.py` | Dump penuh on-event scheduler | +| `telegram-cb.php` | Callback handler (sumber; di-deploy ke container) | +| `poller.sh` | Poller host (sumber; di-deploy ke `/usr/local/bin/msr-poller.sh`) | +| `mk-user.py` / `rm-user.py` | Buat / hapus user test | +| `send-expired-tmr.py` | Kirim pesan expired + tombol untuk user tertentu | +| `check-reset.py` | Cek limit-uptime & scheduler user setelah reset | + +## 8. Catatan +- Bot menampilkan tombol sebagai "Reset" โ€” teks `๐Ÿ”„` (U+1F504, `%F0%9F%94%84`) di-render Telegram sebagai ikon ๐Ÿ”„. +- `keep-result=no` di scheduler fetch (tidak menyimpan file respons di router). Saat debug bisa ubah `keep-result=yes` untuk lihat file `sendMessage?...`. +- Jika tombol ditekan dua kali, user kedua dikirim ulang notif "expired" (karena sudah reset, limit-uptime unlimited โ†’ tidak ada scheduler baru sampai limit diubah lagi). diff --git a/baca-file.py b/baca-file.py new file mode 100644 index 0000000..8efb398 --- /dev/null +++ b/baca-file.py @@ -0,0 +1,72 @@ +import socket, hashlib +class R: + def __init__(s,h,p=8728): s.h=h; s.p=p; s.s=None + def c(s,u,p): + s.s=socket.socket(); s.s.settimeout(30) + s.s.connect((s.h,s.p)) + s._ws('/login','=name='+u,'=password='+p) + r=s._rr() + if any(isinstance(x,tuple) for x in r): + for x in r: + if isinstance(x,dict) and 'ret' in x: ch=x['ret'] + if ch and len(ch)==32: + hh=hashlib.md5(b'\x00'+p.encode()+bytes.fromhex(ch)).hexdigest() + s._ws('/login','=name='+u,'=response=00'+hh); s._rr() + return s + def _el(s,l): + if l<0x80: return bytes([l]) + elif l<0x4000: l|=0x8000; return bytes([(l>>8)&0xFF,l&0xFF]) + else: return bytes([0xC0|((l>>16)&0x0F),(l>>8)&0xFF,l&0xFF]) if l<0x200000 else bytes([0xE0|((l>>24)&0x07),(l>>16)&0xFF,(l>>8)&0xFF,l&0xFF]) + def _ww(s,w): s.s.sendall(s._el(len(w))+w.encode()) + def _ws(s,c,*ws): s._ww(c); [s._ww(w) for w in ws]; s.s.sendall(b'\x00') + def _rw(s): + f=s.s.recv(1) + if f==b'\x00': return None + lb=f[0]; l=lb + if lb&0x80: + if (lb&0xC0)==0x80: l=((lb&0x3F)<<8)+s.s.recv(1)[0] + elif (lb&0xE0)==0xC0: l=((lb&0x1F)<<16)+(s.s.recv(1)[0]<<8)+s.s.recv(1)[0] + else: l=((lb&0x0F)<<24)+(s.s.recv(1)[0]<<16)+(s.s.recv(1)[0]<<8)+s.s.recv(1)[0] + d=b'' + while len(d)=3 else '' + else: d[w]='' + rs.append(d) + elif r=='!trap': rs.append(('!trap',t)) + return rs + def cmd(s,c,**a): + ws=[c] + for k,v in a.items(): + ws.append(('?' if k.startswith('?') else '=')+k+'='+v) + s._ws(*ws) + return s._rr() + def close(s): + if s.s: s.s.close() + +api=R('remote.vpnmurahjogja.my.id',33206).c('mi4','m14') +r = api.cmd('/file/print', **{'?type':'file'}) +for x in r: + if isinstance(x,dict) and 'sendMessage' in x.get('name',''): + api.cmd('/file/print', **{'file': x.get('name')}) + api.cmd('/file/remove', **{'=.id': x.get('.id')}) +api.close() diff --git a/cb-check.php b/cb-check.php new file mode 100644 index 0000000..4b4f206 --- /dev/null +++ b/cb-check.php @@ -0,0 +1,7 @@ +>8)&0xFF,l&0xFF]) + else: return bytes([0xC0|((l>>16)&0x0F),(l>>8)&0xFF,l&0xFF]) if l<0x200000 else bytes([0xE0|((l>>24)&0x07),(l>>16)&0xFF,(l>>8)&0xFF,l&0xFF]) + def _ww(s,w): s.s.sendall(s._el(len(w))+w.encode()) + def _ws(s,c,*ws): s._ww(c); [s._ww(w) for w in ws]; s.s.sendall(b'\x00') + def _rw(s): + f=s.s.recv(1) + if f==b'\x00': return None + lb=f[0]; l=lb + if lb&0x80: + if (lb&0xC0)==0x80: l=((lb&0x3F)<<8)+s.s.recv(1)[0] + elif (lb&0xE0)==0xC0: l=((lb&0x1F)<<16)+(s.s.recv(1)[0]<<8)+s.s.recv(1)[0] + else: l=((lb&0x0F)<<24)+(s.s.recv(1)[0]<<16)+(s.s.recv(1)[0]<<8)+s.s.recv(1)[0] + d=b'' + while len(d)=3 else '' + else: d[w]='' + rs.append(d) + elif r=='!trap': rs.append(('!trap',t)) + return rs + def cmd(s,c,**a): + ws=[c] + for k,v in a.items(): + ws.append(('?' if k.startswith('?') else '=')+k+'='+v) + s._ws(*ws) + return s._rr() + def close(s): + if s.s: s.s.close() + +api=R('remote.vpnmurahjogja.my.id',33206).c('mi4','m14') + +print('=== DNS settings ===') +for d in api.cmd('/ip/dns/print'): + for k in ['servers', 'dynamic-servers', 'static', 'use-doh-server', 'doh-timeout']: + v = d.get(k,'') + if v: print(' %s: %s' % (k, v)) + +print() +print('=== DNS static entries for msr / dimanaaja ===') +for s in api.cmd('/ip/dns/static/print'): + n = s.get('name','') + if 'dimanaaja' in n or 'msr' in n or 'mikhmon' in n: + print(' %s -> %s' % (n, s.get('address',''))) + +print() +print('=== Try DNS resolve with 8.8.8.8 as server ===') +r = api.cmd('/ip/dns/query', **{'server': '8.8.8.8', 'name': 'msr.s.dimanaaja.biz.id', 'type': 'A'}) +print(' %s' % str(r)[:300]) + +api.close() diff --git a/cek-net.py b/cek-net.py new file mode 100644 index 0000000..84de4ce --- /dev/null +++ b/cek-net.py @@ -0,0 +1,90 @@ +import socket, hashlib +class R: + def __init__(s,h,p=8728): s.h=h; s.p=p; s.s=None + def c(s,u,p): + s.s=socket.socket(); s.s.settimeout(20) + s.s.connect((s.h,s.p)) + s._ws('/login','=name='+u,'=password='+p) + r=s._rr() + if any(isinstance(x,tuple) for x in r): + for x in r: + if isinstance(x,dict) and 'ret' in x: ch=x['ret'] + if ch and len(ch)==32: + hh=hashlib.md5(b'\x00'+p.encode()+bytes.fromhex(ch)).hexdigest() + s._ws('/login','=name='+u,'=response=00'+hh); s._rr() + return s + def _el(s,l): + if l<0x80: return bytes([l]) + elif l<0x4000: l|=0x8000; return bytes([(l>>8)&0xFF,l&0xFF]) + else: return bytes([0xC0|((l>>16)&0x0F),(l>>8)&0xFF,l&0xFF]) if l<0x200000 else bytes([0xE0|((l>>24)&0x07),(l>>16)&0xFF,(l>>8)&0xFF,l&0xFF]) + def _ww(s,w): s.s.sendall(s._el(len(w))+w.encode()) + def _ws(s,c,*ws): s._ww(c); [s._ww(w) for w in ws]; s.s.sendall(b'\x00') + def _rw(s): + f=s.s.recv(1) + if f==b'\x00': return None + lb=f[0]; l=lb + if lb&0x80: + if (lb&0xC0)==0x80: l=((lb&0x3F)<<8)+s.s.recv(1)[0] + elif (lb&0xE0)==0xC0: l=((lb&0x1F)<<16)+(s.s.recv(1)[0]<<8)+s.s.recv(1)[0] + else: l=((lb&0x0F)<<24)+(s.s.recv(1)[0]<<16)+(s.s.recv(1)[0]<<8)+s.s.recv(1)[0] + d=b'' + while len(d)=3 else '' + else: d[w]='' + rs.append(d) + elif r=='!trap': rs.append(('!trap',t)) + return rs + def cmd(s,c,**a): + ws=[c] + for k,v in a.items(): + ws.append(('?' if k.startswith('?') else '=')+k+'='+v) + s._ws(*ws) + return s._rr() + def close(s): + if s.s: s.s.close() + +api=R('remote.vpnmurahjogja.my.id',33206).c('mi4','m14') + +print('=== Router IP Addresses ===') +for a in api.cmd('/ip/address/print'): + print(' %-20s %-12s net=%s' % (a.get('address',''), a.get('interface',''), a.get('network',''))) + +print() +print('=== Routes to 100.100.31.46 / tailscale ===') +for r in api.cmd('/ip/route/print'): + dst = r.get('dst-address','') + gw = r.get('gateway','') + if dst and ('100.100' in dst or '0.0.0.0' == dst.split('/')[0]): + print(' dst=%-20s gateway=%-15s dist=%s' % (dst, gw, r.get('distance',''))) + +print() +print('=== Route check: to 100.100.31.46 ===') +r = api.cmd('/ip/route/check', address='100.100.31.46') +print(' %s' % str(r)[:300]) + +print() +print('=== Route check: to 192.168.100.6 ===') +r2 = api.cmd('/ip/route/check', address='192.168.100.6') +print(' %s' % str(r2)[:300]) + +api.close() diff --git a/cek-rute.py b/cek-rute.py new file mode 100644 index 0000000..c8cf084 --- /dev/null +++ b/cek-rute.py @@ -0,0 +1,81 @@ +import socket, hashlib +class R: + def __init__(s,h,p=8728): s.h=h; s.p=p; s.s=None + def c(s,u,p): + s.s=socket.socket(); s.s.settimeout(25) + s.s.connect((s.h,s.p)) + s._ws('/login','=name='+u,'=password='+p) + r=s._rr() + if any(isinstance(x,tuple) for x in r): + for x in r: + if isinstance(x,dict) and 'ret' in x: ch=x['ret'] + if ch and len(ch)==32: + hh=hashlib.md5(b'\x00'+p.encode()+bytes.fromhex(ch)).hexdigest() + s._ws('/login','=name='+u,'=response=00'+hh); s._rr() + return s + def _el(s,l): + if l<0x80: return bytes([l]) + elif l<0x4000: l|=0x8000; return bytes([(l>>8)&0xFF,l&0xFF]) + else: return bytes([0xC0|((l>>16)&0x0F),(l>>8)&0xFF,l&0xFF]) if l<0x200000 else bytes([0xE0|((l>>24)&0x07),(l>>16)&0xFF,(l>>8)&0xFF,l&0xFF]) + def _ww(s,w): s.s.sendall(s._el(len(w))+w.encode()) + def _ws(s,c,*ws): s._ww(c); [s._ww(w) for w in ws]; s.s.sendall(b'\x00') + def _rw(s): + f=s.s.recv(1) + if f==b'\x00': return None + lb=f[0]; l=lb + if lb&0x80: + if (lb&0xC0)==0x80: l=((lb&0x3F)<<8)+s.s.recv(1)[0] + elif (lb&0xE0)==0xC0: l=((lb&0x1F)<<16)+(s.s.recv(1)[0]<<8)+s.s.recv(1)[0] + else: l=((lb&0x0F)<<24)+(s.s.recv(1)[0]<<16)+(s.s.recv(1)[0]<<8)+s.s.recv(1)[0] + d=b'' + while len(d)=3 else '' + else: d[w]='' + rs.append(d) + elif r=='!trap': rs.append(('!trap',t)) + return rs + def cmd(s,c,**a): + ws=[c] + for k,v in a.items(): + ws.append(('?' if k.startswith('?') else '=')+k+'='+v) + s._ws(*ws) + return s._rr() + def close(s): + if s.s: s.s.close() + +api=R('remote.vpnmurahjogja.my.id',33206).c('mi4','m14') + +print('=== ALL ROUTES ===') +for r in api.cmd('/ip/route/print'): + dst = r.get('dst-address','') + gw = r.get('gateway','') + iface = r.get('gateway-status','') + print(' dst=%-18s gw=%-18s dist=%s type=%s' % (dst, gw, r.get('distance',''), r.get('routing-table',''))) + +print() +print('=== ARP / neighbours on ether1 (192.168.100.x) ===') +for n in api.cmd('/ip/arp/print'): + if '100.' in n.get('address',''): + print(' %-18s iface=%-10s %s' % (n.get('address',''), n.get('interface',''), n.get('mac-address',''))) + +api.close() diff --git a/check-all-notif.py b/check-all-notif.py new file mode 100644 index 0000000..490851d --- /dev/null +++ b/check-all-notif.py @@ -0,0 +1,111 @@ +import socket, hashlib +class R: + def __init__(s,h,p=8728): s.h=h; s.p=p; s.s=None + def c(s,u,p): + s.s=socket.socket(); s.s.settimeout(15) + s.s.connect((s.h,s.p)) + s._ws('/login','=name='+u,'=password='+p) + r=s._rr() + if any(isinstance(x,tuple) for x in r): + for x in r: + if isinstance(x,dict) and 'ret' in x: ch=x['ret'] + if ch and len(ch)==32: + hh=hashlib.md5(b'\x00'+p.encode()+bytes.fromhex(ch)).hexdigest() + s._ws('/login','=name='+u,'=response=00'+hh); s._rr() + return s + def _el(s,l): + if l<0x80: return bytes([l]) + elif l<0x4000: l|=0x8000; return bytes([(l>>8)&0xFF,l&0xFF]) + else: return bytes([0xC0|((l>>16)&0x0F),(l>>8)&0xFF,l&0xFF]) if l<0x200000 else bytes([0xE0|((l>>24)&0x07),(l>>16)&0xFF,(l>>8)&0xFF,l&0xFF]) + def _ww(s,w): s.s.sendall(s._el(len(w))+w.encode()) + def _ws(s,c,*ws): s._ww(c); [s._ww(w) for w in ws]; s.s.sendall(b'\x00') + def _rw(s): + f=s.s.recv(1) + if f==b'\x00': return None + lb=f[0]; l=lb + if lb&0x80: + if (lb&0xC0)==0x80: l=((lb&0x3F)<<8)+s.s.recv(1)[0] + elif (lb&0xE0)==0xC0: l=((lb&0x1F)<<16)+(s.s.recv(1)[0]<<8)+s.s.recv(1)[0] + else: l=((lb&0x0F)<<24)+(s.s.recv(1)[0]<<16)+(s.s.recv(1)[0]<<8)+s.s.recv(1)[0] + d=b'' + while len(d)=3 else '' + else: d[w]='' + rs.append(d) + elif r=='!trap': rs.append(('!trap',t)) + return rs + def cmd(s,c,**a): + ws=[c] + for k,v in a.items(): + ws.append(('?' if k.startswith('?') else '=')+k+'='+v) + s._ws(*ws) + return s._rr() + def close(s): + if s.s: s.s.close() + +api=R('remote.vpnmurahjogja.my.id',33206).c('mi4','m14') + +# Check ALL profiles for notification capability +print('=== All Profiles with notify ===') +for p in api.cmd('/ip/hotspot/user/profile/print'): + name = p.get('name','') + ol = p.get('on-login','') + has_notify = 'notify' in ol or 'telegram' in ol or 'bot' in ol or 'fetch' in ol + has_expmode = 'rem' in ol or 'ntf' in ol + print(' %s | has_notify=%s | has_expmode=%s' % (name, has_notify, has_expmode)) + +# Check all schedulers for notify calls +print() +print('=== Schedulers that call notify ===') +for si in api.cmd('/system/scheduler/print'): + oe = si.get('on-event','') + n = si.get('name','') + if 'notify-expire' in oe or 'telegram' in oe or 'fetch' in oe: + print(' %s: calls notify-expire' % n) + elif 'set limit-uptime' in oe: + print(' %s: silent (limit-uptime only)' % n) + elif 'remove' in oe: + print(' %s: remove only' % n) + else: + print(' %s: other' % n) + +# Count how many times per day each notify-scheduler would fire +print() +print('=== Notification frequency ===') +for si in api.cmd('/system/scheduler/print'): + n = si.get('name','') + intv = si.get('interval','') + oe = si.get('on-event','') + if 'notify-expire' in oe: + # Parse interval + parts = intv.split(':') + if len(parts) == 3: + hrs = int(parts[0]) + mins = int(parts[1]) + secs = int(parts[2]) + total_secs = hrs*3600 + mins*60 + secs + if total_secs > 0: + per_day = 86400 // total_secs + print(' %s: every %s = ~%dx/day per EXPIRED user (NO dedup!)' % (n, intv, per_day)) + +api.close() diff --git a/check-and-remove-test.py b/check-and-remove-test.py new file mode 100644 index 0000000..0203531 --- /dev/null +++ b/check-and-remove-test.py @@ -0,0 +1,113 @@ +import socket, hashlib + +class R: + def __init__(self,h,p=8728): self.h=h; self.p=p; self.s=None + def c(self,u,p): + self.s=socket.socket(socket.AF_INET,socket.SOCK_STREAM); self.s.settimeout(10) + self.s.connect((self.h,self.p)) + self._ws('/login','=name='+u,'=password='+p) + r=self._rr() + if any(isinstance(x,tuple) and x[0]=='!trap' for x in r): + for x in r: + if isinstance(x,dict) and 'ret' in x: ch=x['ret'] + if ch and len(ch)==32: + h=hashlib.md5(b'\x00'+p.encode()+bytes.fromhex(ch)).hexdigest() + self._ws('/login','=name='+u,'=response=00'+h) + self._rr() + return self + def _el(self,l): + if l<0x80: return bytes([l]) + elif l<0x4000: l|=0x8000; return bytes([(l>>8)&0xFF,l&0xFF]) + elif l<0x200000: l|=0xC00000; return bytes([(l>>16)&0xFF,(l>>8)&0xFF,l&0xFF]) + elif l<0x10000000: l|=0xE0000000; return bytes([(l>>24)&0xFF,(l>>16)&0xFF,(l>>8)&0xFF,l&0xFF]) + else: return bytes([0xF0,(l>>24)&0xFF,(l>>16)&0xFF,(l>>8)&0xFF,l&0xFF]) + def _ww(self,w): self.s.sendall(self._el(len(w))+w.encode('utf-8')) + def _ws(self,c,*ws): self._ww(c); [self._ww(w) for w in ws]; self.s.sendall(b'\x00') + def _rb(self): return self.s.recv(1) + def _rw(self): + f=self._rb() + if f==b'\x00': return None + lb=f[0] + if lb&0x80: + if (lb&0xC0)==0x80: l=((lb&0x3F)<<8)+self._rb()[0] + elif (lb&0xE0)==0xC0: l=((lb&0x1F)<<16)+(self._rb()[0]<<8)+self._rb()[0] + elif (lb&0xF0)==0xE0: l=((lb&0x0F)<<24)+(self._rb()[0]<<16)+(self._rb()[0]<<8)+self._rb()[0] + else: l=(self._rb()[0]<<24)+(self._rb()[0]<<16)+(self._rb()[0]<<8)+self._rb()[0] + else: l=lb + d=b'' + while len(d)=3 else '' + else: d[w]='' + rs.append(d) + elif r=='!trap': rs.append(('!trap',t)) + return rs + def cmd(self,c,**a): + ws=[c] + for k,v in a.items(): + if k.startswith('?'): ws.append(f'{k}={v}') + else: ws.append(f'={k}={v}') + self._ws(*ws) + return self._rr() + def close(self): + if self.s: self.s.close(); self.s=None + +api=R('192.168.100.2').c('mi4','m14') + +print("=== DHCP Lease 192.168.53.177 ===") +for l in api.cmd('/ip/dhcp-server/lease/print', **{'?address': '192.168.53.177'}): + print(f" MAC: {l.get('mac-address')}") + print(f" Hostname: {l.get('host-name')}") + print(f" Status: {l.get('status')}") + print(f" Server: {l.get('server')}") + print(f" Comment: {l.get('comment')}") + lid = l.get('.id') + + # Remove it + print(f"\n Removing lease {lid}...") + api.cmd('/ip/dhcp-server/lease/remove', **{'.id': lid}) + print(" Done.") + +print("\n=== IP Binding untuk MAC CA:1A:02:C2:8C:1E ===") +for b in api.cmd('/ip/hotspot/ip-binding/print'): + mac = b.get('mac-address', '') + if 'CA:1A:02:C2:8C:1E' in mac or 'CA:1A' in mac[:7]: + print(f" Server: {b.get('server')} MAC: {mac} IP: {b.get('address')} Type: {b.get('type')}") + +print("\n=== IP Binding ALL ===") +for b in api.cmd('/ip/hotspot/ip-binding/print'): + print(f" MAC: {b.get('mac-address')} IP: {b.get('address')} Type: {b.get('type')} Server: {b.get('server')}") + +print("\n=== MAC CA:1A:02:C2:8C:1E di Hotspot User ===") +for u in api.cmd('/ip/hotspot/user/print'): + mac = u.get('mac-address', '') + comment = u.get('comment', '') + name = u.get('name', '') + limit_uptime = u.get('limit-uptime', '') + profile = u.get('profile', '') + if mac and 'CA:1A' in mac: + print(f" User: {name} Profile: {profile} MAC: {mac} Comment: {comment} limit-uptime: {limit_uptime}") + +api.close() diff --git a/check-cb.py b/check-cb.py new file mode 100644 index 0000000..b600ea0 --- /dev/null +++ b/check-cb.py @@ -0,0 +1,12 @@ +import json, requests +r = requests.get('https://api.telegram.org/bot5115036608:AAGeyXVq5eNvdAkaCQHU9NfH16uAAiSKiuk/getUpdates?timeout=5') +d = r.json() +print('ok:', d.get('ok')) +print('count:', len(d.get('result', []))) +for u in d.get('result', []): + types = [k for k in u if k != 'update_id'] + print(f' update {u["update_id"]}: {types}') + if 'callback_query' in u: + cb = u['callback_query'] + print(f' data: {cb.get("data","")}') + print(f' from: {cb.get("from",{}).get("first_name","")}') diff --git a/check-cb2.py b/check-cb2.py new file mode 100644 index 0000000..c306661 --- /dev/null +++ b/check-cb2.py @@ -0,0 +1,12 @@ +import requests, time +r = requests.get('https://api.telegram.org/bot5115036608:AAGeyXVq5eNvdAkaCQHU9NfH16uAAiSKiuk/getUpdates', params={'timeout': 10, 'allowed_updates': ['callback_query']}, timeout=15) +d = r.json() +print('ok:', d.get('ok')) +print('count:', len(d.get('result', []))) +for u in d.get('result', []): + types = [k for k in u if k != 'update_id'] + print(' update %s: %s' % (u['update_id'], types)) + if 'callback_query' in u: + cb = u['callback_query'] + print(' data: %s' % cb.get('data','')) + print(' from: %s' % cb.get('from',{}).get('first_name','')) diff --git a/check-connect.py b/check-connect.py new file mode 100644 index 0000000..6511826 --- /dev/null +++ b/check-connect.py @@ -0,0 +1,10 @@ +import socket, hashlib, subprocess +s=socket.socket(); s.settimeout(5) +try: + s.connect(('192.168.100.2',8728)) + print('API OK') +except Exception as e: + print('API FAIL: '+str(e)) +s.close() +r=subprocess.run(['ping','-c','1','192.168.100.2'],capture_output=True,timeout=5) +print('PING: '+('OK' if r.returncode==0 else 'FAIL')) diff --git a/check-disabled.py b/check-disabled.py new file mode 100644 index 0000000..83a147e --- /dev/null +++ b/check-disabled.py @@ -0,0 +1,29 @@ +import paramiko + +client = paramiko.SSHClient() +client.set_missing_host_key_policy(paramiko.AutoAddPolicy()) +client.connect('192.168.100.2', username='mi4', password='m14', look_for_keys=False, allow_agent=False) + +schedulers = ["Bulanan2", "Bulanan4", "vc-2K3J", "vc-5K10J", "Family", "DevB2"] + +for s in schedulers: + stdin, stdout, stderr = client.exec_command(f'/system scheduler get {s} disabled') + d = stdout.read().decode().strip() + e = stderr.read().decode().strip() + print(f"{s}: disabled={d}") + if e and "no such item" in e.lower(): + print(f" WARN: {e}") + +stdin, stdout, stderr = client.exec_command('/log print where topics~"scheduler"') +logs = stdout.read().decode().strip() +print(f"\n--- Scheduler log entries (last 20) ---") +for line in logs.split('\n')[-20:]: + print(line) + +stdin, stdout, stderr = client.exec_command('/log print where message~"error" or message~"failure"') +logs = stdout.read().decode().strip() +print(f"\n--- Error log entries (last 20) ---") +for line in logs.split('\n')[-20:]: + print(line) + +client.close() diff --git a/check-disabled2.py b/check-disabled2.py new file mode 100644 index 0000000..3d98902 --- /dev/null +++ b/check-disabled2.py @@ -0,0 +1,20 @@ +import paramiko + +client = paramiko.SSHClient() +client.set_missing_host_key_policy(paramiko.AutoAddPolicy()) +client.connect('192.168.100.2', username='mi4', password='m14', look_for_keys=False, allow_agent=False) + +# List all schedulers with their IDs and disabled status +stdin, stdout, stderr = client.exec_command('/system scheduler print terse') +lines = stdout.read().decode().strip().split('\n') +for line in lines: + if not line.strip(): + continue + print(line.strip()) + +print("\n--- Check disabled property differently ---") +# Try using print with flags +stdin, stdout, stderr = client.exec_command('/system scheduler print brief') +print(stdout.read().decode()) + +client.close() diff --git a/check-logs.py b/check-logs.py new file mode 100644 index 0000000..f66adb7 --- /dev/null +++ b/check-logs.py @@ -0,0 +1,95 @@ +import socket, hashlib +class R: + def __init__(s,h,p=8728): s.h=h; s.p=p; s.s=None + def c(s,u,p): + s.s=socket.socket(); s.s.settimeout(15) + s.s.connect((s.h,s.p)) + s._ws('/login','=name='+u,'=password='+p) + r=s._rr() + if any(isinstance(x,tuple) for x in r): + for x in r: + if isinstance(x,dict) and 'ret' in x: ch=x['ret'] + if ch and len(ch)==32: + hh=hashlib.md5(b'\x00'+p.encode()+bytes.fromhex(ch)).hexdigest() + s._ws('/login','=name='+u,'=response=00'+hh); s._rr() + return s + def _el(s,l): + if l<0x80: return bytes([l]) + elif l<0x4000: l|=0x8000; return bytes([(l>>8)&0xFF,l&0xFF]) + else: return bytes([0xC0|((l>>16)&0x0F),(l>>8)&0xFF,l&0xFF]) if l<0x200000 else bytes([0xE0|((l>>24)&0x07),(l>>16)&0xFF,(l>>8)&0xFF,l&0xFF]) + def _ww(s,w): s.s.sendall(s._el(len(w))+w.encode()) + def _ws(s,c,*ws): s._ww(c); [s._ww(w) for w in ws]; s.s.sendall(b'\x00') + def _rw(s): + f=s.s.recv(1) + if f==b'\x00': return None + lb=f[0]; l=lb + if lb&0x80: + if (lb&0xC0)==0x80: l=((lb&0x3F)<<8)+s.s.recv(1)[0] + elif (lb&0xE0)==0xC0: l=((lb&0x1F)<<16)+(s.s.recv(1)[0]<<8)+s.s.recv(1)[0] + else: l=((lb&0x0F)<<24)+(s.s.recv(1)[0]<<16)+(s.s.recv(1)[0]<<8)+s.s.recv(1)[0] + d=b'' + while len(d)=3 else '' + else: d[w]='' + rs.append(d) + elif r=='!trap': rs.append(('!trap',t)) + return rs + def cmd(s,c,**a): + ws=[c] + for k,v in a.items(): + ws.append(('?' if k.startswith('?') else '=')+k+'='+v) + s._ws(*ws) + return s._rr() + def close(s): + if s.s: s.s.close() + +api=R('remote.vpnmurahjogja.my.id',33206).c('mi4','m14') + +# Check system clock +clk = api.cmd('/system/clock/print') +if clk: + print('Clock: %s' % clk[0]) + +# Check scheduler last run +logs = api.cmd('/log/print', **{'?topics': 'system', '?message': 'Bulanan4'}) +print() +print('=== Recent logs for Bulanan4 ===') +for l in logs[:10]: + print(l.get('time','') + ' ' + l.get('topics','') + ' ' + l.get('message','')) + +# Also check for errors +logs2 = api.cmd('/log/print', **{'?topics': 'error'}) +print() +print('=== Errors ===') +for l in logs2[:5]: + print(l.get('time','') + ' ' + l.get('topics','') + ' ' + l.get('message','')) + +# Check system resource +res = api.cmd('/system/resource/print') +if res: + r = res[0] + print() + print('Uptime: %s' % r.get('uptime','')) + print('CPU: %s%%' % r.get('cpu-load','')) + +api.close() diff --git a/check-msr-key.py b/check-msr-key.py new file mode 100644 index 0000000..5afa49e --- /dev/null +++ b/check-msr-key.py @@ -0,0 +1,44 @@ +import base64, json + +# Try to decode pWJm +enc = "pWJm" + +# Base64 +try: + dec = base64.b64decode(enc) + print(f"base64: '{enc}' -> bytes {list(dec)}") +except: + print("not base64") + +# Check Mikhmon encryption in config +import os +config_path = r"C:\Users\Admin\dev\Mikhmon Server\mikhmon\include\config.php" +with open(config_path, 'r') as f: + content = f.read() + +# Find Msr line +for line in content.split('\n'): + if "'Msr'" in line or "'rm-jogja'" in line: + print(f"\nConfig line: {line.strip()[:200]}") + +# Check if there's a Mikhmon config.json or similar +for root, dirs, files in os.walk(r"C:\Users\Admin\dev\Mikhmon Server"): + for f in files: + if f.endswith('.json') or f.endswith('.ini') or f.endswith('.txt'): + path = os.path.join(root, f) + size = os.path.getsize(path) + if size < 100000: + print(f"\n=== {path} ({size} bytes) ===") + try: + with open(path, 'r') as fh: + for line in fh.read().split('\n')[:5]: + print(f" {line.strip()[:150]}") + except: + print(" (binary)") + +# Check for any .env or key file +print("\n=== Searching for MSR key files ===") +for root, dirs, files in os.walk(r"C:\Users\Admin\dev\Mikhmon Server"): + for f in files: + if 'bit' in f.lower() or 'father' in f.lower() or 'msrkey' in f.lower() or 'license' in f.lower() or 'key' in f.lower(): + print(f" {os.path.join(root, f)}") diff --git a/check-nadia-container.py b/check-nadia-container.py new file mode 100644 index 0000000..87fac07 --- /dev/null +++ b/check-nadia-container.py @@ -0,0 +1,100 @@ +import socket, hashlib +class R: + def __init__(self,h,p=8728): self.h=h; self.p=p; self.s=None + def c(self,u,p): + self.s=socket.socket(); self.s.settimeout(10) + self.s.connect((self.h,self.p)) + self._ws('/login','=name='+u,'=password='+p) + r=self._rr() + if any(isinstance(x,tuple) for x in r): + for x in r: + if isinstance(x,dict) and 'ret' in x: ch=x['ret'] + if ch and len(ch)==32: + h=hashlib.md5(b'\x00'+p.encode()+bytes.fromhex(ch)).hexdigest() + self._ws('/login','=name='+u,'=response=00'+h); self._rr() + return self + def _el(self,l): + if l<0x80: return bytes([l]) + elif l<0x4000: l|=0x8000; return bytes([(l>>8)&0xFF,l&0xFF]) + else: return bytes([0xC0|((l>>16)&0x0F),(l>>8)&0xFF,l&0xFF]) if l<0x200000 else bytes([0xE0|((l>>24)&0x07),(l>>16)&0xFF,(l>>8)&0xFF,l&0xFF]) + def _ww(self,w): self.s.sendall(self._el(len(w))+w.encode()) + def _ws(self,c,*ws): self._ww(c); [self._ww(w) for w in ws]; self.s.sendall(b'\x00') + def _rw(self): + f=self.s.recv(1) + if f==b'\x00': return None + lb=f[0]; l=lb + if lb&0x80: + if (lb&0xC0)==0x80: l=((lb&0x3F)<<8)+self.s.recv(1)[0] + elif (lb&0xE0)==0xC0: l=((lb&0x1F)<<16)+(self.s.recv(1)[0]<<8)+self.s.recv(1)[0] + else: l=((lb&0x0F)<<24)+(self.s.recv(1)[0]<<16)+(self.s.recv(1)[0]<<8)+self.s.recv(1)[0] + d=b'' + while len(d)=3 else '' + else: d[w]='' + rs.append(d) + elif r=='!trap': rs.append(('!trap',t)) + return rs + def cmd(self,c,**a): + ws=[c] + for k,v in a.items(): + ws.append(('?' if k.startswith('?') else '=')+k+'='+v) + self._ws(*ws) + return self._rr() + def close(self): + if self.s: self.s.close() + +api=R('remote.vpnmurahjogja.my.id',33206).c('mi4','m14') + +# Check Nadia +users = api.cmd('/ip/hotspot/user/print', **{'?name': 'nadia'}) +if users: + u = users[0] + print('NADIA:') + for k,v in u.items(): + print(' {}={}'.format(k,v)) +else: + print('NADIA: not found') + +# Check Bulanan4 scheduler +for si in api.cmd('/system/scheduler/print'): + if si['name'] == 'Bulanan4': + oe = si.get('on-event','') + print() + print('Bulanan4 scheduler:') + print(' disabled={}'.format(si.get('disabled',''))) + print(' interval={}'.format(si.get('interval',''))) + print(' pic count={}'.format(oe.count(':pic'))) + print(' pick count={}'.format(oe.count(':pick'))) + break + +# Check Bulanan4 users +users4 = api.cmd('/ip/hotspot/user/print', **{'?profile': 'Bulanan4'}) +print() +print('Bulanan4 users: {}'.format(len(users4))) +for u in users4: + n = u.get('name','') + c = u.get('comment','') + lim = u.get('limit-uptime','') + print(' {} | comment={} | limit-uptime={}'.format(n,c,lim)) + +api.close() diff --git a/check-notif-freq.py b/check-notif-freq.py new file mode 100644 index 0000000..5a7bda0 --- /dev/null +++ b/check-notif-freq.py @@ -0,0 +1,92 @@ +import socket, hashlib +class R: + def __init__(s,h,p=8728): s.h=h; s.p=p; s.s=None + def c(s,u,p): + s.s=socket.socket(); s.s.settimeout(15) + s.s.connect((s.h,s.p)) + s._ws('/login','=name='+u,'=password='+p) + r=s._rr() + if any(isinstance(x,tuple) for x in r): + for x in r: + if isinstance(x,dict) and 'ret' in x: ch=x['ret'] + if ch and len(ch)==32: + hh=hashlib.md5(b'\x00'+p.encode()+bytes.fromhex(ch)).hexdigest() + s._ws('/login','=name='+u,'=response=00'+hh); s._rr() + return s + def _el(s,l): + if l<0x80: return bytes([l]) + elif l<0x4000: l|=0x8000; return bytes([(l>>8)&0xFF,l&0xFF]) + else: return bytes([0xC0|((l>>16)&0x0F),(l>>8)&0xFF,l&0xFF]) if l<0x200000 else bytes([0xE0|((l>>24)&0x07),(l>>16)&0xFF,(l>>8)&0xFF,l&0xFF]) + def _ww(s,w): s.s.sendall(s._el(len(w))+w.encode()) + def _ws(s,c,*ws): s._ww(c); [s._ww(w) for w in ws]; s.s.sendall(b'\x00') + def _rw(s): + f=s.s.recv(1) + if f==b'\x00': return None + lb=f[0]; l=lb + if lb&0x80: + if (lb&0xC0)==0x80: l=((lb&0x3F)<<8)+s.s.recv(1)[0] + elif (lb&0xE0)==0xC0: l=((lb&0x1F)<<16)+(s.s.recv(1)[0]<<8)+s.s.recv(1)[0] + else: l=((lb&0x0F)<<24)+(s.s.recv(1)[0]<<16)+(s.s.recv(1)[0]<<8)+s.s.recv(1)[0] + d=b'' + while len(d)=3 else '' + else: d[w]='' + rs.append(d) + elif r=='!trap': rs.append(('!trap',t)) + return rs + def cmd(s,c,**a): + ws=[c] + for k,v in a.items(): + ws.append(('?' if k.startswith('?') else '=')+k+'='+v) + s._ws(*ws) + return s._rr() + def close(s): + if s.s: s.s.close() + +api=R('remote.vpnmurahjogja.my.id',33206).c('mi4','m14') + +# Check tunnelbroker +print('=== tunnelbroker on-event ===') +for si in api.cmd('/system/scheduler/print'): + if si['name'] == 'tunnelbroker': + print(si.get('on-event','')[:300]) + break + +# Check DevB2 - does it notify? +print() +print('=== DevB2 on-event (first 300 chars) ===') +for si in api.cmd('/system/scheduler/print'): + if si['name'] == 'DevB2': + oe = si.get('on-event','') + print(oe[:300]) + print('...') + print('notify-expire in oe: %s' % ('notify-expire' in oe)) + break + +# All user profiles summary +print() +print('=== All profiles summary ===') +for p in api.cmd('/ip/hotspot/user/profile/print'): + print(' %s: on-login has notify=%s' % (p['name'], 'expand' in p.get('on-login','') or 'set limit-uptime' in p.get('on-login',''))) + +api.close() diff --git a/check-notif.py b/check-notif.py new file mode 100644 index 0000000..233f223 --- /dev/null +++ b/check-notif.py @@ -0,0 +1,113 @@ +import socket, hashlib +class R: + def __init__(s,h,p=8728): s.h=h; s.p=p; s.s=None + def c(s,u,p): + s.s=socket.socket(); s.s.settimeout(15) + s.s.connect((s.h,s.p)) + s._ws('/login','=name='+u,'=password='+p) + r=s._rr() + if any(isinstance(x,tuple) for x in r): + for x in r: + if isinstance(x,dict) and 'ret' in x: ch=x['ret'] + if ch and len(ch)==32: + hh=hashlib.md5(b'\x00'+p.encode()+bytes.fromhex(ch)).hexdigest() + s._ws('/login','=name='+u,'=response=00'+hh); s._rr() + return s + def _el(s,l): + if l<0x80: return bytes([l]) + elif l<0x4000: l|=0x8000; return bytes([(l>>8)&0xFF,l&0xFF]) + else: return bytes([0xC0|((l>>16)&0x0F),(l>>8)&0xFF,l&0xFF]) if l<0x200000 else bytes([0xE0|((l>>24)&0x07),(l>>16)&0xFF,(l>>8)&0xFF,l&0xFF]) + def _ww(s,w): s.s.sendall(s._el(len(w))+w.encode()) + def _ws(s,c,*ws): s._ww(c); [s._ww(w) for w in ws]; s.s.sendall(b'\x00') + def _rw(s): + f=s.s.recv(1) + if f==b'\x00': return None + lb=f[0]; l=lb + if lb&0x80: + if (lb&0xC0)==0x80: l=((lb&0x3F)<<8)+s.s.recv(1)[0] + elif (lb&0xE0)==0xC0: l=((lb&0x1F)<<16)+(s.s.recv(1)[0]<<8)+s.s.recv(1)[0] + else: l=((lb&0x0F)<<24)+(s.s.recv(1)[0]<<16)+(s.s.recv(1)[0]<<8)+s.s.recv(1)[0] + d=b'' + while len(d)=3 else '' + else: d[w]='' + rs.append(d) + elif r=='!trap': rs.append(('!trap',t)) + return rs + def cmd(s,c,**a): + ws=[c] + for k,v in a.items(): + ws.append(('?' if k.startswith('?') else '=')+k+'='+v) + s._ws(*ws) + return s._rr() + def close(s): + if s.s: s.s.close() + +api=R('remote.vpnmurahjogja.my.id',33206).c('mi4','m14') + +# Check Family scheduler (the one with notifications) +print('=== Family scheduler ===') +for si in api.cmd('/system/scheduler/print'): + if si['name'] == 'Family': + oe = si.get('on-event','') + print(' disabled=%s' % si.get('disabled','')) + print(' interval=%s' % si.get('interval','')) + print(' contains :pic(typo)=%s' % (':pic ' in oe)) + print(' has notify call=%s' % ('notify-expire' in oe)) + print(' has telegram=%s' % ('5115036608' in oe)) + print() + print(' FULL on-event:') + print(oe) + print() + break +else: + print(' Family scheduler NOT FOUND') + +# Check Family users with notifications +print('=== Family profile users ===') +for u in api.cmd('/ip/hotspot/user/print', **{'?profile': 'Family'}): + n = u.get('name','') + c = u.get('comment','') + lim = u.get('limit-uptime','') + print(' %s | comment=%s | limit-uptime=%s' % (n, c, lim)) + +# Check all schedulers +print() +print('=== ALL Schedulers ===') +for si in api.cmd('/system/scheduler/print'): + n = si.get('name','') + dis = si.get('disabled','') + intr = si.get('interval','') + print(' %s interval=%s disabled=%s' % (n, intr, dis)) + +# Check notify-expire.php in container +import subprocess +ssh = subprocess.run( + ['ssh', '-o', 'StrictHostKeyChecking=no', 'rl', + 'docker exec mikhmon cat /var/www/html/notify-expire.php | head -3'], + capture_output=True, text=True, timeout=10) +print() +print('=== Container notify-expire.php header ===') +print(ssh.stdout[:200] if ssh.stdout else 'FAIL: ' + ssh.stderr[:200]) + +api.close() diff --git a/check-ntp.py b/check-ntp.py new file mode 100644 index 0000000..7de6d8a --- /dev/null +++ b/check-ntp.py @@ -0,0 +1,94 @@ +import socket, hashlib +class R: + def __init__(s,h,p=8728): s.h=h; s.p=p; s.s=None + def c(s,u,p): + s.s=socket.socket(); s.s.settimeout(15) + s.s.connect((s.h,s.p)) + s._ws('/login','=name='+u,'=password='+p) + r=s._rr() + if any(isinstance(x,tuple) for x in r): + for x in r: + if isinstance(x,dict) and 'ret' in x: ch=x['ret'] + if ch and len(ch)==32: + hh=hashlib.md5(b'\x00'+p.encode()+bytes.fromhex(ch)).hexdigest() + s._ws('/login','=name='+u,'=response=00'+hh); s._rr() + return s + def _el(s,l): + if l<0x80: return bytes([l]) + elif l<0x4000: l|=0x8000; return bytes([(l>>8)&0xFF,l&0xFF]) + else: return bytes([0xC0|((l>>16)&0x0F),(l>>8)&0xFF,l&0xFF]) if l<0x200000 else bytes([0xE0|((l>>24)&0x07),(l>>16)&0xFF,(l>>8)&0xFF,l&0xFF]) + def _ww(s,w): s.s.sendall(s._el(len(w))+w.encode()) + def _ws(s,c,*ws): s._ww(c); [s._ww(w) for w in ws]; s.s.sendall(b'\x00') + def _rw(s): + f=s.s.recv(1) + if f==b'\x00': return None + lb=f[0]; l=lb + if lb&0x80: + if (lb&0xC0)==0x80: l=((lb&0x3F)<<8)+s.s.recv(1)[0] + elif (lb&0xE0)==0xC0: l=((lb&0x1F)<<16)+(s.s.recv(1)[0]<<8)+s.s.recv(1)[0] + else: l=((lb&0x0F)<<24)+(s.s.recv(1)[0]<<16)+(s.s.recv(1)[0]<<8)+s.s.recv(1)[0] + d=b'' + while len(d)=3 else '' + else: d[w]='' + rs.append(d) + elif r=='!trap': rs.append(('!trap',t)) + return rs + def cmd(s,c,**a): + ws=[c] + for k,v in a.items(): + ws.append(('?' if k.startswith('?') else '=')+k+'='+v) + s._ws(*ws) + return s._rr() + def close(s): + if s.s: s.s.close() + +api=R('remote.vpnmurahjogja.my.id',33206).c('mi4','m14') + +# Check NTP +print('=== NTP Client ===') +ntp = api.cmd('/system/ntp/client/print') +if ntp: + for k,v in ntp[0].items(): + print(' %s=%s' % (k,v)) +else: + print(' (no ntp config)') + +# Check NTP servers +print() +print('=== NTP Servers ===') +srvs = api.cmd('/system/ntp/client/servers/print') +for s in srvs: + print(' %s' % s) + +# Try setting clock +print() +print('=== Setting clock to jul/24/2026 ===') +api.cmd('/system/clock/set', **{'date': 'jul/24/2026', 'time': '12:00:00'}) + +# Verify +clk = api.cmd('/system/clock/print') +if clk: + print('Clock now: date=%s time=%s' % (clk[0].get('date',''), clk[0].get('time',''))) + +api.close() diff --git a/check-reset.py b/check-reset.py new file mode 100644 index 0000000..d5ab182 --- /dev/null +++ b/check-reset.py @@ -0,0 +1,76 @@ +import socket, hashlib +class R: + def __init__(s,h,p=8728): s.h=h; s.p=p; s.s=None + def c(s,u,p): + s.s=socket.socket(); s.s.settimeout(60) + s.s.connect((s.h,s.p)) + s._ws('/login','=name='+u,'=password='+p) + r=s._rr() + if any(isinstance(x,tuple) for x in r): + for x in r: + if isinstance(x,dict) and 'ret' in x: ch=x['ret'] + if ch and len(ch)==32: + hh=hashlib.md5(b'\x00'+p.encode()+bytes.fromhex(ch)).hexdigest() + s._ws('/login','=name='+u,'=response=00'+hh); s._rr() + return s + def _el(s,l): + if l<0x80: return bytes([l]) + elif l<0x4000: l|=0x8000; return bytes([(l>>8)&0xFF,l&0xFF]) + else: return bytes([0xC0|((l>>16)&0x0F),(l>>8)&0xFF,l&0xFF]) if l<0x200000 else bytes([0xE0|((l>>24)&0x07),(l>>16)&0xFF,(l>>8)&0xFF,l&0xFF]) + def _ww(s,w): s.s.sendall(s._el(len(w))+w.encode()) + def _ws(s,c,*ws): s._ww(c); [s._ww(w) for w in ws]; s.s.sendall(b'\x00') + def _rw(s): + f=s.s.recv(1) + if f==b'\x00': return None + lb=f[0]; l=lb + if lb&0x80: + if (lb&0xC0)==0x80: l=((lb&0x3F)<<8)+s.s.recv(1)[0] + elif (lb&0xE0)==0xC0: l=((lb&0x1F)<<16)+(s.s.recv(1)[0]<<8)+s.s.recv(1)[0] + else: l=((lb&0x0F)<<24)+(s.s.recv(1)[0]<<16)+(s.s.recv(1)[0]<<8)+s.s.recv(1)[0] + d=b'' + while len(d)=3 else '' + else: d[w]='' + rs.append(d) + elif r=='!trap': rs.append(('!trap',t)) + return rs + def cmd(s,c,**a): + ws=[c] + for k,v in a.items(): + ws.append(('?' if k.startswith('?') else '=')+k+'='+v) + s._ws(*ws) + return s._rr() + def close(s): + if s.s: s.s.close() + +api=R('remote.vpnmurahjogja.my.id',33206).c('mi4','m14') + +r = api.cmd('/ip/hotspot/user/print', **{'?name': 'TMR-1'}) +for x in r: + if isinstance(x,dict): + print('USER TMR-1:', 'limit-uptime=', x.get('limit-uptime'), 'comment=', x.get('comment'), 'uptime=', x.get('uptime')) + +sch = api.cmd('/system/scheduler/print') +tmr_sch = [s.get('name') for s in sch if isinstance(s,dict) and s.get('name')=='TMR-1'] +print('scheduler TMR-1 exists:', bool(tmr_sch)) +api.close() diff --git a/check-runs.py b/check-runs.py new file mode 100644 index 0000000..f969497 --- /dev/null +++ b/check-runs.py @@ -0,0 +1,116 @@ +import socket, hashlib, 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') + +# Check current time and scheduler status +print("=== System Time ===") +resp = api.cmd('/system/clock/print') +print(f" {resp[0].get('date')} {resp[0].get('time')}") + +print("\n=== Scheduler Run Counts ===") +for item in api.cmd('/system/scheduler/print'): + name = item['name'] + rc = item.get('run-count', '?') + disabled = item.get('disabled', 'false') + interval = item.get('interval', '?') + next_run = item.get('next-run', '?') + if name in ["Log", "tunnelbroker", "backup-relay"]: + continue + print(f" {name}: run={rc} disabled={disabled} interval={interval} next={next_run}") + +# Wait 30s and check again +print("\nWaiting 30 seconds...") +time.sleep(30) + +print("=== After 30s ===") +for item in api.cmd('/system/scheduler/print'): + name = item['name'] + rc = item.get('run-count', '?') + if name in ["Log", "tunnelbroker", "backup-relay"]: + continue + print(f" {name}: run={rc}") + +api.close() diff --git a/check-scheduler-detail.py b/check-scheduler-detail.py new file mode 100644 index 0000000..4f86f47 --- /dev/null +++ b/check-scheduler-detail.py @@ -0,0 +1,94 @@ +import socket, hashlib +class R: + def __init__(s,h,p=8728): s.h=h; s.p=p; s.s=None + def c(s,u,p): + s.s=socket.socket(); s.s.settimeout(15) + s.s.connect((s.h,s.p)) + s._ws('/login','=name='+u,'=password='+p) + r=s._rr() + if any(isinstance(x,tuple) for x in r): + for x in r: + if isinstance(x,dict) and 'ret' in x: ch=x['ret'] + if ch and len(ch)==32: + hh=hashlib.md5(b'\x00'+p.encode()+bytes.fromhex(ch)).hexdigest() + s._ws('/login','=name='+u,'=response=00'+hh); s._rr() + return s + def _el(s,l): + if l<0x80: return bytes([l]) + elif l<0x4000: l|=0x8000; return bytes([(l>>8)&0xFF,l&0xFF]) + else: return bytes([0xC0|((l>>16)&0x0F),(l>>8)&0xFF,l&0xFF]) if l<0x200000 else bytes([0xE0|((l>>24)&0x07),(l>>16)&0xFF,(l>>8)&0xFF,l&0xFF]) + def _ww(s,w): s.s.sendall(s._el(len(w))+w.encode()) + def _ws(s,c,*ws): s._ww(c); [s._ww(w) for w in ws]; s.s.sendall(b'\x00') + def _rw(s): + f=s.s.recv(1) + if f==b'\x00': return None + lb=f[0]; l=lb + if lb&0x80: + if (lb&0xC0)==0x80: l=((lb&0x3F)<<8)+s.s.recv(1)[0] + elif (lb&0xE0)==0xC0: l=((lb&0x1F)<<16)+(s.s.recv(1)[0]<<8)+s.s.recv(1)[0] + else: l=((lb&0x0F)<<24)+(s.s.recv(1)[0]<<16)+(s.s.recv(1)[0]<<8)+s.s.recv(1)[0] + d=b'' + while len(d)=3 else '' + else: d[w]='' + rs.append(d) + elif r=='!trap': rs.append(('!trap',t)) + return rs + def cmd(s,c,**a): + ws=[c] + for k,v in a.items(): + ws.append(('?' if k.startswith('?') else '=')+k+'='+v) + s._ws(*ws) + return s._rr() + def close(s): + if s.s: s.s.close() + +api=R('remote.vpnmurahjogja.my.id',33206).c('mi4','m14') + +for si in api.cmd('/system/scheduler/print'): + if si['name'] == 'Bulanan4': + oe = si.get('on-event','') + print('=== Bulanan4 on-event FULL ===') + print(oe) + print('=== END ===') + print() + print('disabled=%s' % si.get('disabled','')) + print('interval=%s' % si.get('interval','')) + print('contains :pic=%s' % (':pic ' in oe)) + print('contains :pick=%s' % (':pick ' in oe)) + break + +# Check all Bulanan4 users with details +print() +print('=== Bulanan4 user details ===') +nowd = 'jul/24/2026' +nowt = 8*60 # 08:00 in minutes for comparison +print('Current ref: date=%s time=%d min' % (nowd, nowt)) +for u in api.cmd('/ip/hotspot/user/print', **{'?profile': 'Bulanan4'}): + n = u.get('name','') + c = u.get('comment','') + lim = u.get('limit-uptime','') + dis = u.get('disabled','') + print('%s | comment=%s | limit-uptime=%s | disabled=%s' % (n, c, lim, dis)) + +api.close() diff --git a/check-script-oneliner.py b/check-script-oneliner.py new file mode 100644 index 0000000..d511862 --- /dev/null +++ b/check-script-oneliner.py @@ -0,0 +1,111 @@ +import socket, hashlib, time + +s = socket.socket(socket.AF_INET, socket.SOCK_STREAM) +s.settimeout(10) +s.connect(('192.168.100.2', 8728)) + +def el(l): + if l < 0x80: return bytes([l]) + elif l < 0x4000: l |= 0x8000; return bytes([(l>>8)&0xFF, l&0xFF]) + elif l < 0x200000: l |= 0xC00000; return bytes([(l>>16)&0xFF, (l>>8)&0xFF, l&0xFF]) + elif l < 0x10000000: l |= 0xE0000000; return bytes([(l>>24)&0xFF, (l>>16)&0xFF, (l>>8)&0xFF, l&0xFF]) + else: return bytes([0xF0, (l>>24)&0xFF, (l>>16)&0xFF, (l>>8)&0xFF, l&0xFF]) + +def ww(w): s.sendall(el(len(w)) + (w.encode() if isinstance(w, str) else w)) + +def ws(c, *a): + ww(c) + for x in a: ww(x) + s.sendall(b'\x00') + +def rb(): return s.recv(1) + +def rw(): + f = rb() + if f == b'\x00': return None + lb = f[0] + if lb & 0x80: + if (lb & 0xC0) == 0x80: l = ((lb & 0x3F) << 8) + rb()[0] + elif (lb & 0xE0) == 0xC0: l = ((lb & 0x1F) << 16) + (rb()[0] << 8) + rb()[0] + elif (lb & 0xF0) == 0xE0: l = ((lb & 0x0F) << 24) + (rb()[0] << 16) + (rb()[0] << 8) + rb()[0] + else: l = (rb()[0] << 24) + (rb()[0] << 16) + (rb()[0] << 8) + rb()[0] + else: l = lb + d = b'' + while len(d) < l: + c = s.recv(l - len(d)) + if not c: break + d += c + return d.decode('utf-8', errors='replace') + +def rss(): + ws = [] + while True: + w = rw() + if w is None: break + ws.append(w) + return ws + +def rr(): + rs = [] + while True: + sent = rss() + if not sent: break + r = sent[0]; t = sent[1:] + if r == '!done': break + elif r == '!re': + d = {} + for w in t: + if '=' in w: + p = w.split('=', 2) + d[p[1]] = p[2] if len(p) >= 3 else '' + else: d[w] = '' + rs.append(d) + elif r == '!trap': + rs.append(('!trap', t)) + return rs + +def cmd(c, **a): + w = [c] + for k, v in a.items(): + if k.startswith('?'): w.append(f'{k}={v}') + else: w.append(f'={k}={v}') + ws(*w) + return rr() + +# Login +ws('/login', '=name=mi4', '=password=m14') +r = rr() +if any(isinstance(x, tuple) and x[0] == '!trap' for x in r): + for x in r: + if isinstance(x, dict) and 'ret' in x: ch = x['ret'] + if ch and len(ch) == 32: + resp = '00' + hashlib.md5(b'\x00' + b'm14' + bytes.fromhex(ch)).hexdigest() + ws('/login', '=name=mi4', f'=response={resp}') + rr() + +# Dump on-event Bulanan2 +print("=== Bulanan2 on-event ===") +for s_item in cmd('/system/scheduler/print'): + if s_item['name'] == 'Bulanan2': + oe = s_item.get('on-event', '') + lines = oe.split('\n') + print(f"Total lines: {len(lines)}") + for i, l in enumerate(lines): + print(f"{i+1:3d}: {l}") + # Count :pic vs :pick + print(f"\n:pik occurrences: {oe.count(':pic,') + oe.count(':pic')} (should be 0 if fully :pick)") + print(f":pick occurrences: {oe.count(':pick')}") + break + +# Dump on-login Bulanan2 +print("\n=== Bulanan2 on-login ===") +for p_item in cmd('/ip/hotspot/profile/print'): + if p_item['name'] == 'Bulanan2': + ol = p_item.get('on-login', '') + lines = ol.split('\n') + print(f"Total lines: {len(lines)}") + for i, l in enumerate(lines): + print(f"{i+1:3d}: {l}") + break + +s.close() diff --git a/check-telegram-schedulers.py b/check-telegram-schedulers.py new file mode 100644 index 0000000..737d2ff --- /dev/null +++ b/check-telegram-schedulers.py @@ -0,0 +1,89 @@ +import socket, hashlib + +s=socket.socket(socket.AF_INET,socket.SOCK_STREAM); s.settimeout(10); s.connect(('192.168.100.2',8728)) +def el(l): + if l<0x80: return bytes([l]) + elif l<0x4000: l|=0x8000; return bytes([(l>>8)&0xFF,l&0xFF]) + elif l<0x200000: l|=0xC00000; return bytes([(l>>16)&0xFF,(l>>8)&0xFF,l&0xFF]) + elif l<0x10000000: l|=0xE0000000; return bytes([(l>>24)&0xFF,(l>>16)&0xFF,(l>>8)&0xFF,l&0xFF]) + else: return bytes([0xF0,(l>>24)&0xFF,(l>>16)&0xFF,(l>>8)&0xFF,l&0xFF]) +def ww(w): s.sendall(el(len(w))+(w.encode() if isinstance(w,str) else w)) +def ws(c,*a): ww(c); [ww(x) for x in a]; s.sendall(b'\x00') +def rb(): return s.recv(1) +def rw(): + f=rb() + if f==b'\x00': return None + lb=f[0] + if lb&0x80: + if (lb&0xC0)==0x80: l=((lb&0x3F)<<8)+rb()[0] + elif (lb&0xE0)==0xC0: l=((lb&0x1F)<<16)+(rb()[0]<<8)+rb()[0] + elif (lb&0xF0)==0xE0: l=((lb&0x0F)<<24)+(rb()[0]<<16)+(rb()[0]<<8)+rb()[0] + else: l=(rb()[0]<<24)+(rb()[0]<<16)+(rb()[0]<<8)+rb()[0] + else: l=lb + d=b'' + while len(d)=3 else '' + else: d[w]='' + rs.append(d) + elif r=='!trap': rs.append(('!trap',t)) + return rs +def cmd(c,**a): + w=[c] + for k,v in a.items(): + if k.startswith('?'): w.append(k+'='+str(v)) + else: w.append('='+k+'='+str(v)) + ws(*w) + return rr() + +ws('/login','=name=mi4','=password=m14') +r=rr() +if any(True for x in r if isinstance(x,tuple) and x[0]=='!trap'): + ch=None + for x in r: + if isinstance(x,dict) and 'ret' in x: ch=x['ret'] + if ch and len(ch)==32: + resp='00'+hashlib.md5(b'\x00'+b'm14'+bytes.fromhex(ch)).hexdigest() + ws('/login','=name=mi4','=response='+resp) + rr() + +print("=== Scheduler on-event Telegram check ===") +for si in cmd('/system/scheduler/print'): + name=si['name'] + if name in ['Log','tunnelbroker','backup-relay']: continue + oe=si.get('on-event','') + has_tg='5115036608' in oe or 'telegram' in oe.lower() or 'sendMessage' in oe + print(f" {name}: Telegram={'YES' if has_tg else 'no'}") + if has_tg: + lines=oe.split('\n') + for i,l in enumerate(lines): + if '5115036608' in l or 'telegram' in l.lower() or 'sendMessage' in l: + print(f" TG line: {l.strip()[:120]}") + +print("\n=== Script Telegram check ===") +for sc in cmd('/system/script/print'): + name=sc['name'] + src=sc.get('source','') + has_tg='5115036608' in src or 'telegram' in src.lower() or 'sendMessage' in src + print(f" {name}: Telegram={'YES' if has_tg else 'no'}") + +s.close() diff --git a/check-test123.py b/check-test123.py new file mode 100644 index 0000000..7c70116 --- /dev/null +++ b/check-test123.py @@ -0,0 +1,108 @@ +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: + h = hashlib.md5(b'\x00' + password.encode() + bytes.fromhex(chal)).hexdigest() + self._write_sentence('/login', f'=name={username}', f'=response=00{h}') + 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}") + 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') + +print("=== User test123 ===") +r = api.cmd('/ip/hotspot/user/print', **{'?name': 'test123'}) +if r: + u = r[0] + print(f" Comment: {u.get('comment')}") + print(f" limit-uptime: {u.get('limit-uptime')}") + print(f" Profile: {u.get('profile')}") + print(f" Uptime: {u.get('uptime')}") +else: + print(" NOT FOUND") + +print("\n=== Scheduler Run Counts ===") +for item in api.cmd('/system/scheduler/print'): + name = item['name'] + if name in ['Log', 'tunnelbroker', 'backup-relay']: continue + print(f" {name}: run={item.get('run-count')} next={item.get('next-run')}") + +print("\n=== Total Hotspot Users ===") +all_users = list(api.cmd('/ip/hotspot/user/print')) +print(f" {len(all_users)} users") + +api.close() diff --git a/check-tg-thread.py b/check-tg-thread.py new file mode 100644 index 0000000..805c41d --- /dev/null +++ b/check-tg-thread.py @@ -0,0 +1,17 @@ +import requests, json + +r = requests.get('https://api.telegram.org/bot5115036608:AAGeyXVq5eNvdAkaCQHU9NfH16uAAiSKiuk/getUpdates?offset=0') +d = r.json() +msgs = [m for m in d.get('result', []) if 'message' in m and m['message'].get('text') == 'Test topic id'] +for m in msgs: + msg = m['message'] + tid = msg.get('message_thread_id', 'none') + cid = msg['chat']['id'] + from_id = msg['from']['id'] + print(f'chat={cid} thread_id={tid} from={from_id} text={msg.get("text","")}') +if not msgs: + print('no matching messages found') + for m in d.get('result', []): + if 'message' in m: + mm = m['message'] + print(f" text='{mm.get('text','')[:60]}' thread_id={mm.get('message_thread_id','-')}") diff --git a/check-tg.py b/check-tg.py new file mode 100644 index 0000000..0ddbe35 --- /dev/null +++ b/check-tg.py @@ -0,0 +1,10 @@ +import requests, json + +r = requests.get('https://api.telegram.org/bot5115036608:AAGeyXVq5eNvdAkaCQHU9NfH16uAAiSKiuk/getUpdates?offset=-10') +d = r.json() +msgs = [m for m in d.get('result',[]) if 'message' in m] +print(f"Total messages: {len(msgs)}") +for m in msgs[-5:]: + cid = m['message']['chat']['id'] + txt = m['message'].get('text','')[:150] + print(f" chat={cid} text={txt}") diff --git a/check-tg2.py b/check-tg2.py new file mode 100644 index 0000000..fee5f90 --- /dev/null +++ b/check-tg2.py @@ -0,0 +1,21 @@ +import requests, json + +# Get updates without offset +r = requests.get('https://api.telegram.org/bot5115036608:AAGeyXVq5eNvdAkaCQHU9NfH16uAAiSKiuk/getUpdates') +d = r.json() +print(f"ok: {d.get('ok')}, count: {len(d.get('result',[]))}") +msgs = [m for m in d['result'] if 'message' in m] +for m in msgs[-10:]: + mid = m['message']['message_id'] + cid = m['message']['chat']['id'] + txt = m['message'].get('text', m['message'].get('caption', ''))[:200] + print(f" mid={mid} chat={cid} txt={txt}") + +# Also check the test we sent +print("\n--- Check laporkan group test ---") +r2 = requests.get('https://api.telegram.org/bot5115036608:AAGeyXVq5eNvdAkaCQHU9NfH16uAAiSKiuk/getUpdates?offset=0&limit=100') +d2 = r2.json() +msgs2 = [m for m in d2['result'] if 'message' in m and m['message']['chat']['id'] == -1002554941429] +print(f"Messages in laporkan: {len(msgs2)}") +for m in msgs2[-5:]: + print(f" mid={m['message']['message_id']} text={m['message'].get('text','')[:200]}") diff --git a/check-users.py b/check-users.py new file mode 100644 index 0000000..9c23657 --- /dev/null +++ b/check-users.py @@ -0,0 +1,80 @@ +import socket, hashlib + +class R: + def __init__(self,h,p=8728): self.h=h; self.p=p; self.s=None + def c(self,u,p): + self.s=socket.socket(socket.AF_INET,socket.SOCK_STREAM); self.s.settimeout(15) + self.s.connect((self.h,self.p)) + self._ws('/login','=name='+u,'=password='+p) + r=self._rr() + if any(isinstance(x,tuple) for x in r): + for x in r: + if isinstance(x,dict) and 'ret' in x: ch=x['ret'] + if ch and len(ch)==32: + h=hashlib.md5(b'\x00'+p.encode()+bytes.fromhex(ch)).hexdigest() + self._ws('/login','=name='+u,'=response=00'+h) + self._rr() + return self + def _el(self,l): + if l<0x80: return bytes([l]) + elif l<0x4000: l|=0x8000; return bytes([(l>>8)&0xFF,l&0xFF]) + else: return bytes([0xC0|((l>>16)&0x0F),(l>>8)&0xFF,l&0xFF]) if l<0x200000 else bytes([0xE0|((l>>24)&0x07),(l>>16)&0xFF,(l>>8)&0xFF,l&0xFF]) + def _ww(self,w): self.s.sendall(self._el(len(w))+w.encode('utf-8')) + def _ws(self,c,*ws): self._ww(c); [self._ww(w) for w in ws]; self.s.sendall(b'\x00') + def _rw(self): + f=self._rb() + if f==b'\x00': return None + lb=f[0]; l=lb + if lb&0x80: + if (lb&0xC0)==0x80: l=((lb&0x3F)<<8)+self._rb()[0] + elif (lb&0xE0)==0xC0: l=((lb&0x1F)<<16)+(self._rb()[0]<<8)+self._rb()[0] + else: l=((lb&0x0F)<<24)+(self._rb()[0]<<16)+(self._rb()[0]<<8)+self._rb()[0] + d=b'' + while len(d)=3 else '' + else: d[w]='' + rs.append(d) + elif r=='!trap': rs.append(('!trap',t)) + return rs + def cmd(self,c,**a): + ws=[c] + for k,v in a.items(): ws.append(('?' if k.startswith('?') else '=')+k+'='+v) + self._ws(*ws) + return self._rr() + def close(self): + if self.s: self.s.close(); self.s=None + +api=R('192.168.100.2').c('mi4','m14') +print("=== All hotspot users ===") +for u in api.cmd('/ip/hotspot/user/print'): + name = u.get('name','') + if 'tg_test' in name or 'test' in name: + print(" %s | %s | profile=%s | uptime=%s | comment=%s" % (u.get('.id',''), name, u.get('profile',''), u.get('limit-uptime',''), u.get('comment','')[:40] if u.get('comment') else '-')) +print("---") +for u in api.cmd('/ip/hotspot/user/print'): + name = u.get('name','') + prof = u.get('profile','') + if prof == 'Family': + print(" %s | %s | uptime=%s | limit-bytes=%s | comment=%s" % (u.get('.id',''), name, u.get('limit-uptime',''), u.get('limit-bytes-total',''), u.get('comment','')[:30] if u.get('comment') else '-')) +api.close() diff --git a/create-a55-test.py b/create-a55-test.py new file mode 100644 index 0000000..a27320f --- /dev/null +++ b/create-a55-test.py @@ -0,0 +1,110 @@ +import socket, hashlib + +class R: + def __init__(self,h,p=8728): self.h=h; self.p=p; self.s=None + def c(self,u,p): + self.s=socket.socket(socket.AF_INET,socket.SOCK_STREAM); self.s.settimeout(10) + self.s.connect((self.h,self.p)) + self._ws('/login','=name='+u,'=password='+p) + r=self._rr() + if any(isinstance(x,tuple) and x[0]=='!trap' for x in r): + for x in r: + if isinstance(x,dict) and 'ret' in x: ch=x['ret'] + if ch and len(ch)==32: + h=hashlib.md5(b'\x00'+p.encode()+bytes.fromhex(ch)).hexdigest() + self._ws('/login','=name='+u,'=response=00'+h) + self._rr() + return self + def _el(self,l): + if l<0x80: return bytes([l]) + elif l<0x4000: l|=0x8000; return bytes([(l>>8)&0xFF,l&0xFF]) + elif l<0x200000: l|=0xC00000; return bytes([(l>>16)&0xFF,(l>>8)&0xFF,l&0xFF]) + elif l<0x10000000: l|=0xE0000000; return bytes([(l>>24)&0xFF,(l>>16)&0xFF,(l>>8)&0xFF,l&0xFF]) + else: return bytes([0xF0,(l>>24)&0xFF,(l>>16)&0xFF,(l>>8)&0xFF,l&0xFF]) + def _ww(self,w): self.s.sendall(self._el(len(w))+w.encode('utf-8')) + def _ws(self,c,*ws): self._ww(c); [self._ww(w) for w in ws]; self.s.sendall(b'\x00') + def _rb(self): return self.s.recv(1) + def _rw(self): + f=self._rb() + if f==b'\x00': return None + lb=f[0] + if lb&0x80: + if (lb&0xC0)==0x80: l=((lb&0x3F)<<8)+self._rb()[0] + elif (lb&0xE0)==0xC0: l=((lb&0x1F)<<16)+(self._rb()[0]<<8)+self._rb()[0] + elif (lb&0xF0)==0xE0: l=((lb&0x0F)<<24)+(self._rb()[0]<<16)+(self._rb()[0]<<8)+self._rb()[0] + else: l=(self._rb()[0]<<24)+(self._rb()[0]<<16)+(self._rb()[0]<<8)+self._rb()[0] + else: l=lb + d=b'' + while len(d)=3 else '' + else: d[w]='' + rs.append(d) + elif r=='!trap': rs.append(('!trap',t)) + return rs + def cmd(self,c,**a): + ws=[c] + for k,v in a.items(): + if k.startswith('?'): ws.append(f'{k}={v}') + else: ws.append(f'={k}={v}') + self._ws(*ws) + return self._rr() + def close(self): + if self.s: self.s.close(); self.s=None + +api=R('192.168.100.2').c('mi4','m14') + +print("Router time:") +r=api.cmd('/system/clock/print')[0] +print(f" {r['date']} {r['time']}") + +# Create user for A55-Labibah - already expired (comment yesterday) +# MAC-bound so only A55 can login +api.cmd('/ip/hotspot/user/add', **{ + 'name': 'labibah_tes', + 'password': '123', + 'profile': 'Bulanan2', + 'mac-address': 'CA:1A:02:C2:8C:1E', + 'comment': 'jul/06/2026 00:00:00' +}) +print("\nCreated user: labibah_tes / 123") +print(" Profile: Bulanan2 (di-expire otomatis)") +print(" MAC: CA:1A:02:C2:8C:1E (hanya A55-Labibah)") +print(" Comment: jul/06/2026 (SUDAH EXPIRED)") + +# Verify +r = list(api.cmd('/ip/hotspot/user/print', **{'?name': 'labibah_tes'})) +if r: + u = r[0] + print(f"\nVerify: name={u['name']} profile={u.get('profile')} mac={u.get('mac-address')} comment={u.get('comment')} limit-uptime={u.get('limit-uptime')}") + print("Status: BELUM login (limit-uptime kosong)") + print("\nPrediksi: saat A55 login lewat hotspot,") + print(" - on-login script akan deteksi comment jul/06 (expired)") + print(" - limit-uptime langsung diset = 1s") + print(" - device kena kick/limit otomatis โœ…") +else: + print("GAGAL create user!") + +api.close() diff --git a/create-test-reset.py b/create-test-reset.py new file mode 100644 index 0000000..e8a4769 --- /dev/null +++ b/create-test-reset.py @@ -0,0 +1,78 @@ +import socket, hashlib + +class R: + def __init__(self,h,p=8728): self.h=h; self.p=p; self.s=None + def c(self,u,p): + self.s=socket.socket(socket.AF_INET,socket.SOCK_STREAM); self.s.settimeout(15) + self.s.connect((self.h,self.p)) + self._ws('/login','=name='+u,'=password='+p) + r=self._rr() + if any(isinstance(x,tuple) for x in r): + for x in r: + if isinstance(x,dict) and 'ret' in x: ch=x['ret'] + if ch and len(ch)==32: + h=hashlib.md5(b'\x00'+p.encode()+bytes.fromhex(ch)).hexdigest() + self._ws('/login','=name='+u,'=response=00'+h) + self._rr() + return self + def _el(self,l): + if l<0x80: return bytes([l]) + elif l<0x4000: l|=0x8000; return bytes([(l>>8)&0xFF,l&0xFF]) + else: return bytes([0xC0|((l>>16)&0x0F),(l>>8)&0xFF,l&0xFF]) if l<0x200000 else bytes([0xE0|((l>>24)&0x07),(l>>16)&0xFF,(l>>8)&0xFF,l&0xFF]) + def _ww(self,w): self.s.sendall(self._el(len(w))+w.encode('utf-8')) + def _ws(self,c,*ws): self._ww(c); [self._ww(w) for w in ws]; self.s.sendall(b'\x00') + def _rw(self): + f=self._rb() + if f==b'\x00': return None + lb=f[0]; l=lb + if lb&0x80: + if (lb&0xC0)==0x80: l=((lb&0x3F)<<8)+self._rb()[0] + elif (lb&0xE0)==0xC0: l=((lb&0x1F)<<16)+(self._rb()[0]<<8)+self._rb()[0] + else: l=((lb&0x0F)<<24)+(self._rb()[0]<<16)+(self._rb()[0]<<8)+self._rb()[0] + d=b'' + while len(d)=3 else '' + else: d[w]='' + rs.append(d) + elif r=='!trap': rs.append(('!trap',t)) + return rs + def cmd(self,c,**a): + ws=[c] + for k,v in a.items(): ws.append(('?' if k.startswith('?') else '=')+k+'='+v) + self._ws(*ws) + return self._rr() + def close(self): + if self.s: self.s.close(); self.s=None + +api=R('192.168.100.2').c('mi4','m14') + +# Create test user with expired date +api.cmd('/ip/hotspot/user/add', name='test_reset_family', profile='Family', comment='23/07/2026 01:00:00', **{'limit-uptime': '1s'}) +print("Created test user") + +# Verify +for u in api.cmd('/ip/hotspot/user/print', **{'?name': 'test_reset_family'}): + print(" %s | uptime=%s | comment=%s" % (u.get('name',''), u.get('limit-uptime',''), u.get('comment',''))) + +api.close() diff --git a/debug-bytes.py b/debug-bytes.py new file mode 100644 index 0000000..c552eb8 --- /dev/null +++ b/debug-bytes.py @@ -0,0 +1,135 @@ +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() diff --git a/debug-replace.py b/debug-replace.py new file mode 100644 index 0000000..be162af --- /dev/null +++ b/debug-replace.py @@ -0,0 +1,127 @@ +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(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 Bulanan2 on-event +resp = api.cmd('/system/scheduler/print', **{'?name': 'Bulanan2'}) +raw = resp[0]['on-event'] + +print(f"Raw length: {len(raw)}") +print(f"Raw first 200: {raw[:200]}") +print(f":pic count: {raw.count(':pic')}") +print(f":pick count: {raw.count(':pick')}") + +# Find all :pic positions +idx = 0 +positions = [] +while True: + idx = raw.find(':pic', idx) + if idx == -1: break + positions.append(idx) + print(f" :pic at {idx}: ...{raw[max(0,idx-8):idx+12]}...") + idx += 4 + +print(f"\nReplacing :pic -> :pick...") +fixed = raw.replace(':pic', ':pick') +print(f"Fixed length: {len(fixed)}") +print(f"Fixed :pic count: {fixed.count(':pic')}") +print(f"Fixed :pick count: {fixed.count(':pick')}") +match_count = 0 +for i, (a, b) in enumerate(zip(raw, fixed)): + if a != b: + match_count += 1 + if match_count <= 10: + print(f" Diff at {i}: '{a}' -> '{b}' (ctx: ...{raw[max(0,i-5):i+5]}...)") + +# Also try manual byte-level check +raw_bytes = raw.encode('utf-8') +fixed_bytes = fixed.encode('utf-8') +print(f"\nRaw bytes length: {len(raw_bytes)}") +print(f"Fixed bytes length: {len(fixed_bytes)}") +print(f"Expected diff: {raw.count(':pic')} chars * 1 = {raw.count(':pic')} diff") +print(f"Actual diff: {len(fixed_bytes) - len(raw_bytes)}") + +api.close() diff --git a/debug-set.py b/debug-set.py new file mode 100644 index 0000000..5fcd1ae --- /dev/null +++ b/debug-set.py @@ -0,0 +1,30 @@ +import librouteros + +api = librouteros.connect('192.168.100.2', 'mi4', 'm14', port=8728) + +# Get Bulanan2 details +items = list(api('/system/scheduler/print')) +for item in items: + if item['name'] == 'Bulanan2': + print(f'Bulanan2:') + print(f' .id = {item[".id"]}') + oe = item['on-event'] + print(f' on-event length = {len(oe)}') + print(f' on-event first 50: {oe[:50]}') + + # Test: set on-event to very simple script + simple = ':put "test123"' + print(f'\nSetting on-event to: {simple}') + api('/system/scheduler/set', **{'.id': item['.id'], 'on-event': simple}) + + # Re-read + items2 = list(api('/system/scheduler/print')) + for item2 in items2: + if item2['name'] == 'Bulanan2': + oe2 = item2['on-event'] + print(f'After set, on-event = {oe2}') + print(f'Match: {oe2 == simple}') + break + break + +api.close() diff --git a/debug-set2.py b/debug-set2.py new file mode 100644 index 0000000..33c9f8d --- /dev/null +++ b/debug-set2.py @@ -0,0 +1,62 @@ +import librouteros + +api = librouteros.connect('192.168.100.2', 'mi4', 'm14', port=8728) + +items = list(api('/system/scheduler/print')) + +for item in items: + if item['name'] == 'Bulanan2': + oid = item['.id'] + print(f'Bulanan2 .id = {oid}') + print(f'on-event len = {len(item["on-event"])}') + + # Try setting via different .id formats + test_cmds = [ + # Test 1: numeric id with comment + ('comment', f'TEST_{oid}'), + ] + + for field, val in test_cmds: + api('/system/scheduler/set', **{'.id': oid, field: val}) + + # Verify comment + items = list(api('/system/scheduler/print')) + for i in items: + if i['name'] == 'Bulanan2': + print(f'Comment after: {i.get("comment")}') + + # Test: set on-event with simplest possible script + print(f'\nSetting on-event with no special chars...') + try: + api('/system/scheduler/set', **{'.id': oid, 'on-event': 'put hello'}) + except Exception as e: + print(f' ERROR: {e}') + + # Re-read + items = list(api('/system/scheduler/print')) + for i in items: + if i['name'] == 'Bulanan2': + oe = i.get('on-event', '') + print(f'on-event now: {oe[:60]}') + print(f'Changed: {oe != item["on-event"]}') + + # Test: set on-event with : character + print(f'\nSetting on-event with colon...') + try: + api('/system/scheduler/set', **{'.id': oid, 'on-event': ':put "hi"'}) + except Exception as e: + print(f' ERROR: {e}') + + items = list(api('/system/scheduler/print')) + for i in items: + if i['name'] == 'Bulanan2': + oe = i.get('on-event', '') + print(f'on-event now: {oe[:60]}') + expected = ':put "hi"' + print(f'Changed to :put: {oe == expected}') + + # Restore comment + api('/system/scheduler/set', **{'.id': oid, 'comment': 'Monitor Profile Bulanan2'}) + break + +api.close() diff --git a/debug-ssh.py b/debug-ssh.py new file mode 100644 index 0000000..abff835 --- /dev/null +++ b/debug-ssh.py @@ -0,0 +1,27 @@ +import paramiko + +client = paramiko.SSHClient() +client.set_missing_host_key_policy(paramiko.AutoAddPolicy()) +client.connect('192.168.100.2', username='mi4', password='m14', look_for_keys=False, allow_agent=False) + +# Test various ways to get on-event +tests = [ + '/system scheduler get Bulanan2 on-event', + '/system scheduler print terse where name=Bulanan2', + '/system scheduler print terse ?name=Bulanan2', + ':put [/system scheduler get Bulanan2 on-event]', + ':put [/system scheduler find where name=Bulanan2]', +] + +for cmd in tests: + print(f"\n--- {cmd[:60]} ---") + stdin, stdout, stderr = client.exec_command(cmd) + import time + time.sleep(0.5) + out = stdout.read().decode().strip() + err = stderr.read().decode().strip() + print(f"out: {out[:200]}") + if err: + print(f"err: {err[:200]}") + +client.close() diff --git a/deploy-to-somewhere/README.md b/deploy-to-somewhere/README.md new file mode 100644 index 0000000..6fc01f6 --- /dev/null +++ b/deploy-to-somewhere/README.md @@ -0,0 +1,129 @@ +# Telegram Expiry Notifier + Reset Button โ€” Deploy ke Server + +## Overview + +Mengganti direct call Telegram API dari RouterOS dengan PHP intermediary agar bisa kirim inline button "Reset" untuk user hotspot yang expired. + +### Arsitektur + +``` +RouterOS scheduler (Family) + โ†“ call HTTP GET + โ†“ +notify-expire.php (server) + โ†“ POST ke Telegram API + โ†“ +โš ๏ธ User X expired [๐Ÿ”„ Reset] (ke topik Notif Expire) + โ†“ admin klik "Reset" + โ†“ callback_query = "reset_X" ke Bot API + โ†“ +tg-cb-poll scheduler (router, tiap 60s) + โ†“ call HTTP GET + โ†“ +telegram-cb.php (server) + โ†“ polling getUpdates โ†’ proses callback + โ†“ RouterOS API: set limit-uptime=0, comment="", reset-counters + โ†“ +โœ… User X has been reset (ke topik Notif Expire) +``` + +### Files + +| File | Fungsi | +|------|--------| +| `notify-expire.php` | Kirim notif + inline button ke Telegram | +| `telegram-cb.php` | Polling callback query + eksekusi reset | +| `setup.sh` | Setup script untuk server (install PHP + deploy) | + +### Prerequisites (Server) + +- Linux server (tested on Ubuntu/Debian) +- PHP 7.4+ dengan `curl` extension +- Akses keluar ke `api.telegram.org` (port 443) +- Akses ke RouterOS API (`192.168.100.2:8728`) +- `curl` binary di PATH (untuk internal call dari PHP ke Telegram) + +### Setup Cepat + +```bash +# 1. Copy files ke server +scp notify-expire.php player@somewhere:/home/player/telegram-notif/ +scp telegram-cb.php player@somewhere:/home/player/telegram-notif/ +scp setup.sh player@somewhere:/home/player/telegram-notif/ + +# 2. SSH ke server +ssh player@somewhere + +# 3. Run setup +cd ~/telegram-notif +chmod +x setup.sh +./setup.sh + +# 4. Start PHP built-in server (background) +nohup php -S 0.0.0.0:8080 -t /home/player/telegram-notif > /tmp/telegram-notif.log 2>&1 & +``` + +### Update RouterOS Scheduler + +#### A. Family scheduler โ€” notifikasi + +Ganti on-event: + +```bash +# OLD +/tool fetch url=("https://api.telegram.org/bot" . $bot . "/sendMessage?chat_id=" . $chat . "&text=" . $msg) http-method=get keep-result=no + +# NEW โ€” arahkan ke IP Tailscale server +/tool fetch url=("http://100.100.31.46:8080/notify-expire.php?key=msr-2026-reset&name=" . $name) http-method=get keep-result=no +``` + +#### B. Tambah scheduler polling callback + +```bash +/system scheduler add name=tg-cb-poll interval=00:01:00 start-time=startup on-event=":delay 10s;/tool fetch url=\"http://100.100.31.46:8080/telegram-cb.php?key=msr-2026-reset\" http-method=get keep-result=no" disabled=no +``` + +### Secret Key + +`msr-2026-reset` โ€” shared secret di URL parameter `key`. Diganti sesuai kebutuhan. + +### Bot Config + +- Token: `5115036608:AAGeyXVq5eNvdAkaCQHU9NfH16uAAiSKiuk` +- Group: `-1002554941429` (laporkan) +- Topic ID: `42` (Notif Expire) + +### RouterOS Credentials (untuk callback reset) + +- IP: `192.168.100.2` +- User: `mi4` +- Password: `m14` + +### Troubleshooting + +**Logs**: +```bash +tail -f /tmp/telegram-notif.log +``` + +**Test notifikasi**: +```bash +curl "http://localhost:8080/notify-expire.php?key=msr-2026-reset&name=test_user" +``` + +**Test reset langsung**: +```bash +curl "http://localhost:8080/telegram-cb.php?key=msr-2026-reset&test=test_user" +``` + +**PHP curl extension**: +```bash +sudo apt install php-curl +``` + +### File Reference + +Source files di `C:\Users\Admin\dev\Mikhmon Server\mikhmon\`: +- `notify-expire.php` +- `process/telegram-cb.php` +- `lib/routeros_api.class.php` (dependency untuk koneksi ke RouterOS) diff --git a/deploy-to-somewhere/deploy.ps1 b/deploy-to-somewhere/deploy.ps1 new file mode 100644 index 0000000..2de05ad --- /dev/null +++ b/deploy-to-somewhere/deploy.ps1 @@ -0,0 +1,44 @@ +# Telegram Expiry Notifier โ€” Deploy ke somewhere +# Jalankan script ini. Masukkan password SSH player@somewhere ketika diminta. + +$server = "player@somewhere" +$remoteDir = "/home/player/telegram-notif" +$scriptDir = Split-Path -Parent $MyInvocation.MyCommand.Path + +Write-Host "=== Deploy Telegram Notifier ke $server ===" -ForegroundColor Cyan +Write-Host "" + +# 1. Buat remote directory via SSH +Write-Host "[1/4] Membuat remote directory..." -ForegroundColor Yellow +Start-Process -Wait -WindowStyle Normal -FilePath "ssh" -ArgumentList "-t $server mkdir -p $remoteDir" +Write-Host "" + +# 2. Copy files via SCP +Write-Host "[2/4] Copy files via SCP..." -ForegroundColor Yellow +Start-Process -Wait -WindowStyle Normal -FilePath "scp" -ArgumentList "$scriptDir\notify-expire.php $scriptDir\telegram-cb.php $scriptDir\routeros_api.class.php $scriptDir\setup.sh ${server}:${remoteDir}/" +Write-Host "" + +# 3. Run setup via SSH +Write-Host "[3/4] Run remote setup..." -ForegroundColor Yellow +Write-Host " (akan menginstall PHP + membuat systemd service)" -ForegroundColor Gray +Start-Process -Wait -WindowStyle Normal -FilePath "ssh" -ArgumentList "-t $server cd $remoteDir && chmod +x setup.sh && ./setup.sh" +Write-Host "" + +# 4. Test +Write-Host "[4/4] Test..." -ForegroundColor Yellow +Start-Process -Wait -WindowStyle Normal -FilePath "ssh" -ArgumentList "-t $server curl -s 'http://localhost:8080/notify-expire.php?key=msr-2026-reset&name=test_deploy'" +Write-Host "" + +Write-Host "=== Done ===" -ForegroundColor Green +Write-Host "" +Write-Host "Update RouterOS scheduler:" +Write-Host " Family on-event:" +Write-Host ' /tool fetch url=("http://100.100.31.46:8080/notify-expire.php?key=msr-2026-reset&name=" . $name) http-method=get keep-result=no' +Write-Host "" +Write-Host " Tambah scheduler:" +Write-Host ' /system scheduler add name=tg-cb-poll interval=00:01:00 start-time=startup on-event=":delay 10s;/tool fetch url=\"http://100.100.31.46:8080/telegram-cb.php?key=msr-2026-reset\" http-method=get keep-result=no" disabled=no' +Write-Host "" +Write-Host "Remote management:" +Write-Host " ssh $server" +Write-Host " sudo systemctl status telegram-notif" +Write-Host " sudo journalctl -u telegram-notif -f" diff --git a/deploy-to-somewhere/notify-expire.php b/deploy-to-somewhere/notify-expire.php new file mode 100644 index 0000000..66e6f86 --- /dev/null +++ b/deploy-to-somewhere/notify-expire.php @@ -0,0 +1,49 @@ + array(array(array( + 'text' => '๐Ÿ”„ Reset', + 'callback_data' => "reset_{$user}" + ))) +)); + +$data = http_build_query(array( + 'chat_id' => $chat, + 'message_thread_id' => $thread, + 'text' => $text, + 'parse_mode' => 'Markdown', + 'reply_markup' => $markup, +)); + +function tg_post($url, $data) { + $ctx = stream_context_create(array('http' => array( + 'method' => 'POST', + 'header' => 'Content-Type: application/x-www-form-urlencoded', + 'content' => $data, + 'timeout' => 10, + ))); + return @file_get_contents($url, false, $ctx); +} + +$r = tg_post("https://api.telegram.org/bot{$bot}/sendMessage", $data); +if ($r === false) { http_response_code(500); echo "fail: http error"; exit; } + +$result = json_decode($r, true); +if (isset($result['ok']) && $result['ok']) { + echo "sent: " . $result['result']['message_id']; +} else { + http_response_code(500); + echo "fail: " . (isset($result['description']) ? $result['description'] : 'no response'); +} diff --git a/deploy-to-somewhere/routeros_api.class.php b/deploy-to-somewhere/routeros_api.class.php new file mode 100644 index 0000000..dad2e86 --- /dev/null +++ b/deploy-to-somewhere/routeros_api.class.php @@ -0,0 +1,647 @@ +debug) { + echo $text . "\n"; + } + } + + + /** + * + * + * @param string $length + * + * @return void + */ + public function encodeLength($length) + { + if ($length < 0x80) { + $length = chr($length); + } elseif ($length < 0x4000) { + $length |= 0x8000; + $length = chr(($length >> 8) & 0xFF) . chr($length & 0xFF); + } elseif ($length < 0x200000) { + $length |= 0xC00000; + $length = chr(($length >> 16) & 0xFF) . chr(($length >> 8) & 0xFF) . chr($length & 0xFF); + } elseif ($length < 0x10000000) { + $length |= 0xE0000000; + $length = chr(($length >> 24) & 0xFF) . chr(($length >> 16) & 0xFF) . chr(($length >> 8) & 0xFF) . chr($length & 0xFF); + } elseif ($length >= 0x10000000) { + $length = chr(0xF0) . chr(($length >> 24) & 0xFF) . chr(($length >> 16) & 0xFF) . chr(($length >> 8) & 0xFF) . chr($length & 0xFF); + } + + return $length; + } + + + /** + * Login to RouterOS + * + * @param string $ip Hostname (IP or domain) of the RouterOS server + * @param string $login The RouterOS username + * @param string $password The RouterOS password + * + * @return boolean If we are connected or not + */ + public function connect($ip, $login, $password) + { + for ($ATTEMPT = 1; $ATTEMPT <= $this->attempts; $ATTEMPT++) { + $this->connected = false; + $PROTOCOL = ($this->ssl ? 'ssl://' : '' ); + $context = stream_context_create(array('ssl' => array('ciphers' => 'ADH:ALL', 'verify_peer' => false, 'verify_peer_name' => false))); + $this->debug('Connection attempt #' . $ATTEMPT . ' to ' . $PROTOCOL . $ip . ':' . $this->port . '...'); + $this->socket = @stream_socket_client($PROTOCOL . $ip.':'. $this->port, $this->error_no, $this->error_str, $this->timeout, STREAM_CLIENT_CONNECT,$context); + if ($this->socket) { + socket_set_timeout($this->socket, $this->timeout); + $this->write('/login', false); + $this->write('=name=' . $login, false); + $this->write('=password=' . $password); + $RESPONSE = $this->read(false); + if (isset($RESPONSE[0])) { + if ($RESPONSE[0] == '!done') { + if (!isset($RESPONSE[1])) { + // Login method post-v6.43 + $this->connected = true; + break; + } else { + // Login method pre-v6.43 + $MATCHES = array(); + if (preg_match_all('/[^=]+/i', $RESPONSE[1], $MATCHES)) { + if ($MATCHES[0][0] == 'ret' && strlen($MATCHES[0][1]) == 32) { + $this->write('/login', false); + $this->write('=name=' . $login, false); + $this->write('=response=00' . md5(chr(0) . $password . pack('H*', $MATCHES[0][1]))); + $RESPONSE = $this->read(false); + if (isset($RESPONSE[0]) && $RESPONSE[0] == '!done') { + $this->connected = true; + break; + } + } + } + } + } + } + fclose($this->socket); + } + sleep($this->delay); + } + + if ($this->connected) { + $this->debug('Connected...'); + } else { + $this->debug('Error...'); + } + return $this->connected; + } + + + /** + * Disconnect from RouterOS + * + * @return void + */ + public function disconnect() + { + // let's make sure this socket is still valid. it may have been closed by something else + if( is_resource($this->socket) ) { + fclose($this->socket); + } + $this->connected = false; + $this->debug('Disconnected...'); + } + + + /** + * Parse response from Router OS + * + * @param array $response Response data + * + * @return array Array with parsed data + */ + public function parseResponse($response) + { + if (is_array($response)) { + $PARSED = array(); + $CURRENT = null; + $singlevalue = null; + foreach ($response as $x) { + if (in_array($x, array('!fatal','!re','!trap'))) { + if ($x == '!re') { + $CURRENT =& $PARSED[]; + } else { + $CURRENT =& $PARSED[$x][]; + } + } elseif ($x != '!done') { + $MATCHES = array(); + if (preg_match_all('/[^=]+/i', $x, $MATCHES)) { + if ($MATCHES[0][0] == 'ret') { + $singlevalue = $MATCHES[0][1]; + } + $CURRENT[$MATCHES[0][0]] = (isset($MATCHES[0][1]) ? $MATCHES[0][1] : ''); + } + } + } + + if (empty($PARSED) && !is_null($singlevalue)) { + $PARSED = $singlevalue; + } + + return $PARSED; + } else { + return array(); + } + } + + + /** + * Parse response from Router OS + * + * @param array $response Response data + * + * @return array Array with parsed data + */ + public function parseResponse4Smarty($response) + { + if (is_array($response)) { + $PARSED = array(); + $CURRENT = null; + $singlevalue = null; + foreach ($response as $x) { + if (in_array($x, array('!fatal','!re','!trap'))) { + if ($x == '!re') { + $CURRENT =& $PARSED[]; + } else { + $CURRENT =& $PARSED[$x][]; + } + } elseif ($x != '!done') { + $MATCHES = array(); + if (preg_match_all('/[^=]+/i', $x, $MATCHES)) { + if ($MATCHES[0][0] == 'ret') { + $singlevalue = $MATCHES[0][1]; + } + $CURRENT[$MATCHES[0][0]] = (isset($MATCHES[0][1]) ? $MATCHES[0][1] : ''); + } + } + } + foreach ($PARSED as $key => $value) { + $PARSED[$key] = $this->arrayChangeKeyName($value); + } + return $PARSED; + if (empty($PARSED) && !is_null($singlevalue)) { + $PARSED = $singlevalue; + } + } else { + return array(); + } + } + + + /** + * Change "-" and "/" from array key to "_" + * + * @param array $array Input array + * + * @return array Array with changed key names + */ + public function arrayChangeKeyName(&$array) + { + if (is_array($array)) { + foreach ($array as $k => $v) { + $tmp = str_replace("-", "_", $k); + $tmp = str_replace("/", "_", $tmp); + if ($tmp) { + $array_new[$tmp] = $v; + } else { + $array_new[$k] = $v; + } + } + return $array_new; + } else { + return $array; + } + } + + + /** + * Read data from Router OS + * + * @param boolean $parse Parse the data? default: true + * + * @return array Array with parsed or unparsed data + */ + public function read($parse = true) + { + $RESPONSE = array(); + $receiveddone = false; + while (true) { + // Read the first byte of input which gives us some or all of the length + // of the remaining reply. + $BYTE = ord(fread($this->socket, 1)); + $LENGTH = 0; + // If the first bit is set then we need to remove the first four bits, shift left 8 + // and then read another byte in. + // We repeat this for the second and third bits. + // If the fourth bit is set, we need to remove anything left in the first byte + // and then read in yet another byte. + if ($BYTE & 128) { + if (($BYTE & 192) == 128) { + $LENGTH = (($BYTE & 63) << 8) + ord(fread($this->socket, 1)); + } else { + if (($BYTE & 224) == 192) { + $LENGTH = (($BYTE & 31) << 8) + ord(fread($this->socket, 1)); + $LENGTH = ($LENGTH << 8) + ord(fread($this->socket, 1)); + } else { + if (($BYTE & 240) == 224) { + $LENGTH = (($BYTE & 15) << 8) + ord(fread($this->socket, 1)); + $LENGTH = ($LENGTH << 8) + ord(fread($this->socket, 1)); + $LENGTH = ($LENGTH << 8) + ord(fread($this->socket, 1)); + } else { + $LENGTH = ord(fread($this->socket, 1)); + $LENGTH = ($LENGTH << 8) + ord(fread($this->socket, 1)); + $LENGTH = ($LENGTH << 8) + ord(fread($this->socket, 1)); + $LENGTH = ($LENGTH << 8) + ord(fread($this->socket, 1)); + } + } + } + } else { + $LENGTH = $BYTE; + } + + $_ = ""; + + // If we have got more characters to read, read them in. + if ($LENGTH > 0) { + $_ = ""; + $retlen = 0; + while ($retlen < $LENGTH) { + $toread = $LENGTH - $retlen; + $_ .= fread($this->socket, $toread); + $retlen = strlen($_); + } + $RESPONSE[] = $_; + $this->debug('>>> [' . $retlen . '/' . $LENGTH . '] bytes read.'); + } + + // If we get a !done, make a note of it. + if ($_ == "!done") { + $receiveddone = true; + } + + $STATUS = socket_get_status($this->socket); + if ($LENGTH > 0) { + $this->debug('>>> [' . $LENGTH . ', ' . $STATUS['unread_bytes'] . ']' . $_); + } + + if ((!$this->connected && !$STATUS['unread_bytes']) || ($this->connected && !$STATUS['unread_bytes'] && $receiveddone)) { + break; + } + } + + if ($parse) { + $RESPONSE = $this->parseResponse($RESPONSE); + } + + return $RESPONSE; + } + + + /** + * Write (send) data to Router OS + * + * @param string $command A string with the command to send + * @param mixed $param2 If we set an integer, the command will send this data as a "tag" + * If we set it to boolean true, the funcion will send the comand and finish + * If we set it to boolean false, the funcion will send the comand and wait for next command + * Default: true + * + * @return boolean Return false if no command especified + */ + public function write($command, $param2 = true) + { + if ($command) { + $data = explode("\n", $command); + foreach ($data as $com) { + $com = trim($com); + fwrite($this->socket, $this->encodeLength(strlen($com)) . $com); + $this->debug('<<< [' . strlen($com) . '] ' . $com); + } + + if (gettype($param2) == 'integer') { + fwrite($this->socket, $this->encodeLength(strlen('.tag=' . $param2)) . '.tag=' . $param2 . chr(0)); + $this->debug('<<< [' . strlen('.tag=' . $param2) . '] .tag=' . $param2); + } elseif (gettype($param2) == 'boolean') { + fwrite($this->socket, ($param2 ? chr(0) : '')); + } + + return true; + } else { + return false; + } + } + + + /** + * Write (send) data to Router OS + * + * @param string $com A string with the command to send + * @param array $arr An array with arguments or queries + * + * @return array Array with parsed + */ + public function comm($com, $arr = array()) + { + $count = count($arr); + $this->write($com, !$arr); + $i = 0; + if ($this->isIterable($arr)) { + foreach ($arr as $k => $v) { + switch ($k[0]) { + case "?": + $el = "$k=$v"; + break; + case "~": + $el = "$k~$v"; + break; + default: + $el = "=$k=$v"; + break; + } + + $last = ($i++ == $count - 1); + $this->write($el, $last); + } + } + + return $this->read(); + } + + /** + * Standard destructor + * + * @return void + */ + public function __destruct() + { + $this->disconnect(); + } +} + +// encrypt decript + +function encrypt($string, $key=128) { + $result = ''; + for($i=0, $k= strlen($string); $i<$k; $i++) { + $char = substr($string, $i, 1); + $keychar = substr($key, ($i % strlen($key))-1, 1); + $char = chr(ord($char)+ord($keychar)); + $result .= $char; + } + return base64_encode($result); +} +function decrypt($string, $key=128) { + $result = ''; + $string = base64_decode($string); + for($i=0, $k=strlen($string); $i< $k ; $i++) { + $char = substr($string, $i, 1); + $keychar = substr($key, ($i % strlen($key))-1, 1); + $char = chr(ord($char)-ord($keychar)); + $result .= $char; + } + return $result; +} + +// Reformat date time MikroTik +// by Laksamadi Guko + +function formatInterval($dtm){ +$val_convert = $dtm; +$new_format = str_replace("s", "", str_replace("m", "m ", str_replace("h", "h ", str_replace("d", "d ", str_replace("w", "w ", $val_convert))))); +return $new_format; +} + +function formatDTM($dtm){ +if(substr($dtm, 1,1) == "d" || substr($dtm, 2,1) == "d"){ + $day = explode("d",$dtm)[0]."d"; + $day = str_replace("d", "d ", str_replace("w", "w ", $day)); + $dtm = explode("d",$dtm)[1]; +}elseif(substr($dtm, 1,1) == "w" && substr($dtm, 3,1) == "d" || substr($dtm, 2,1) == "w" && substr($dtm, 4,1) == "d"){ + $day = explode("d",$dtm)[0]."d"; + $day = str_replace("d", "d ", str_replace("w", "w ", $day)); + $dtm = explode("d",$dtm)[1]; +}elseif (substr($dtm, 1,1) == "w" || substr($dtm, 2,1) == "w" ) { + $day = explode("w",$dtm)[0]."w"; + $day = str_replace("d", "d ", str_replace("w", "w ", $day)); + $dtm = explode("w",$dtm)[1]; +} + +// secs +if(strlen($dtm) == "2" && substr($dtm, -1) == "s"){ + $format = $day." 00:00:0".substr($dtm, 0,-1); +}elseif(strlen($dtm) == "3" && substr($dtm, -1) == "s"){ + $format = $day." 00:00:".substr($dtm, 0,-1); +//minutes +}elseif(strlen($dtm) == "2" && substr($dtm, -1) == "m"){ + $format = $day." 00:0".substr($dtm, 0,-1).":00"; +}elseif(strlen($dtm) == "3" && substr($dtm, -1) == "m"){ + $format = $day." 00:".substr($dtm, 0,-1).":00"; +//hours +}elseif(strlen($dtm) == "2" && substr($dtm, -1) == "h"){ + $format = $day." 0".substr($dtm, 0,-1).":00:00"; +}elseif(strlen($dtm) == "3" && substr($dtm, -1) == "h"){ + $format = $day." ".substr($dtm, 0,-1).":00:00"; + +//minutes -secs +}elseif(strlen($dtm) == "4" && substr($dtm, -1) == "s" && substr($dtm,1,-2) == "m"){ + $format = $day." "."00:0".substr($dtm, 0,1).":0".substr($dtm, 2,-1); +}elseif(strlen($dtm) == "5" && substr($dtm, -1) == "s" && substr($dtm,1,-3) == "m"){ + $format = $day." "."00:0".substr($dtm, 0,1).":".substr($dtm, 2,-1); +}elseif(strlen($dtm) == "5" && substr($dtm, -1) == "s" && substr($dtm,2,-2) == "m"){ + $format = $day." "."00:".substr($dtm, 0,2).":0".substr($dtm, 3,-1); +}elseif(strlen($dtm) == "6" && substr($dtm, -1) == "s" && substr($dtm,2,-3) == "m"){ + $format = $day." "."00:".substr($dtm, 0,2).":".substr($dtm, 3,-1); + +//hours -secs +}elseif(strlen($dtm) == "4" && substr($dtm, -1) == "s" && substr($dtm,1,-2) == "h"){ + $format = $day." 0".substr($dtm, 0,1).":00:0".substr($dtm, 2,-1); +}elseif(strlen($dtm) == "5" && substr($dtm, -1) == "s" && substr($dtm,1,-3) == "h"){ + $format = $day." 0".substr($dtm, 0,1).":00:".substr($dtm, 2,-1); +}elseif(strlen($dtm) == "5" && substr($dtm, -1) == "s" && substr($dtm,2,-2) == "h"){ + $format = $day." ".substr($dtm, 0,2).":00:0".substr($dtm, 3,-1); +}elseif(strlen($dtm) == "6" && substr($dtm, -1) == "s" && substr($dtm,2,-3) == "h"){ + $format = $day." ".substr($dtm, 0,2).":00:".substr($dtm, 3,-1); + +//hours -secs +}elseif(strlen($dtm) == "4" && substr($dtm, -1) == "m" && substr($dtm,1,-2) == "h"){ + $format = $day." 0".substr($dtm, 0,1).":0".substr($dtm, 2,-1).":00"; +}elseif(strlen($dtm) == "5" && substr($dtm, -1) == "m" && substr($dtm,1,-3) == "h"){ + $format = $day." 0".substr($dtm, 0,1).":".substr($dtm, 2,-1).":00"; +}elseif(strlen($dtm) == "5" && substr($dtm, -1) == "m" && substr($dtm,2,-2) == "h"){ + $format = $day." ".substr($dtm, 0,2).":0".substr($dtm, 3,-1).":00"; +}elseif(strlen($dtm) == "6" && substr($dtm, -1) == "m" && substr($dtm,2,-3) == "h"){ + $format = $day." ".substr($dtm, 0,2).":".substr($dtm, 3,-1).":00"; + +//hours minutes secs +}elseif(strlen($dtm) == "6" && substr($dtm, -1) == "s" && substr($dtm,3,-2) == "m" && substr($dtm,1,-4) == "h"){ + $format = $day." 0".substr($dtm, 0,1).":0".substr($dtm, 2,-3).":0".substr($dtm, 4,-1); +}elseif(strlen($dtm) == "7" && substr($dtm, -1) == "s" && substr($dtm,3,-3) == "m" && substr($dtm,1,-5) == "h"){ + $format = $day." 0".substr($dtm, 0,1).":0".substr($dtm, 2,-4).":".substr($dtm, 4,-1); +}elseif(strlen($dtm) == "7" && substr($dtm, -1) == "s" && substr($dtm,4,-2) == "m" && substr($dtm,1,-5) == "h"){ + $format = $day." 0".substr($dtm, 0,1).":".substr($dtm, 2,-3).":0".substr($dtm, 5,-1); +}elseif(strlen($dtm) == "8" && substr($dtm, -1) == "s" && substr($dtm,4,-3) == "m" && substr($dtm,1,-6) == "h"){ + $format = $day." 0".substr($dtm, 0,1).":".substr($dtm, 2,-4).":".substr($dtm, 5,-1); +}elseif(strlen($dtm) == "7" && substr($dtm, -1) == "s" && substr($dtm,4,-2) == "m" && substr($dtm,2,-4) == "h"){ + $format = $day." ".substr($dtm, 0,2).":0".substr($dtm, 3,-3).":0".substr($dtm, 5,-1); +}elseif(strlen($dtm) == "8" && substr($dtm, -1) == "s" && substr($dtm,4,-3) == "m" && substr($dtm,2,-5) == "h"){ + $format = $day." ".substr($dtm, 0,2).":0".substr($dtm, 3,-4).":".substr($dtm, 5,-1); +}elseif(strlen($dtm) == "8" && substr($dtm, -1) == "s" && substr($dtm,5,-2) == "m" && substr($dtm,2,-5) == "h"){ + $format = $day." ".substr($dtm, 0,2).":".substr($dtm, 3,-3).":0".substr($dtm, 6,-1); +}elseif(strlen($dtm) == "9" && substr($dtm, -1) == "s" && substr($dtm,5,-3) == "m" && substr($dtm,2,-6) == "h"){ + $format = $day." ".substr($dtm, 0,2).":".substr($dtm, 3,-4).":".substr($dtm, 6,-1); + +}else{ + $format = $dtm; +} +return $format; +} + + +function randN($length) { + $chars = "23456789"; + $charArray = str_split($chars); + $charCount = strlen($chars); + $result = ""; + for($i=1;$i<=$length;$i++) + { + $randChar = rand(0,$charCount-1); + $result .= $charArray[$randChar]; + } + return $result; +} + +function randUC($length) { + $chars = "ABCDEFGHJKLMNPRSTUVWXYZ"; + $charArray = str_split($chars); + $charCount = strlen($chars); + $result = ""; + for($i=1;$i<=$length;$i++) + { + $randChar = rand(0,$charCount-1); + $result .= $charArray[$randChar]; + } + return $result; +} +function randLC($length) { + $chars = "abcdefghijkmnprstuvwxyz"; + $charArray = str_split($chars); + $charCount = strlen($chars); + $result = ""; + for($i=1;$i<=$length;$i++) + { + $randChar = rand(0,$charCount-1); + $result .= $charArray[$randChar]; + } + return $result; +} + +function randULC($length) { + $chars = "ABCDEFGHJKLMNPRSTUVWXYZabcdefghijkmnprstuvwxyz"; + $charArray = str_split($chars); + $charCount = strlen($chars); + $result = ""; + for($i=1;$i<=$length;$i++) + { + $randChar = rand(0,$charCount-1); + $result .= $charArray[$randChar]; + } + return $result; +} + +function randNLC($length) { + $chars = "23456789abcdefghijkmnprstuvwxyz"; + $charArray = str_split($chars); + $charCount = strlen($chars); + $result = ""; + for($i=1;$i<=$length;$i++) + { + $randChar = rand(0,$charCount-1); + $result .= $charArray[$randChar]; + } + return $result; +} + +function randNUC($length) { + $chars = "23456789ABCDEFGHJKLMNPRSTUVWXYZ"; + $charArray = str_split($chars); + $charCount = strlen($chars); + $result = ""; + for($i=1;$i<=$length;$i++) + { + $randChar = rand(0,$charCount-1); + $result .= $charArray[$randChar]; + } + return $result; +} + +function randNULC($length) { + $chars = "23456789ABCDEFGHJKLMNPRSTUVWXYZabcdefghijkmnprstuvwxyz"; + $charArray = str_split($chars); + $charCount = strlen($chars); + $result = ""; + for($i=1;$i<=$length;$i++) + { + $randChar = rand(0,$charCount-1); + $result .= $charArray[$randChar]; + } + return $result; +} + +?> diff --git a/deploy-to-somewhere/setup.sh b/deploy-to-somewhere/setup.sh new file mode 100644 index 0000000..0c6a550 --- /dev/null +++ b/deploy-to-somewhere/setup.sh @@ -0,0 +1,50 @@ +#!/bin/bash +set -e + +echo "=== Telegram Expiry Notifier โ€” Setup ===" + +DIR="/home/player/telegram-notif" +PORT=8080 +SERVICE="telegram-notif" + +# 1. Install PHP + curl +echo "[1/4] Installing PHP + php-curl..." +sudo apt update -qq +sudo apt install -y -qq php-cli php-curl curl 2>/dev/null + +# 2. Create directory +echo "[2/4] Creating directory..." +mkdir -p "$DIR" + +# 3. Copy files (run this script from the same dir as the PHP files) +echo "[3/4] Copying files..." +SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)" +cp "$SCRIPT_DIR/notify-expire.php" "$DIR/" +cp "$SCRIPT_DIR/telegram-cb.php" "$DIR/" + +# 4. Create systemd service +echo "[4/4] Creating systemd service..." +sudo tee /etc/systemd/system/$SERVICE.service > /dev/null < array( + 'method' => 'POST', + 'header' => 'Content-Type: application/x-www-form-urlencoded', + 'content' => $data, + 'timeout' => 15, + ))); + $r = @file_get_contents($url, false, $ctx); + } else { + $r = @file_get_contents($url, false, stream_context_create(array('http' => array('timeout' => 5)))); + } + return $r ? json_decode($r, true) : null; +} + +if (isset($_GET['test'])) { + $username = preg_replace('/[^a-zA-Z0-9_.@-]/', '', $_GET['test']); + if (!$username) { die('invalid username'); } + include_once(dirname(__DIR__) . '/lib/routeros_api.class.php'); + $api = new RouterosAPI(); + $api->debug = false; + $api->port = 33206; + if (!$api->connect('remote.vpnmurahjogja.my.id', 'mi4', 'm14')) { die('conn fail'); } + $found = $api->comm('/ip/hotspot/user/print', array('?name' => $username)); + if (empty($found)) { $api->disconnect(); die('user not found'); } + $uid = $found[0]['.id']; + $api->comm('/ip/hotspot/user/set', array('.id' => $uid, 'limit-uptime' => '0', 'comment' => '')); + $api->comm('/ip/hotspot/user/reset-counters', array('.id' => $uid)); + $sch = $api->comm('/system/scheduler/print', array('?name' => $username)); + if (!empty($sch) && isset($sch[0]['.id'])) { + $api->comm('/system/scheduler/remove', array('.id' => $sch[0]['.id'])); + } + $api->disconnect(); + tg_api('sendMessage', array( + 'chat_id' => '-1002554941429', + 'message_thread_id' => 42, + 'text' => "โœ… *{$username}* has been reset\nReady to use again", + 'parse_mode' => 'Markdown', + )); + echo "reset ok: {$username}"; + exit; +} + +$updates = tg_api('getUpdates', array('timeout' => 5, 'allowed_updates' => '["callback_query"]')); +if (!$updates || !isset($updates['ok']) || !$updates['ok']) { echo "no updates"; exit; } + +$processed = 0; +$last_id = 0; + +include_once(dirname(__DIR__) . '/lib/routeros_api.class.php'); + +foreach ($updates['result'] as $u) { + $cb = isset($u['callback_query']) ? $u['callback_query'] : null; + if (!$cb) continue; + + $data = isset($cb['data']) ? $cb['data'] : ''; + $cb_id = isset($cb['id']) ? $cb['id'] : ''; + $last_id = max($last_id, $u['update_id']); + + if (substr($data, 0, 6) !== 'reset_') continue; + + $username = substr($data, 6); + $username = preg_replace('/[^a-zA-Z0-9_.@-]/', '', $username); + if (!$username) continue; + + $api = new RouterosAPI(); + $api->debug = false; + $api->port = 33206; + if (!$api->connect('remote.vpnmurahjogja.my.id', 'mi4', 'm14')) { + echo "conn fail for {$username} "; + continue; + } + + $found = $api->comm('/ip/hotspot/user/print', array('?name' => $username)); + if (empty($found)) { + $api->disconnect(); + tg_api('answerCallbackQuery', array( + 'callback_query_id' => $cb_id, + 'text' => "User {$username} not found", + 'show_alert' => true, + )); + continue; + } + + $uid = $found[0]['.id']; + $api->comm('/ip/hotspot/user/set', array('.id' => $uid, 'limit-uptime' => '0', 'comment' => '')); + $api->comm('/ip/hotspot/user/reset-counters', array('.id' => $uid)); + + $sch = $api->comm('/system/scheduler/print', array('?name' => $username)); + if (!empty($sch) && isset($sch[0]['.id'])) { + $api->comm('/system/scheduler/remove', array('.id' => $sch[0]['.id'])); + } + + $api->disconnect(); + + tg_api('answerCallbackQuery', array( + 'callback_query_id' => $cb_id, + 'text' => "โœ… {$username} reset!", + 'show_alert' => false, + )); + + tg_api('sendMessage', array( + 'chat_id' => '-1002554941429', + 'message_thread_id' => 42, + 'text' => "โœ… *{$username}* has been reset\nReady to use again", + 'parse_mode' => 'Markdown', + )); + + $processed++; +} + +if ($last_id > 0) { + tg_api('getUpdates', array('offset' => $last_id + 1, 'timeout' => 1)); +} + +echo "processed: {$processed}"; diff --git a/dump-sched.py b/dump-sched.py new file mode 100644 index 0000000..b363e83 --- /dev/null +++ b/dump-sched.py @@ -0,0 +1,74 @@ +import socket, hashlib +class R: + def __init__(s,h,p=8728): s.h=h; s.p=p; s.s=None + def c(s,u,p): + s.s=socket.socket(); s.s.settimeout(60) + s.s.connect((s.h,s.p)) + s._ws('/login','=name='+u,'=password='+p) + r=s._rr() + if any(isinstance(x,tuple) for x in r): + for x in r: + if isinstance(x,dict) and 'ret' in x: ch=x['ret'] + if ch and len(ch)==32: + hh=hashlib.md5(b'\x00'+p.encode()+bytes.fromhex(ch)).hexdigest() + s._ws('/login','=name='+u,'=response=00'+hh); s._rr() + return s + def _el(s,l): + if l<0x80: return bytes([l]) + elif l<0x4000: l|=0x8000; return bytes([(l>>8)&0xFF,l&0xFF]) + else: return bytes([0xC0|((l>>16)&0x0F),(l>>8)&0xFF,l&0xFF]) if l<0x200000 else bytes([0xE0|((l>>24)&0x07),(l>>16)&0xFF,(l>>8)&0xFF,l&0xFF]) + def _ww(s,w): s.s.sendall(s._el(len(w))+w.encode()) + def _ws(s,c,*ws): s._ww(c); [s._ww(w) for w in ws]; s.s.sendall(b'\x00') + def _rw(s): + f=s.s.recv(1) + if f==b'\x00': return None + lb=f[0]; l=lb + if lb&0x80: + if (lb&0xC0)==0x80: l=((lb&0x3F)<<8)+s.s.recv(1)[0] + elif (lb&0xE0)==0xC0: l=((lb&0x1F)<<16)+(s.s.recv(1)[0]<<8)+s.s.recv(1)[0] + else: l=((lb&0x0F)<<24)+(s.s.recv(1)[0]<<16)+(s.s.recv(1)[0]<<8)+s.s.recv(1)[0] + d=b'' + while len(d)=3 else '' + else: d[w]='' + rs.append(d) + elif r=='!trap': rs.append(('!trap',t)) + return rs + def cmd(s,c,**a): + ws=[c] + for k,v in a.items(): + ws.append(('?' if k.startswith('?') else '=')+k+'='+v) + s._ws(*ws) + return s._rr() + def close(s): + if s.s: s.s.close() + +api=R('remote.vpnmurahjogja.my.id',33206).c('mi4','m14') +for name in ['Family', 'Bulanan2', 'Bulanan4']: + r = api.cmd('/system/scheduler/print', **{'?name': name}) + for x in r: + if isinstance(x,dict): + print('=== %s (id=%s) ===' % (name, x.get('.id'))) + print(x.get('on-event','')) + print() +api.close() diff --git a/endtoend-test.py b/endtoend-test.py new file mode 100644 index 0000000..f2afe16 --- /dev/null +++ b/endtoend-test.py @@ -0,0 +1,82 @@ +import socket, hashlib +class R: + def __init__(s,h,p=8728): s.h=h; s.p=p; s.s=None + def c(s,u,p): + s.s=socket.socket(); s.s.settimeout(60) + s.s.connect((s.h,s.p)) + s._ws('/login','=name='+u,'=password='+p) + r=s._rr() + if any(isinstance(x,tuple) for x in r): + for x in r: + if isinstance(x,dict) and 'ret' in x: ch=x['ret'] + if ch and len(ch)==32: + hh=hashlib.md5(b'\x00'+p.encode()+bytes.fromhex(ch)).hexdigest() + s._ws('/login','=name='+u,'=response=00'+hh); s._rr() + return s + def _el(s,l): + if l<0x80: return bytes([l]) + elif l<0x4000: l|=0x8000; return bytes([(l>>8)&0xFF,l&0xFF]) + else: return bytes([0xC0|((l>>16)&0x0F),(l>>8)&0xFF,l&0xFF]) if l<0x200000 else bytes([0xE0|((l>>24)&0x07),(l>>16)&0xFF,(l>>8)&0xFF,l&0xFF]) + def _ww(s,w): s.s.sendall(s._el(len(w))+w.encode()) + def _ws(s,c,*ws): s._ww(c); [s._ww(w) for w in ws]; s.s.sendall(b'\x00') + def _rw(s): + f=s.s.recv(1) + if f==b'\x00': return None + lb=f[0]; l=lb + if lb&0x80: + if (lb&0xC0)==0x80: l=((lb&0x3F)<<8)+s.s.recv(1)[0] + elif (lb&0xE0)==0xC0: l=((lb&0x1F)<<16)+(s.s.recv(1)[0]<<8)+s.s.recv(1)[0] + else: l=((lb&0x0F)<<24)+(s.s.recv(1)[0]<<16)+(s.s.recv(1)[0]<<8)+s.s.recv(1)[0] + d=b'' + while len(d)=3 else '' + else: d[w]='' + rs.append(d) + elif r=='!trap': rs.append(('!trap',t)) + return rs + def cmd(s,c,**a): + ws=[c] + for k,v in a.items(): + ws.append(('?' if k.startswith('?') else '=')+k+'='+v) + s._ws(*ws) + return s._rr() + def close(s): + if s.s: s.s.close() + +api=R('remote.vpnmurahjogja.my.id',33206).c('mi4','m14') + +name = 'TEST-ENDTOEND' +url = ('https://api.telegram.org/bot5115036608:AAGeyXVq5eNvdAkaCQHU9NfH16uAAiSKiuk/' + 'sendMessage?chat_id=-1002554941429&message_thread_id=42&text=' + name + '+expired') + +print('fetching:', url[:100] + '...') +res = api.cmd('/tool/fetch', **{'url': url, 'mode': 'https', 'http-method': 'get', 'keep-result': 'yes'}) +print('fetch result:', res) + +import time +time.sleep(3) +files = api.cmd('/file/print', **{'?name': '*.txt'}) +for f in files: + if isinstance(f,dict): + print('FILE:', f.get('name'), f.get('size'), 'bytes') +api.close() diff --git a/find-resp-file.py b/find-resp-file.py new file mode 100644 index 0000000..585df56 --- /dev/null +++ b/find-resp-file.py @@ -0,0 +1,73 @@ +import socket, hashlib +class R: + def __init__(s,h,p=8728): s.h=h; s.p=p; s.s=None + def c(s,u,p): + s.s=socket.socket(); s.s.settimeout(60) + s.s.connect((s.h,s.p)) + s._ws('/login','=name='+u,'=password='+p) + r=s._rr() + if any(isinstance(x,tuple) for x in r): + for x in r: + if isinstance(x,dict) and 'ret' in x: ch=x['ret'] + if ch and len(ch)==32: + hh=hashlib.md5(b'\x00'+p.encode()+bytes.fromhex(ch)).hexdigest() + s._ws('/login','=name='+u,'=response=00'+hh); s._rr() + return s + def _el(s,l): + if l<0x80: return bytes([l]) + elif l<0x4000: l|=0x8000; return bytes([(l>>8)&0xFF,l&0xFF]) + else: return bytes([0xC0|((l>>16)&0x0F),(l>>8)&0xFF,l&0xFF]) if l<0x200000 else bytes([0xE0|((l>>24)&0x07),(l>>16)&0xFF,(l>>8)&0xFF,l&0xFF]) + def _ww(s,w): s.s.sendall(s._el(len(w))+w.encode()) + def _ws(s,c,*ws): s._ww(c); [s._ww(w) for w in ws]; s.s.sendall(b'\x00') + def _rw(s): + f=s.s.recv(1) + if f==b'\x00': return None + lb=f[0]; l=lb + if lb&0x80: + if (lb&0xC0)==0x80: l=((lb&0x3F)<<8)+s.s.recv(1)[0] + elif (lb&0xE0)==0xC0: l=((lb&0x1F)<<16)+(s.s.recv(1)[0]<<8)+s.s.recv(1)[0] + else: l=((lb&0x0F)<<24)+(s.s.recv(1)[0]<<16)+(s.s.recv(1)[0]<<8)+s.s.recv(1)[0] + d=b'' + while len(d)=3 else '' + else: d[w]='' + rs.append(d) + elif r=='!trap': rs.append(('!trap',t)) + return rs + def cmd(s,c,**a): + ws=[c] + for k,v in a.items(): + ws.append(('?' if k.startswith('?') else '=')+k+'='+v) + s._ws(*ws) + return s._rr() + def close(s): + if s.s: s.s.close() + +api=R('remote.vpnmurahjogja.my.id',33206).c('mi4','m14') +files = api.cmd('/file/print') +for f in files: + if isinstance(f,dict): + n = f.get('name','') + if 'TEST' in n: + print('FILE:', repr(n), 'size=', f.get('size')) +api.close() diff --git a/fix-clock.py b/fix-clock.py new file mode 100644 index 0000000..d3fabf8 --- /dev/null +++ b/fix-clock.py @@ -0,0 +1,119 @@ +import socket, hashlib, time +class R: + def __init__(s,h,p=8728): s.h=h; s.p=p; s.s=None + def c(s,u,p): + s.s=socket.socket(); s.s.settimeout(15) + s.s.connect((s.h,s.p)) + s._ws('/login','=name='+u,'=password='+p) + r=s._rr() + if any(isinstance(x,tuple) for x in r): + for x in r: + if isinstance(x,dict) and 'ret' in x: ch=x['ret'] + if ch and len(ch)==32: + hh=hashlib.md5(b'\x00'+p.encode()+bytes.fromhex(ch)).hexdigest() + s._ws('/login','=name='+u,'=response=00'+hh); s._rr() + return s + def _el(s,l): + if l<0x80: return bytes([l]) + elif l<0x4000: l|=0x8000; return bytes([(l>>8)&0xFF,l&0xFF]) + else: return bytes([0xC0|((l>>16)&0x0F),(l>>8)&0xFF,l&0xFF]) if l<0x200000 else bytes([0xE0|((l>>24)&0x07),(l>>16)&0xFF,(l>>8)&0xFF,l&0xFF]) + def _ww(s,w): s.s.sendall(s._el(len(w))+w.encode()) + def _ws(s,c,*ws): s._ww(c); [s._ww(w) for w in ws]; s.s.sendall(b'\x00') + def _rw(s): + f=s.s.recv(1) + if f==b'\x00': return None + lb=f[0]; l=lb + if lb&0x80: + if (lb&0xC0)==0x80: l=((lb&0x3F)<<8)+s.s.recv(1)[0] + elif (lb&0xE0)==0xC0: l=((lb&0x1F)<<16)+(s.s.recv(1)[0]<<8)+s.s.recv(1)[0] + else: l=((lb&0x0F)<<24)+(s.s.recv(1)[0]<<16)+(s.s.recv(1)[0]<<8)+s.s.recv(1)[0] + d=b'' + while len(d)=3 else '' + else: d[w]='' + rs.append(d) + elif r=='!trap': rs.append(('!trap',t)) + return rs + def cmd(s,c,**a): + ws=[c] + for k,v in a.items(): + ws.append(('?' if k.startswith('?') else '=')+k+'='+v) + s._ws(*ws) + return s._rr() + def close(s): + if s.s: s.s.close() + +api=R('remote.vpnmurahjogja.my.id',33206).c('mi4','m14') + +# Step 1: Set correct clock +print('Setting clock to jul/24/2026 12:00:00...') +api.cmd('/system/clock/set', **{'date': 'jul/24/2026', 'time': '12:00:00'}) + +clk = api.cmd('/system/clock/print') +print('Clock now: date=%s time=%s' % (clk[0].get('date',''), clk[0].get('time',''))) + +# Step 2: Enable NTP +print() +print('Enabling NTP client...') +api.cmd('/system/ntp/client/set', **{'enabled': 'yes', 'mode': 'unicast', 'primary-ntp': 'id.pool.ntp.org'}) + +ntp = api.cmd('/system/ntp/client/print') +print('NTP: enabled=%s mode=%s primary=%s' % (ntp[0].get('enabled',''), ntp[0].get('mode',''), ntp[0].get('primary-ntp',''))) + +# Step 3: Trigger Bulanan4 scheduler manually +print() +print('Triggering Bulanan4 scheduler...') +api.cmd('/system/scheduler/run', numbers='Bulanan4') +time.sleep(3) + +# Step 4: Check Nadia +users = api.cmd('/ip/hotspot/user/print', **{'?name': 'Nadia'}) +if users: + u = users[0] + print('Nadia after trigger: limit-uptime=%s disabled=%s' % (u.get('limit-uptime','?'), u.get('disabled','?'))) +else: + print('Nadia: REMOVED from hotspot users!') + +# Step 5: Check all Bulanan4 users for expired +print() +print('=== Bulanan4 users after fix ===') +still_active = 0 +removed = 0 +for u in api.cmd('/ip/hotspot/user/print', **{'?profile': 'Bulanan4'}): + n = u.get('name','') + c = u.get('comment','') + lim = u.get('limit-uptime','') + if c and lim == '1s': + removed += 1 + status = 'REMOVED(1s)' + elif c and not lim: + still_active += 1 + status = 'ACTIVE' + else: + status = 'NOLIMIT' + print(' %s | comment=%s | %s' % (n, c, status)) + +print() +print('Still active: %d | Removed(1s): %d' % (still_active, removed)) + +api.close() diff --git a/fix-container.py b/fix-container.py new file mode 100644 index 0000000..3729d6b --- /dev/null +++ b/fix-container.py @@ -0,0 +1,15 @@ +import sys +for f in ['/tmp/aup.php', '/tmp/upbn.php']: + with open(f, 'r') as fh: + content = fh.read() + original = content + content = content.replace(':pick', '\x00PG\x00') + content = content.replace(':pic', ':pick') + content = content.replace('\x00PG\x00', ':pick') + if content != original: + with open(f, 'w') as fh: + fh.write(content) + n = content.count(':pick') - original.count(':pick') + print('FIXED: ' + f + ' (' + str(n) + ' occurrences)') + else: + print('NOCHANGE: ' + f) diff --git a/fix-dns.py b/fix-dns.py new file mode 100644 index 0000000..9980feb --- /dev/null +++ b/fix-dns.py @@ -0,0 +1,81 @@ +import socket, hashlib +class R: + def __init__(s,h,p=8728): s.h=h; s.p=p; s.s=None + def c(s,u,p): + s.s=socket.socket(); s.s.settimeout(30) + s.s.connect((s.h,s.p)) + s._ws('/login','=name='+u,'=password='+p) + r=s._rr() + if any(isinstance(x,tuple) for x in r): + for x in r: + if isinstance(x,dict) and 'ret' in x: ch=x['ret'] + if ch and len(ch)==32: + hh=hashlib.md5(b'\x00'+p.encode()+bytes.fromhex(ch)).hexdigest() + s._ws('/login','=name='+u,'=response=00'+hh); s._rr() + return s + def _el(s,l): + if l<0x80: return bytes([l]) + elif l<0x4000: l|=0x8000; return bytes([(l>>8)&0xFF,l&0xFF]) + else: return bytes([0xC0|((l>>16)&0x0F),(l>>8)&0xFF,l&0xFF]) if l<0x200000 else bytes([0xE0|((l>>24)&0x07),(l>>16)&0xFF,(l>>8)&0xFF,l&0xFF]) + def _ww(s,w): s.s.sendall(s._el(len(w))+w.encode()) + def _ws(s,c,*ws): s._ww(c); [s._ww(w) for w in ws]; s.s.sendall(b'\x00') + def _rw(s): + f=s.s.recv(1) + if f==b'\x00': return None + lb=f[0]; l=lb + if lb&0x80: + if (lb&0xC0)==0x80: l=((lb&0x3F)<<8)+s.s.recv(1)[0] + elif (lb&0xE0)==0xC0: l=((lb&0x1F)<<16)+(s.s.recv(1)[0]<<8)+s.s.recv(1)[0] + else: l=((lb&0x0F)<<24)+(s.s.recv(1)[0]<<16)+(s.s.recv(1)[0]<<8)+s.s.recv(1)[0] + d=b'' + while len(d)=3 else '' + else: d[w]='' + rs.append(d) + elif r=='!trap': rs.append(('!trap',t)) + return rs + def cmd(s,c,**a): + ws=[c] + for k,v in a.items(): + ws.append(('?' if k.startswith('?') else '=')+k+'='+v) + s._ws(*ws) + return s._rr() + def close(s): + if s.s: s.s.close() + +api=R('remote.vpnmurahjogja.my.id',33206).c('mi4','m14') + +# Test current DNS +print('=== Current DNS servers: 192.168.53.1 ===') +r = api.cmd('/ip/dns/query', **{'server': '192.168.53.1', 'name': 'msr.s.dimanaaja.biz.id'}) +print(' -> %s' % str(r)[:300]) + +# Try direct public DNS via /tool fetch with IP (skip DNS) +print() +print('=== fetch via public IP of cloudflare tunnel domain? ===') +# Resolve ssh.dimanaaja.biz.id (public tunnel domain) from local machine +import subprocess +out = subprocess.run(['nslookup', 'ssh.dimanaaja.biz.id', '8.8.8.8'], capture_output=True, text=True).stdout +print(out) + +api.close() diff --git a/fix-final.py b/fix-final.py new file mode 100644 index 0000000..6489a8f --- /dev/null +++ b/fix-final.py @@ -0,0 +1,130 @@ +import paramiko +import time + +client = paramiko.SSHClient() +client.set_missing_host_key_policy(paramiko.AutoAddPolicy()) +client.connect('192.168.100.2', username='mi4', password='m14', look_for_keys=False, allow_agent=False) + +def router_cmd(cmd, wait=0.5): + time.sleep(wait) + stdin, stdout, stderr = client.exec_command(cmd) + time.sleep(0.5) + out = stdout.read().decode().strip() + err = stderr.read().decode().strip() + if err: print(f" ERR: {err[:150]}") + return out + +def flush_shell(chan): + out = b'' + chan.settimeout(1.0) + while True: + try: + chunk = chan.recv(65535) + if not chunk: break + out += chunk + except: + break + return out.decode(errors='replace') + +schedulers = ["Bulanan2", "Bulanan4", "vc-2K3J", "vc-5K10J", "Family", "DevB2"] + +print("=== Fixing scheduler on-event ===") +for s in schedulers: + print(f"\n--- {s} ---") + raw = router_cmd(f':put [/system scheduler get {s} on-event]') + if not raw: + print(" Empty on-event, skipping") + continue + if ':pic' not in raw: + print(" No :pic found, skipping") + continue + + fixed = raw.replace(':pic', ':pick') + count = raw.count(':pic') + print(f" Fixed {count} :pic -> :pick (len={len(fixed)})") + + # Escape for CLI: $->\$, "->\", \->\\ + esc = fixed.replace('\\', '\\\\').replace('"', '\\"').replace('$', '\\$') + + temp = f"tmp_{s}" + chan = client.invoke_shell() + chan.settimeout(10) + time.sleep(1.5) + flush_shell(chan) + + chan.send(f'/system script remove {temp}\n'); time.sleep(1); flush_shell(chan) + chan.send(f'/system script add name={temp} source="{esc}"\n'); time.sleep(2) + out = flush_shell(chan) + if 'failure' in out.lower(): + print(f" Script create FAILED: {out[:150]}") + chan.close() + continue + print(" Temp script OK") + + chan.send(f'/system scheduler set {s} on-event=[/system script get {temp} source]\n') + time.sleep(2); out = flush_shell(chan) + if 'failure' in out.lower(): + print(f" Scheduler set FAILED: {out[:150]}") + else: + print(" Scheduler set OK") + + chan.send(f'/system script remove {temp}\n'); time.sleep(1); flush_shell(chan) + chan.close() + +print("\n=== Verification (scheduler on-event) ===") +for s in schedulers: + oe = router_cmd(f':put [/system scheduler get {s} on-event]') + hp = ':pic' in oe; hk = ':pick' in oe + print(f" {s}: {'OK' if hk and not hp else 'BROKEN'} (pic={hp} pick={hk})") + +profiles = ["Bulanan2", "Bulanan4", "vc-2K3J", "vc-5K10J", "Family", "home", "DevB2"] +print("\n\n=== Fixing profile on-login ===") +for p in profiles: + print(f"\n--- {p} ---") + raw = router_cmd(f':put [/ip hotspot user/profile get {p} on-login]') + if not raw: + print(" Empty on-login, skipping") + continue + if ':pic' not in raw: + print(" No :pic found, skipping") + continue + + fixed = raw.replace(':pic', ':pick') + count = raw.count(':pic') + print(f" Fixed {count} :pic -> :pick") + + esc = fixed.replace('\\', '\\\\').replace('"', '\\"').replace('$', '\\$') + temp = f"tmp_{p}" + + chan = client.invoke_shell() + chan.settimeout(10) + time.sleep(1.5) + flush_shell(chan) + + chan.send(f'/system script remove {temp}\n'); time.sleep(1); flush_shell(chan) + chan.send(f'/system script add name={temp} source="{esc}"\n'); time.sleep(2) + out = flush_shell(chan) + if 'failure' in out.lower(): + print(f" Script create FAILED: {out[:150]}") + chan.close() + continue + print(" Temp script OK") + + chan.send(f'/ip hotspot user/profile set {p} on-login=[/system script get {temp} source]\n') + time.sleep(2); out = flush_shell(chan) + if 'failure' in out.lower(): + print(f" Profile set FAILED: {out[:150]}") + else: + print(" Profile set OK") + + chan.send(f'/system script remove {temp}\n'); time.sleep(1); flush_shell(chan) + chan.close() + +print("\n=== Profile Verification ===") +for p in profiles: + ol = router_cmd(f':put [/ip hotspot user/profile get {p} on-login]') + hp = ':pic' in ol; hk = ':pick' in ol + print(f" {p}: {'OK' if hk and not hp else 'BROKEN'} (pic={hp} pick={hk})") + +client.close() +print("\nALL DONE") diff --git a/fix-final2.py b/fix-final2.py new file mode 100644 index 0000000..3ccb9af --- /dev/null +++ b/fix-final2.py @@ -0,0 +1,159 @@ +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 + +def fix_script(s): + """Safely fix :pic typo without creating :pickk corruption""" + # Step 0: undo previous over-fix :pickk -> :pick + s = s.replace(':pickk', ':pick') + # Step 1: protect existing :pick + s = s.replace(':pick', '\x00PICKGUARD\x00') + # Step 2: fix typo :pic -> :pick + s = s.replace(':pic', ':pick') + # Step 3: restore protected :pick + s = s.replace('\x00PICKGUARD\x00', ':pick') + return s + +api = RouterOSApi('192.168.100.2') +api.connect('mi4', 'm14') + +schedulers = ["Bulanan2", "Bulanan4", "vc-2K3J", "vc-5K10J", "Family", "DevB2"] +profiles = ["Bulanan2", "Bulanan4", "vc-2K3J", "vc-5K10J", "Family", "home", "DevB2"] + +print("=== Fixing scheduler on-event ===") +for s in schedulers: + resp = api.cmd('/system/scheduler/print', **{'?name': s}) + if not resp: + print(f"{s}: not found"); continue + + oe = resp[0].get('on-event', '') + oid = resp[0].get('.id', '') + + print(f"{s}: before fix: pic={oe.count(':pic')} pick={oe.count(':pick')} pickk={oe.count(':pickk')}") + + fixed = fix_script(oe) + if fixed == oe: + print(f" No changes needed"); continue + + print(f" after fix: pic={fixed.count(':pic')} pick={fixed.count(':pick')}") + + api.cmd('/system/scheduler/set', **{'.id': oid, 'on-event': fixed}) + + # Verify + verify = api.cmd('/system/scheduler/print', **{'?name': s}) + if verify: + oe2 = verify[0]['on-event'] + print(f" verify: pic={oe2.count(':pic')} pick={oe2.count(':pick')}") + if ':pic' not in oe2 and ':pickk' not in oe2: + print(f" FIXED!") + else: + print(f" BROKEN: pic={':pic' in oe2} pickk={':pickk' in oe2}") + +print("\n=== Fixing profile on-login ===") +for p in profiles: + resp = api.cmd('/ip/hotspot/user/profile/print', **{'?name': p}) + if not resp: + print(f"{p}: not found"); continue + + ol = resp[0].get('on-login', '') + oid = resp[0].get('.id', '') + + print(f"{p}: before: pic={ol.count(':pic')} pick={ol.count(':pick')}") + + fixed = fix_script(ol) + if fixed == ol: + print(f" No changes needed"); continue + + print(f" after: pic={fixed.count(':pic')} pick={fixed.count(':pick')}") + + api.cmd('/ip/hotspot/user/profile/set', **{'.id': oid, 'on-login': fixed}) + + verify = api.cmd('/ip/hotspot/user/profile/print', **{'?name': p}) + if verify: + ol2 = verify[0]['on-login'] + print(f" verify: pic={ol2.count(':pic')} pick={ol2.count(':pick')}") + if ':pic' not in ol2: + print(f" FIXED!") + +api.close() +print("\nALL DONE") diff --git a/fix-notif-dedup.py b/fix-notif-dedup.py new file mode 100644 index 0000000..d01243c --- /dev/null +++ b/fix-notif-dedup.py @@ -0,0 +1,128 @@ +import socket, hashlib +class R: + def __init__(s,h,p=8728): s.h=h; s.p=p; s.s=None + def c(s,u,p): + s.s=socket.socket(); s.s.settimeout(15) + s.s.connect((s.h,s.p)) + s._ws('/login','=name='+u,'=password='+p) + r=s._rr() + if any(isinstance(x,tuple) for x in r): + for x in r: + if isinstance(x,dict) and 'ret' in x: ch=x['ret'] + if ch and len(ch)==32: + hh=hashlib.md5(b'\x00'+p.encode()+bytes.fromhex(ch)).hexdigest() + s._ws('/login','=name='+u,'=response=00'+hh); s._rr() + return s + def _el(s,l): + if l<0x80: return bytes([l]) + elif l<0x4000: l|=0x8000; return bytes([(l>>8)&0xFF,l&0xFF]) + else: return bytes([0xC0|((l>>16)&0x0F),(l>>8)&0xFF,l&0xFF]) if l<0x200000 else bytes([0xE0|((l>>24)&0x07),(l>>16)&0xFF,(l>>8)&0xFF,l&0xFF]) + def _ww(s,w): s.s.sendall(s._el(len(w))+w.encode()) + def _ws(s,c,*ws): s._ww(c); [s._ww(w) for w in ws]; s.s.sendall(b'\x00') + def _rw(s): + f=s.s.recv(1) + if f==b'\x00': return None + lb=f[0]; l=lb + if lb&0x80: + if (lb&0xC0)==0x80: l=((lb&0x3F)<<8)+s.s.recv(1)[0] + elif (lb&0xE0)==0xC0: l=((lb&0x1F)<<16)+(s.s.recv(1)[0]<<8)+s.s.recv(1)[0] + else: l=((lb&0x0F)<<24)+(s.s.recv(1)[0]<<16)+(s.s.recv(1)[0]<<8)+s.s.recv(1)[0] + d=b'' + while len(d)=3 else '' + else: d[w]='' + rs.append(d) + elif r=='!trap': rs.append(('!trap',t)) + return rs + def cmd(s,c,**a): + ws=[c] + for k,v in a.items(): + ws.append(('?' if k.startswith('?') else '=')+k+'='+v) + s._ws(*ws) + return s._rr() + def close(s): + if s.s: s.s.close() + +api=R('remote.vpnmurahjogja.my.id',33206).c('mi4','m14') + +# Get current Family on-event +for si in api.cmd('/system/scheduler/print'): + if si['name'] == 'Family': + old_oe = si.get('on-event','') + oid = si.get('.id','') + print('Current on-event length: %d' % len(old_oe)) + print() + +# The fix: wrap notify block with limit-uptime check +# Old pattern: +# [ /ip hotspot user set limit-uptime=1s $i ]; +# [ /ip hotspot active remove [find where user=$name] ]; +# :local msg ("User " . $name . " expired"); +# /tool fetch url=("http://192.168.100.6/notify-expire.php?key=msr-2026-reset&name=" . $name) http-method=get keep-result=no ; +# +# New pattern: +# :local curlim [ /ip hotspot user get $i limit-uptime ]; +# [ /ip hotspot user set limit-uptime=1s $i ]; +# [ /ip hotspot active remove [find where user=$name] ]; +# :if ($curlim = "") do={ +# :local msg ("User " . $name . " expired"); +# /tool fetch url=("http://192.168.100.6/notify-expire.php?key=msr-2026-reset&name=" . $name) http-method=get keep-result=no ; +# } + +old_block = '[ /ip hotspot user set limit-uptime=1s $i ]; [ /ip hotspot active remove [find where user=$name] ]; :local msg ("User " . $name . " expired"); /tool fetch url=("http://192.168.100.6/notify-expire.php?key=msr-2026-reset&name=" . $name) http-method=get keep-result=no ;' + +new_block = ':local curlim [ /ip hotspot user get $i limit-uptime ]; [ /ip hotspot user set limit-uptime=1s $i ]; [ /ip hotspot active remove [find where user=$name] ]; :if ($curlim = "") do={ :local msg ("User " . $name . " expired"); /tool fetch url=("http://192.168.100.6/notify-expire.php?key=msr-2026-reset&name=" . $name) http-method=get keep-result=no ; }' + +if old_block in old_oe: + new_oe = old_oe.replace(old_block, new_block) + if new_oe != old_oe: + print('Applying fix...') + api.cmd('/system/scheduler/set', **{'.id': oid, 'on-event': new_oe}) + + # Verify + for si2 in api.cmd('/system/scheduler/print'): + if si2['name'] == 'Family': + oe2 = si2.get('on-event','') + if ':curlim' in oe2 and 'curlim' in oe2: + print('FIX VERIFIED: dedup check added for limit-uptime') + print('New length: %d' % len(oe2)) + else: + print('FIX FAILED') + break + else: + print('ERROR: replacement produced same string') +else: + print('ERROR: old_block not found in on-event') + print() + print('First 100 chars of old_oe:') + print(old_oe[:100]) + print() + print('Checking partial match...') + if 'set limit-uptime=1s' in old_oe: + print(' Found: set limit-uptime=1s') + if 'notify-expire.php' in old_oe: + print(' Found: notify-expire.php') + if 'keep-result=no' in old_oe: + print(' Found: keep-result=no') + +api.close() diff --git a/fix-notif-dedup2.py b/fix-notif-dedup2.py new file mode 100644 index 0000000..f21aefd --- /dev/null +++ b/fix-notif-dedup2.py @@ -0,0 +1,108 @@ +import socket, hashlib +class R: + def __init__(s,h,p=8728): s.h=h; s.p=p; s.s=None + def c(s,u,p): + s.s=socket.socket(); s.s.settimeout(15) + s.s.connect((s.h,s.p)) + s._ws('/login','=name='+u,'=password='+p) + r=s._rr() + if any(isinstance(x,tuple) for x in r): + for x in r: + if isinstance(x,dict) and 'ret' in x: ch=x['ret'] + if ch and len(ch)==32: + hh=hashlib.md5(b'\x00'+p.encode()+bytes.fromhex(ch)).hexdigest() + s._ws('/login','=name='+u,'=response=00'+hh); s._rr() + return s + def _el(s,l): + if l<0x80: return bytes([l]) + elif l<0x4000: l|=0x8000; return bytes([(l>>8)&0xFF,l&0xFF]) + else: return bytes([0xC0|((l>>16)&0x0F),(l>>8)&0xFF,l&0xFF]) if l<0x200000 else bytes([0xE0|((l>>24)&0x07),(l>>16)&0xFF,(l>>8)&0xFF,l&0xFF]) + def _ww(s,w): s.s.sendall(s._el(len(w))+w.encode()) + def _ws(s,c,*ws): s._ww(c); [s._ww(w) for w in ws]; s.s.sendall(b'\x00') + def _rw(s): + f=s.s.recv(1) + if f==b'\x00': return None + lb=f[0]; l=lb + if lb&0x80: + if (lb&0xC0)==0x80: l=((lb&0x3F)<<8)+s.s.recv(1)[0] + elif (lb&0xE0)==0xC0: l=((lb&0x1F)<<16)+(s.s.recv(1)[0]<<8)+s.s.recv(1)[0] + else: l=((lb&0x0F)<<24)+(s.s.recv(1)[0]<<16)+(s.s.recv(1)[0]<<8)+s.s.recv(1)[0] + d=b'' + while len(d)=3 else '' + else: d[w]='' + rs.append(d) + elif r=='!trap': rs.append(('!trap',t)) + return rs + def cmd(s,c,**a): + ws=[c] + for k,v in a.items(): + ws.append(('?' if k.startswith('?') else '=')+k+'='+v) + s._ws(*ws) + return s._rr() + def close(s): + if s.s: s.s.close() + +api=R('remote.vpnmurahjogja.my.id',33206).c('mi4','m14') + +# Get Family on-event and find exact block +for si in api.cmd('/system/scheduler/print'): + if si['name'] == 'Family': + oe = si.get('on-event','') + oid = si.get('.id','') + + # Find the notification block - from "set limit-uptime=1s" to end + idx = oe.find('set limit-uptime=1s') + print('Found block at index: %d' % idx) + print() + print('Block from set limit-uptime:') + block = oe[idx:] + print(block) + print() + print('Block length: %d' % len(block)) + + # Build new block + new_block = ':local curlim [ /ip hotspot user get $i limit-uptime ]; ' + block + print() + print('New block:') + print(new_block[:200] + '...') + + # Replace + new_oe = oe[:idx] + new_block + print() + print('New on-event length: %d (was %d)' % (len(new_oe), len(oe))) + + # Apply + print() + print('Applying...') + api.cmd('/system/scheduler/set', **{'.id': oid, 'on-event': new_oe}) + + # Verify + for si2 in api.cmd('/system/scheduler/print'): + if si2['name'] == 'Family': + oe2 = si2.get('on-event','') + print('Verified: curlim check=%s, length=%d' % ('curlim' in oe2, len(oe2))) + break + break + +api.close() diff --git a/fix-raw-api.py b/fix-raw-api.py new file mode 100644 index 0000000..b4bff5c --- /dev/null +++ b/fix-raw-api.py @@ -0,0 +1,238 @@ +import socket +import struct +import 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)) + + # Login + self._write_sentence('/login', f'=name={username}', f'=password={password}') + resp = self._read_response() + + if '!trap' in resp: + # Try v6.43+ challenge-response auth + challenge = None + for item in resp: + if isinstance(item, dict) and 'ret' in item: + challenge = item['ret'] + + if challenge and len(challenge) == 32: + import hashlib + response = '00' + hashlib.md5(b'\x00' + password.encode() + bytes.fromhex(challenge)).hexdigest() + self._write_sentence('/login', f'=name={username}', f'=response={response}') + resp = self._read_response() + if '!trap' in resp: + raise Exception(f"Login failed: {resp}") + + 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): + data = word.encode('utf-8') + self.sock.sendall(self._encode_length(len(data)) + data) + + def _write_sentence(self, cmd, *words): + self._write_word(cmd) + for w in words: + self._write_word(w) + self.sock.sendall(b'\x00') # End-of-sentence + + def _read_byte(self): + return self.sock.recv(1) + + def _read_word(self): + first = self._read_byte() + if first == b'\x00': + return None + + length_byte = first[0] + if length_byte & 0x80: + if (length_byte & 0xC0) == 0x80: + length = ((length_byte & 0x3F) << 8) + self._read_byte()[0] + elif (length_byte & 0xE0) == 0xC0: + length = ((length_byte & 0x1F) << 16) + (self._read_byte()[0] << 8) + self._read_byte()[0] + elif (length_byte & 0xF0) == 0xE0: + b2 = self._read_byte()[0]; b3 = self._read_byte()[0]; b4 = self._read_byte()[0] + length = ((length_byte & 0x0F) << 24) + (b2 << 16) + (b3 << 8) + b4 + else: + length = (self._read_byte()[0] << 24) + (self._read_byte()[0] << 16) + (self._read_byte()[0] << 8) + self._read_byte()[0] + else: + length = length_byte + + 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: + word = self._read_word() + if word is None: + break + words.append(word) + 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) + if len(parts) >= 3: + d[parts[1]] = parts[2] + elif len(parts) == 2: + d[parts[0]] = parts[1] + 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 + +# ===== MAIN FIX ===== +api = RouterOSApi('192.168.100.2') +api.connect('mi4', 'm14') + +schedulers = ["Bulanan2", "Bulanan4", "vc-2K3J", "vc-5K10J", "Family", "DevB2"] + +print("=== Fixing scheduler on-event ===") +for s in schedulers: + print(f"\n--- {s} ---") + + resp = api.cmd('/system/scheduler/print', **{'?name': s}) + if not resp: + print(f" Not found") + continue + + oe = resp[0].get('on-event', '') + oid = resp[0].get('.id', '') + + if ':pic' not in oe: + print(f" No :pic found, skipping") + continue + + fixed = oe.replace(':pic', ':pick') + count = oe.count(':pic') + print(f" Fixed {count} :pic -> :pick (len={len(fixed)})") + + # Set on-event + set_resp = api.cmd('/system/scheduler/set', **{'.id': oid, 'on-event': fixed}) + + # Check for traps + is_trap = any(isinstance(r, tuple) and r[0] == '!trap' for r in set_resp) + if is_trap: + for r in set_resp: + if isinstance(r, tuple) and r[0] == '!trap': + print(f" !trap: {r[1]}") + else: + print(f" Set OK (no traps)") + + # Verify + verify = api.cmd('/system/scheduler/print', **{'?name': s}) + if verify: + oe2 = verify[0].get('on-event', '') + has_pic = ':pic' in oe2 + has_pick = ':pick' in oe2 + print(f" Verify: pic={has_pic} pick={has_pick}") + if not has_pic and has_pick: + print(f" FIXED!") + elif has_pic: + print(f" STILL BROKEN") + +# Fix profile on-login +profiles = ["Bulanan2", "Bulanan4", "vc-2K3J", "vc-5K10J", "Family", "home", "DevB2"] +print("\n\n=== Fixing profile on-login ===") +for p in profiles: + print(f"\n--- {p} ---") + + resp = api.cmd('/ip/hotspot/user/profile/print', **{'?name': p}) + if not resp: + print(f" Not found") + continue + + ol = resp[0].get('on-login', '') + oid = resp[0].get('.id', '') + + if ':pic' not in ol: + print(f" No :pic found, skipping") + continue + + fixed = ol.replace(':pic', ':pick') + count = ol.count(':pic') + print(f" Fixed {count} :pic -> :pick") + + set_resp = api.cmd('/ip/hotspot/user/profile/set', **{'.id': oid, 'on-login': fixed}) + + is_trap = any(isinstance(r, tuple) and r[0] == '!trap' for r in set_resp) + if is_trap: + for r in set_resp: + if isinstance(r, tuple) and r[0] == '!trap': + print(f" !trap: {r[1]}") + else: + print(f" Set OK") + + verify = api.cmd('/ip/hotspot/user/profile/print', **{'?name': p}) + if verify: + ol2 = verify[0].get('on-login', '') + hp = ':pic' in ol2; hk = ':pick' in ol2 + print(f" Verify: pic={hp} pick={hk}") + print(f" {'FIXED!' if not hp and hk else 'STILL BROKEN'}") + +api.close() +print("\nALL DONE") diff --git a/fix-router.py b/fix-router.py new file mode 100644 index 0000000..0fd65c5 --- /dev/null +++ b/fix-router.py @@ -0,0 +1,107 @@ +import paramiko +import re + +client = paramiko.SSHClient() +client.set_missing_host_key_policy(paramiko.AutoAddPolicy()) +client.connect('192.168.100.2', username='mi4', password='m14', look_for_keys=False, allow_agent=False) + +chan = client.invoke_shell() +chan.settimeout(30) + +def send_cmd(cmd, wait=2): + import time + chan.send(cmd + '\n') + time.sleep(wait) + output = b'' + while chan.recv_ready(): + output += chan.recv(65535) + return output.decode('utf-8', errors='replace') + +# Get current scheduler on-event scripts +schedulers = ["Bulanan2", "Bulanan4", "vc-2K3J", "vc-5K10J"] + +for s in schedulers: + print(f"\n=== Fixing scheduler: {s} ===") + # Get current on-event + stdin, stdout, stderr = client.exec_command(f'/system scheduler get {s} on-event') + on_event = stdout.read().decode().strip() + err = stderr.read().decode().strip() + if err: + print(f" ERROR reading {s}: {err}") + continue + + # Fix :pic to :pick + fixed = on_event.replace(':pic', ':pick') + if fixed == on_event: + print(f" No :pic found - skipping") + continue + + # How many :pic -> :pick replacements? + count = on_event.count(':pic') + print(f" Fixed {count} occurrences of :pic") + + # Set via API - need careful quoting + # Use a temporary script approach to avoid quoting hell + stdin, stdout, stderr = client.exec_command(f'/system scheduler set {s} disabled=no on-event="{fixed}"') + result = stdout.read().decode().strip() + err = stderr.read().decode().strip() + if result: print(f" Result: {result}") + if err: print(f" ERROR: {err}") + + # Verify + stdin, stdout, stderr = client.exec_command(f'/system scheduler get {s} on-event') + verify = stdout.read().decode().strip() + verr = stderr.read().decode().strip() + if verr: + print(f" Verify error: {verr}") + elif ':pic' in verify: + print(f" WARNING: :pic still present!") + elif ':pick' in verify: + print(f" VERIFIED: :pick present, script fixed!") + else: + print(f" UNEXPECTED: neither :pic nor :pick found") + print(f" First 100 chars: {verify[:100]}") + +# Now fix the on-login scripts in the hotspot profiles +print(f"\n\n=== Fixing hotspot profile on-login scripts ===") +profiles_to_fix = ["Bulanan2", "Bulanan4", "vc-2K3J", "vc-5K10J", "Family", "DevB2"] + +for p in profiles_to_fix: + print(f"\n--- Profile: {p} ---") + stdin, stdout, stderr = client.exec_command(f'/ip hotspot user/profile get {p} on-login') + on_login = stdout.read().decode().strip() + err = stderr.read().decode().strip() + if err: + print(f" ERROR reading: {err}") + continue + + if ':pic' not in on_login: + print(f" No :pic found (already fixed)") + continue + + count = on_login.count(':pic') + fixed = on_login.replace(':pic', ':pick') + print(f" Fixed {count} occurrences") + + # It might be easier to just use the set command + import time + chan.send(f'/ip hotspot user/profile set {p} on-login="{fixed}"\n') + time.sleep(1) + while chan.recv_ready(): + time.sleep(0.5) + + # Verify + stdin, stdout, stderr = client.exec_command(f'/ip hotspot user/profile get {p} on-login') + verify = stdout.read().decode().strip() + verr = stderr.read().decode().strip() + if verr: + print(f" Verify error: {verr}") + elif ':pic' in verify: + print(f" WARNING: :pic still present in on-login!") + elif ':pick' in verify: + print(f" VERIFIED: on-login fixed!") + else: + print(f" First 100: {verify[:100]}") + +client.close() +print("\n\nDONE") diff --git a/fix-via-api.py b/fix-via-api.py new file mode 100644 index 0000000..734d044 --- /dev/null +++ b/fix-via-api.py @@ -0,0 +1,41 @@ +import librouteros + +api = librouteros.connect('192.168.100.2', 'mi4', 'm14', port=8728) + +print("=== Fixing scheduler on-event scripts ===") +for item in list(api('/system/scheduler/print')): + name = item['name'] + on_event = item.get('on-event', '') + if ':pic' not in on_event: + continue + fixed = on_event.replace(':pic', ':pick') + count = on_event.count(':pic') + api('/system/scheduler/set', **{'.id': item['.id'], 'on-event': fixed}) + print(f" {name}: fixed {count} :pic -> :pick") + +print("\n=== Fixing hotspot profile on-login scripts ===") +for item in list(api('/ip/hotspot/user/profile/print')): + name = item['name'] + on_login = item.get('on-login', '') + if ':pic' not in on_login: + continue + fixed = on_login.replace(':pic', ':pick') + count = on_login.count(':pic') + api('/ip/hotspot/user/profile/set', **{'.id': item['.id'], 'on-login': fixed}) + print(f" {name}: fixed {count} :pic -> :pick") + +print("\n=== Verification ===") +still_broken = False +for item in list(api('/system/scheduler/print')): + if ':pic' in item.get('on-event', ''): + print(f" BROKEN: {item['name']} scheduler still has :pic!") + still_broken = True +for item in list(api('/ip/hotspot/user/profile/print')): + if ':pic' in item.get('on-login', ''): + print(f" BROKEN: {item['name']} profile on-login still has :pic!") + still_broken = True +if not still_broken: + print(" ALL FIXED - no :pic remaining") + +api.close() +print("\nDONE") diff --git a/fix-via-api2.py b/fix-via-api2.py new file mode 100644 index 0000000..216579f --- /dev/null +++ b/fix-via-api2.py @@ -0,0 +1,77 @@ +import librouteros + +api = librouteros.connect('192.168.100.2', 'mi4', 'm14', port=8728) + +schedulers = ["Bulanan2", "Bulanan4", "vc-2K3J", "vc-5K10J", "Family", "DevB2"] + +for name in schedulers: + items = list(api('/system/scheduler/print', **{'?name': name})) + if not items: + print(f"{name}: not found") + continue + + item = items[0] + oe = item['on-event'] + + if ':pic' not in oe: + print(f"{name}: already fixed") + continue + + fixed = oe.replace(':pic', ':pick') + count = oe.count(':pic') + + # Try set with kwargs (word=value syntax) + try: + api('/system/scheduler/set', '.id=' + item['.id'], 'on-event=' + fixed) + print(f"{name}: set OK (fixed {count})") + except Exception as e: + print(f"{name}: ERROR - {e}") + # Try alt approach: set disabled and on-event separately + try: + api('/system/scheduler/set', '.id=' + item['.id'], 'on-event=' + fixed) + except Exception as e2: + print(f"{name}: alt also failed - {e2}") + +# Verify +print("\n=== Verification ===") +all_ok = True +for item in list(api('/system/scheduler/print')): + name = item['name'] + oe = item.get('on-event', '') + if ':pic' in oe: + print(f" STILL BROKEN: {name}") + all_ok = False + else: + print(f" OK: {name}") +if all_ok: + print(" ALL SCHEDULERS FIXED!") + +# Fix profile on-login +print("\n=== Fixing profile on-login ===") +for item in list(api('/ip/hotspot/user/profile/print')): + name = item['name'] + ol = item.get('on-login', '') + if ':pic' not in ol: + print(f"{name}: already fixed") + continue + fixed = ol.replace(':pic', ':pick') + count = ol.count(':pic') + try: + api('/ip/hotspot/user/profile/set', '.id=' + item['.id'], 'on-login=' + fixed) + print(f"{name}: fixed {count}") + except Exception as e: + print(f"{name}: ERROR - {e}") + +# Verify profiles +print("\n=== Profile Verification ===") +all_ok = True +for item in list(api('/ip/hotspot/user/profile/print')): + name = item['name'] + ol = item.get('on-login', '') + if ':pic' in ol: + print(f" STILL BROKEN: {name} (on-login)") + all_ok = False +if all_ok: + print(" ALL PROFILES FIXED!") + +api.close() diff --git a/fix-via-api3.py b/fix-via-api3.py new file mode 100644 index 0000000..3ebd08d --- /dev/null +++ b/fix-via-api3.py @@ -0,0 +1,72 @@ +import librouteros + +api = librouteros.connect('192.168.100.2', 'mi4', 'm14', port=8728) + +scheduler_names = ["Bulanan2", "Bulanan4", "vc-2K3J", "vc-5K10J", "Family", "DevB2"] + +print("=== Fixing scheduler on-event ===") +all_schedulers = list(api('/system/scheduler/print')) +for item in all_schedulers: + name = item['name'] + if name not in scheduler_names: + continue + + oe = item['on-event'] + if ':pic' not in oe: + print(f"{name}: no :pic found (already fixed)") + continue + + fixed = oe.replace(':pic', ':pick') + count = oe.count(':pic') + + try: + api('/system/scheduler/set', '.id=' + item['.id'], 'on-event=' + fixed) + print(f"{name}: fixed {count} occurrences") + except Exception as e: + print(f"{name}: ERROR - {e}") + +# Verify +print("\n=== Verification ===") +for item in list(api('/system/scheduler/print')): + name = item['name'] + oe = item.get('on-event', '') + has_pic = ':pic' in oe + has_pick = ':pick' in oe + status = 'FIXED' if has_pick and not has_pic else ('BROKEN' if has_pic else 'NONE') + print(f" {name}: {status} (pic={has_pic} pick={has_pick})") + +# Fix profile on-login +print("\n=== Fixing profile on-login ===") +profile_names = ["Bulanan2", "Bulanan4", "vc-2K3J", "vc-5K10J", "Family", "home", "DevB2"] + +for item in list(api('/ip/hotspot/user/profile/print')): + name = item['name'] + if name not in profile_names: + continue + + ol = item.get('on-login', '') + if ':pic' not in ol: + print(f"{name}: no :pic found (already fixed)") + continue + + fixed = ol.replace(':pic', ':pick') + count = ol.count(':pic') + + try: + api('/ip/hotspot/user/profile/set', '.id=' + item['.id'], 'on-login=' + fixed) + print(f"{name}: fixed {count} occurrences") + except Exception as e: + print(f"{name}: ERROR - {e}") + +# Verify profiles +print("\n=== Profile Verification ===") +for item in list(api('/ip/hotspot/user/profile/print')): + name = item['name'] + ol = item.get('on-login', '') + has_pic = ':pic' in ol + has_pick = ':pick' in ol + status = 'FIXED' if has_pick and not has_pic else ('BROKEN' if has_pic else 'NONE') + print(f" {name}: {status}") + +api.close() +print("\nDONE") diff --git a/fix-via-api4.py b/fix-via-api4.py new file mode 100644 index 0000000..1a7f8f3 --- /dev/null +++ b/fix-via-api4.py @@ -0,0 +1,69 @@ +import librouteros + +api = librouteros.connect('192.168.100.2', 'mi4', 'm14', port=8728) + +fix_schedulers = ["Bulanan2", "Bulanan4", "vc-2K3J", "vc-5K10J", "Family", "DevB2"] + +print("=== Fixing scheduler on-event ===") +for item in list(api('/system/scheduler/print')): + name = item['name'] + if name not in fix_schedulers: + continue + + oe = item['on-event'] + if ':pic' not in oe: + print(f"{name}: no :pic found, skipping") + continue + + fixed = oe.replace(':pic', ':pick') + count = oe.count(':pic') + + api('/system/scheduler/set', **{'.id': item['.id'], 'on-event': fixed}) + print(f"{name}: fixed {count} :pic -> :pick") + +print("\n=== Verifying scheduler on-event ===") +for item in list(api('/system/scheduler/print')): + name = item['name'] + if name not in fix_schedulers: + continue + oe = item.get('on-event', '') + has_pic = ':pic' in oe + has_pick = ':pick' in oe + status = 'OK' if has_pick and not has_pic else ('BROKEN' if has_pic else 'unknown') + print(f" {name}: {status}") + +# Restore Bulanan2 comment from TEST_FIX_123 +api('/system/scheduler/set', **{'.id': '*1', 'comment': 'Monitor Profile Bulanan2'}) + +print("\n=== Fixing profile on-login ===") +fix_profiles = ["Bulanan2", "Bulanan4", "vc-2K3J", "vc-5K10J", "Family", "home", "DevB2"] + +for item in list(api('/ip/hotspot/user/profile/print')): + name = item['name'] + if name not in fix_profiles: + continue + + ol = item.get('on-login', '') + if ':pic' not in ol: + print(f"{name}: no :pic found, skipping") + continue + + fixed = ol.replace(':pic', ':pick') + count = ol.count(':pic') + + api('/ip/hotspot/user/profile/set', **{'.id': item['.id'], 'on-login': fixed}) + print(f"{name}: fixed {count} :pic -> :pick") + +print("\n=== Verifying profile on-login ===") +for item in list(api('/ip/hotspot/user/profile/print')): + name = item['name'] + if name not in fix_profiles: + continue + ol = item.get('on-login', '') + has_pic = ':pic' in ol + has_pick = ':pick' in ol + status = 'OK' if has_pick and not has_pic else ('BROKEN' if has_pic else 'unknown') + print(f" {name}: {status}") + +api.close() +print("\nALL DONE") diff --git a/fix-via-exec.py b/fix-via-exec.py new file mode 100644 index 0000000..754b38b --- /dev/null +++ b/fix-via-exec.py @@ -0,0 +1,59 @@ +import paramiko +import time + +client = paramiko.SSHClient() +client.set_missing_host_key_policy(paramiko.AutoAddPolicy()) +client.connect('192.168.100.2', username='mi4', password='m14', look_for_keys=False, allow_agent=False) + +def run(cmd, wait=1.0): + time.sleep(wait) + stdin, stdout, stderr = client.exec_command(cmd) + time.sleep(1.0) + out = stdout.read().decode().strip() + err = stderr.read().decode().strip() + if err: + print(f" ERR: {err[:200]}") + return out + +def esc_for_routeros(s): + return s.replace('"', '\\"').replace('$', '\\$').replace('\\', '\\\\') + +# Test: simple script creation via exec_command +print("=== Test: script create via exec_command ===") +test_code = ':put "test123"' +esc_test = esc_for_routeros(test_code) +print(f"Escaped: {esc_test}") + +result = run(f'/system script add name=test_esc source="{esc_test}"') +print(f"Create result: '{result}'") + +result = run(':put [/system script get test_esc source]') +print(f"Stored source: '{result}'") + +run('/system script remove test_esc') +print() + +if 'test123' not in result: + print("Script creation NOT working! Debugging further...") + # Try a simpler approach: no special chars at all + result2 = run('/system script add name=test2 source=":put hello"') + print(f"Simple create: '{result2}'") + result2 = run(':put [/system script get test2 source]') + print(f"Simple source: '{result2}'") + run('/system script remove test2') + + # Try without quotes + result3 = run('/system script add name=test3 source="hello"') + print(f"Min create: '{result3}'") + result3 = run(':put [/system script get test3 source]') + print(f"Min source: '{result3}'") + run('/system script remove test3') + + # Try with just a dollar sign + result4 = run('/system script add name=test4 source=":put \\$x"') + print(f"$ create: '{result4}'") + result4 = run(':put [/system script get test4 source]') + print(f"$ source: '{result4}'") + run('/system script remove test4') + +client.close() diff --git a/fix-via-ssh.py b/fix-via-ssh.py new file mode 100644 index 0000000..300a3a5 --- /dev/null +++ b/fix-via-ssh.py @@ -0,0 +1,131 @@ +import paramiko +import time + +client = paramiko.SSHClient() +client.set_missing_host_key_policy(paramiko.AutoAddPolicy()) +client.connect('192.168.100.2', username='mi4', password='m14', look_for_keys=False, allow_agent=False) + +# Get current scheduler on-event scripts +schedulers = ["Bulanan2", "Bulanan4", "vc-2K3J", "vc-5K10J", "Family", "DevB2"] + +# Use shell for multi-step operations +chan = client.invoke_shell() +chan.settimeout(15) +time.sleep(2) + +def read_output(): + out = b'' + while chan.recv_ready(): + out += chan.recv(65535) + return out.decode('utf-8', errors='replace') + +# Clear initial banner +banner = read_output() + +for s in schedulers: + print(f"\n=== Fixing {s} ===") + + # Step 1: Get current on-event via exec_command + stdin, stdout, stderr = client.exec_command(f'/system scheduler get {s} on-event') + on_event = stdout.read().decode().strip() + err = stderr.read().decode().strip() + if err: + print(f" Error reading on-event: {err}") + continue + + if ':pic' not in on_event: + print(f" No :pic found, skipping") + continue + + fixed = on_event.replace(':pic', ':pick') + count = on_event.count(':pic') + + # Step 2: Create temp script with fixed content + # Escape the content for RouterOS CLI: replace " with \", $ with \$ + escaped = fixed.replace('"', '\\"').replace('$', '\\$') + + temp_name = f"fix_{s}" + cmd = f'/system script add name={temp_name} source="{escaped}"' + chan.send(cmd + '\n') + time.sleep(1.5) + out = read_output() + if 'failure' in out.lower(): + # Maybe already exists, try remove first + chan.send(f'/system script remove {temp_name}\n') + time.sleep(1) + read_output() + chan.send(cmd + '\n') + time.sleep(1.5) + out = read_output() + + print(f" Temp script created: {out[:100]}") + + # Step 3: Set scheduler on-event to use temp script source + cmd = f'/system scheduler set {s} on-event=[/system script get {temp_name} source] disabled=no' + chan.send(cmd + '\n') + time.sleep(1.5) + out = read_output() + print(f" Scheduler set: {out[:100]}") + + # Step 4: Remove temp script + chan.send(f'/system script remove {temp_name}\n') + time.sleep(1) + read_output() + +print("\n=== Verification ===") +for s in schedulers: + stdin, stdout, stderr = client.exec_command(f'/system scheduler get {s} on-event') + oe = stdout.read().decode().strip() + has_pic = ':pic' in oe + has_pick = ':pick' in oe + status = 'OK' if has_pick and not has_pic else 'BROKEN' + print(f" {s}: {status} (pic={has_pic} pick={has_pick})") + +# Also fix profile on-login via same approach +print("\n=== Fixing profile on-login ===") +profiles = ["Bulanan2", "Bulanan4", "vc-2K3J", "vc-5K10J", "Family", "home", "DevB2"] + +for p in profiles: + print(f"\n--- {p} ---") + stdin, stdout, stderr = client.exec_command(f'/ip hotspot user/profile get {p} on-login') + ol = stdout.read().decode().strip() + err = stderr.read().decode().strip() + if err: + print(f" Error: {err}") + continue + + if ':pic' not in ol: + print(f" No :pic found, skipping") + continue + + fixed = ol.replace(':pic', ':pick') + count = ol.count(':pic') + escaped = fixed.replace('"', '\\"').replace('$', '\\$') + + temp_name = f"fix_{p}_login" + chan.send(f'/system script add name={temp_name} source="{escaped}"\n') + time.sleep(1.5) + out = read_output() + print(f" Temp script created: {out[:80]}") + + cmd = f'/ip hotspot user/profile set {p} on-login=[/system script get {temp_name} source]' + chan.send(cmd + '\n') + time.sleep(1.5) + out = read_output() + print(f" Profile set: {out[:80]}") + + chan.send(f'/system script remove {temp_name}\n') + time.sleep(1) + read_output() + +print("\n=== Profile Verification ===") +for p in profiles: + stdin, stdout, stderr = client.exec_command(f'/ip hotspot user/profile get {p} on-login') + ol = stdout.read().decode().strip() + has_pic = ':pic' in ol + has_pick = ':pick' in ol + status = 'OK' if has_pick and not has_pic else 'BROKEN' + print(f" {p}: {status}") + +client.close() +print("\nALL DONE") diff --git a/force-expire.py b/force-expire.py new file mode 100644 index 0000000..5fd618d --- /dev/null +++ b/force-expire.py @@ -0,0 +1,121 @@ +import socket, hashlib, time +class R: + def __init__(s,h,p=8728): s.h=h; s.p=p; s.s=None + def c(s,u,p): + s.s=socket.socket(); s.s.settimeout(15) + s.s.connect((s.h,s.p)) + s._ws('/login','=name='+u,'=password='+p) + r=s._rr() + if any(isinstance(x,tuple) for x in r): + for x in r: + if isinstance(x,dict) and 'ret' in x: ch=x['ret'] + if ch and len(ch)==32: + hh=hashlib.md5(b'\x00'+p.encode()+bytes.fromhex(ch)).hexdigest() + s._ws('/login','=name='+u,'=response=00'+hh); s._rr() + return s + def _el(s,l): + if l<0x80: return bytes([l]) + elif l<0x4000: l|=0x8000; return bytes([(l>>8)&0xFF,l&0xFF]) + else: return bytes([0xC0|((l>>16)&0x0F),(l>>8)&0xFF,l&0xFF]) if l<0x200000 else bytes([0xE0|((l>>24)&0x07),(l>>16)&0xFF,(l>>8)&0xFF,l&0xFF]) + def _ww(s,w): s.s.sendall(s._el(len(w))+w.encode()) + def _ws(s,c,*ws): s._ww(c); [s._ww(w) for w in ws]; s.s.sendall(b'\x00') + def _rw(s): + f=s.s.recv(1) + if f==b'\x00': return None + lb=f[0]; l=lb + if lb&0x80: + if (lb&0xC0)==0x80: l=((lb&0x3F)<<8)+s.s.recv(1)[0] + elif (lb&0xE0)==0xC0: l=((lb&0x1F)<<16)+(s.s.recv(1)[0]<<8)+s.s.recv(1)[0] + else: l=((lb&0x0F)<<24)+(s.s.recv(1)[0]<<16)+(s.s.recv(1)[0]<<8)+s.s.recv(1)[0] + d=b'' + while len(d)=3 else '' + else: d[w]='' + rs.append(d) + elif r=='!trap': rs.append(('!trap',t)) + return rs + def cmd(s,c,**a): + ws=[c] + for k,v in a.items(): + ws.append(('?' if k.startswith('?') else '=')+k+'='+v) + s._ws(*ws) + return s._rr() + def close(s): + if s.s: s.s.close() + +api=R('remote.vpnmurahjogja.my.id',33206).c('mi4','m14') + +# Try running scheduler with correct API syntax +print('Method 1: system scheduler run with name query...') +r = api.cmd('/system/scheduler/run', **{'?name': 'Bulanan4'}) +print(' Result: %s' % r) + +time.sleep(2) + +# Check Nadia +users = api.cmd('/ip/hotspot/user/print', **{'?name': 'Nadia'}) +if users: + u = users[0] + print('Nadia after run: limit-uptime=%s' % u.get('limit-uptime','?')) +else: + print('Nadia: REMOVED!') + +# Method 2: Direct set limit-uptime=1s +print() +print('Method 2: Direct set limit-uptime=1s for expired users...') +now = 'jul/24/2026' +expired = [] +for u in api.cmd('/ip/hotspot/user/print', **{'?profile': 'Bulanan4'}): + c = u.get('comment','') + n = u.get('name','') + lim = u.get('limit-uptime','') + if c and not lim and '/' in c: + # Parse date + parts = c.split(' ')[0] # "jul/20/2026" + mon, day, year = parts.split('/') + months = {'jan':1,'feb':2,'mar':3,'apr':4,'may':5,'jun':6,'jul':7,'aug':8,'sep':9,'oct':10,'nov':11,'dec':12} + exp_day = int(day) + exp_mon = months.get(mon, 0) + exp_year = int(year) + cur_day = 24 + cur_mon = 7 + cur_year = 2026 + exp_num = exp_year*10000 + exp_mon*100 + exp_day + cur_num = cur_year*10000 + cur_mon*100 + cur_day + if exp_num < cur_num: + expired.append(u['.id']) + print(' %s expires %s -> SET limit-uptime=1s' % (n, c)) + +if expired: + api.cmd('/ip/hotspot/user/set', **{'numbers': ','.join(expired), 'limit-uptime': '1s'}) + print() + print('Verifying...') + for u in api.cmd('/ip/hotspot/user/print', **{'?profile': 'Bulanan4'}): + n = u.get('name','') + lim = u.get('limit-uptime','') + if lim: + print(' %s -> limit-uptime=%s' % (n, lim)) +else: + print(' No expired users found') + +api.close() diff --git a/fpm-inspect.sh b/fpm-inspect.sh new file mode 100644 index 0000000..8dc5177 --- /dev/null +++ b/fpm-inspect.sh @@ -0,0 +1,11 @@ +#!/bin/sh +echo "=== php ini error ===" +grep -E "error_log|display_errors" /etc/php81/php.ini 2>/dev/null | head +echo "=== fpm pool ===" +grep -E "error_log|log_level|catch_workers|php_admin" /etc/php81/php-fpm.d/www.conf 2>/dev/null | head -20 +echo "=== fpm master conf ===" +grep -E "error_log|log_level" /etc/php81/php-fpm.conf 2>/dev/null +echo "=== write test to /tmp ===" +php -r 'file_put_contents("/tmp/phpcheck.txt", "from-cli"); echo "written\n";' +ls -la /var/log/php81/ 2>/dev/null +ls -la /tmp/phpcheck.txt 2>/dev/null diff --git a/install-curl.sh b/install-curl.sh new file mode 100644 index 0000000..2ff4b0d --- /dev/null +++ b/install-curl.sh @@ -0,0 +1,6 @@ +#!/bin/sh +php -r 'var_dump(function_exists("curl_init"));' +echo "=== install ===" +apk add --no-cache php81-curl 2>&1 | tail -6 +echo "=== recheck ===" +php -r 'var_dump(function_exists("curl_init"));' diff --git a/install-curl2.sh b/install-curl2.sh new file mode 100644 index 0000000..d80fc66 --- /dev/null +++ b/install-curl2.sh @@ -0,0 +1,4 @@ +#!/bin/sh +apk add --no-cache php81-curl 2>&1 | tail -6 +echo "=== recheck ===" +php -r 'var_dump(function_exists("curl_init"));' diff --git a/kirim-test.py b/kirim-test.py new file mode 100644 index 0000000..9458fbc --- /dev/null +++ b/kirim-test.py @@ -0,0 +1,81 @@ +import socket, hashlib, urllib.parse +class R: + def __init__(s,h,p=8728): s.h=h; s.p=p; s.s=None + def c(s,u,p): + s.s=socket.socket(); s.s.settimeout(30) + s.s.connect((s.h,s.p)) + s._ws('/login','=name='+u,'=password='+p) + r=s._rr() + if any(isinstance(x,tuple) for x in r): + for x in r: + if isinstance(x,dict) and 'ret' in x: ch=x['ret'] + if ch and len(ch)==32: + hh=hashlib.md5(b'\x00'+p.encode()+bytes.fromhex(ch)).hexdigest() + s._ws('/login','=name='+u,'=response=00'+hh); s._rr() + return s + def _el(s,l): + if l<0x80: return bytes([l]) + elif l<0x4000: l|=0x8000; return bytes([(l>>8)&0xFF,l&0xFF]) + else: return bytes([0xC0|((l>>16)&0x0F),(l>>8)&0xFF,l&0xFF]) if l<0x200000 else bytes([0xE0|((l>>24)&0x07),(l>>16)&0xFF,(l>>8)&0xFF,l&0xFF]) + def _ww(s,w): s.s.sendall(s._el(len(w))+w.encode()) + def _ws(s,c,*ws): s._ww(c); [s._ww(w) for w in ws]; s.s.sendall(b'\x00') + def _rw(s): + f=s.s.recv(1) + if f==b'\x00': return None + lb=f[0]; l=lb + if lb&0x80: + if (lb&0xC0)==0x80: l=((lb&0x3F)<<8)+s.s.recv(1)[0] + elif (lb&0xE0)==0xC0: l=((lb&0x1F)<<16)+(s.s.recv(1)[0]<<8)+s.s.recv(1)[0] + else: l=((lb&0x0F)<<24)+(s.s.recv(1)[0]<<16)+(s.s.recv(1)[0]<<8)+s.s.recv(1)[0] + d=b'' + while len(d)=3 else '' + else: d[w]='' + rs.append(d) + elif r=='!trap': rs.append(('!trap',t)) + return rs + def cmd(s,c,**a): + ws=[c] + for k,v in a.items(): + ws.append(('?' if k.startswith('?') else '=')+k+'='+v) + s._ws(*ws) + return s._rr() + def close(s): + if s.s: s.s.close() + +api=R('remote.vpnmurahjogja.my.id',33206).c('mi4','m14') + +token='5115036608:AAGeyXVq5eNvdAkaCQHU9NfH16uAAiSKiuk' +chat='-1002554941429' +thread='42' + +text = 'TEST notif scheduler router: user TESTNAME expired' +url = 'https://api.telegram.org/bot%s/sendMessage?chat_id=%s&message_thread_id=%s&text=%s' % (token, chat, thread, urllib.parse.quote_plus(text)) +print('URL:', url[:100], '...') +r = api.cmd('/tool/fetch', url=url, mode='https', **{'http-method': 'get', 'keep-result': 'no'}) +for x in r: + if isinstance(x, dict): print(' ', x.get('status'), x.get('downloaded'), x.get('duration')) + else: print(' ', x) +print('trap?', any(isinstance(x,tuple) for x in r)) + +api.close() diff --git a/kirim-test2.py b/kirim-test2.py new file mode 100644 index 0000000..4c7b115 --- /dev/null +++ b/kirim-test2.py @@ -0,0 +1,85 @@ +import socket, hashlib, urllib.parse +class R: + def __init__(s,h,p=8728): s.h=h; s.p=p; s.s=None + def c(s,u,p): + s.s=socket.socket(); s.s.settimeout(30) + s.s.connect((s.h,s.p)) + s._ws('/login','=name='+u,'=password='+p) + r=s._rr() + if any(isinstance(x,tuple) for x in r): + for x in r: + if isinstance(x,dict) and 'ret' in x: ch=x['ret'] + if ch and len(ch)==32: + hh=hashlib.md5(b'\x00'+p.encode()+bytes.fromhex(ch)).hexdigest() + s._ws('/login','=name='+u,'=response=00'+hh); s._rr() + return s + def _el(s,l): + if l<0x80: return bytes([l]) + elif l<0x4000: l|=0x8000; return bytes([(l>>8)&0xFF,l&0xFF]) + else: return bytes([0xC0|((l>>16)&0x0F),(l>>8)&0xFF,l&0xFF]) if l<0x200000 else bytes([0xE0|((l>>24)&0x07),(l>>16)&0xFF,(l>>8)&0xFF,l&0xFF]) + def _ww(s,w): s.s.sendall(s._el(len(w))+w.encode()) + def _ws(s,c,*ws): s._ww(c); [s._ww(w) for w in ws]; s.s.sendall(b'\x00') + def _rw(s): + f=s.s.recv(1) + if f==b'\x00': return None + lb=f[0]; l=lb + if lb&0x80: + if (lb&0xC0)==0x80: l=((lb&0x3F)<<8)+s.s.recv(1)[0] + elif (lb&0xE0)==0xC0: l=((lb&0x1F)<<16)+(s.s.recv(1)[0]<<8)+s.s.recv(1)[0] + else: l=((lb&0x0F)<<24)+(s.s.recv(1)[0]<<16)+(s.s.recv(1)[0]<<8)+s.s.recv(1)[0] + d=b'' + while len(d)=3 else '' + else: d[w]='' + rs.append(d) + elif r=='!trap': rs.append(('!trap',t)) + return rs + def cmd(s,c,**a): + ws=[c] + for k,v in a.items(): + ws.append(('?' if k.startswith('?') else '=')+k+'='+v) + s._ws(*ws) + return s._rr() + def close(s): + if s.s: s.s.close() + +api=R('remote.vpnmurahjogja.my.id',33206).c('mi4','m14') + +token='5115036608:AAGeyXVq5eNvdAkaCQHU9NfH16uAAiSKiuk' +chat='-1002554941429' +thread='42' + +text = 'TEST2 from router' +url = 'https://api.telegram.org/bot%s/sendMessage?chat_id=%s&message_thread_id=%s&text=%s' % (token, chat, thread, urllib.parse.quote_plus(text)) +r = api.cmd('/tool/fetch', url=url, mode='https', **{'http-method': 'get', 'keep-result': 'yes'}) +for x in r: + if isinstance(x, dict): print(' ', x.get('status'), '| downloaded:', x.get('downloaded')) + else: print(' ', x) + +# baca file hasil default (biasanya 'fetch') +r = api.cmd('/file/print') +for x in r: + if isinstance(x,dict) and ('fetch' in x.get('name','') or x.get('type')=='file'): + print(' file:', x.get('name')) + +api.close() diff --git a/list-files.py b/list-files.py new file mode 100644 index 0000000..598128d --- /dev/null +++ b/list-files.py @@ -0,0 +1,73 @@ +import socket, hashlib +class R: + def __init__(s,h,p=8728): s.h=h; s.p=p; s.s=None + def c(s,u,p): + s.s=socket.socket(); s.s.settimeout(60) + s.s.connect((s.h,s.p)) + s._ws('/login','=name='+u,'=password='+p) + r=s._rr() + if any(isinstance(x,tuple) for x in r): + for x in r: + if isinstance(x,dict) and 'ret' in x: ch=x['ret'] + if ch and len(ch)==32: + hh=hashlib.md5(b'\x00'+p.encode()+bytes.fromhex(ch)).hexdigest() + s._ws('/login','=name='+u,'=response=00'+hh); s._rr() + return s + def _el(s,l): + if l<0x80: return bytes([l]) + elif l<0x4000: l|=0x8000; return bytes([(l>>8)&0xFF,l&0xFF]) + else: return bytes([0xC0|((l>>16)&0x0F),(l>>8)&0xFF,l&0xFF]) if l<0x200000 else bytes([0xE0|((l>>24)&0x07),(l>>16)&0xFF,(l>>8)&0xFF,l&0xFF]) + def _ww(s,w): s.s.sendall(s._el(len(w))+w.encode()) + def _ws(s,c,*ws): s._ww(c); [s._ww(w) for w in ws]; s.s.sendall(b'\x00') + def _rw(s): + f=s.s.recv(1) + if f==b'\x00': return None + lb=f[0]; l=lb + if lb&0x80: + if (lb&0xC0)==0x80: l=((lb&0x3F)<<8)+s.s.recv(1)[0] + elif (lb&0xE0)==0xC0: l=((lb&0x1F)<<16)+(s.s.recv(1)[0]<<8)+s.s.recv(1)[0] + else: l=((lb&0x0F)<<24)+(s.s.recv(1)[0]<<16)+(s.s.recv(1)[0]<<8)+s.s.recv(1)[0] + d=b'' + while len(d)=3 else '' + else: d[w]='' + rs.append(d) + elif r=='!trap': rs.append(('!trap',t)) + return rs + def cmd(s,c,**a): + ws=[c] + for k,v in a.items(): + ws.append(('?' if k.startswith('?') else '=')+k+'='+v) + s._ws(*ws) + return s._rr() + def close(s): + if s.s: s.s.close() + +api=R('remote.vpnmurahjogja.my.id',33206).c('mi4','m14') +files = api.cmd('/file/print') +for f in files: + if isinstance(f,dict): + n = f.get('name','') + if 'send' in n.lower() or n.endswith('.txt') or 'fetch' in n.lower(): + print('FILE:', n, f.get('size')) +api.close() diff --git a/list-sched.py b/list-sched.py new file mode 100644 index 0000000..2598038 --- /dev/null +++ b/list-sched.py @@ -0,0 +1,78 @@ +import socket, hashlib +class R: + def __init__(s,h,p=8728): s.h=h; s.p=p; s.s=None + def c(s,u,p): + s.s=socket.socket(); s.s.settimeout(30) + s.s.connect((s.h,s.p)) + s._ws('/login','=name='+u,'=password='+p) + r=s._rr() + if any(isinstance(x,tuple) for x in r): + for x in r: + if isinstance(x,dict) and 'ret' in x: ch=x['ret'] + if ch and len(ch)==32: + hh=hashlib.md5(b'\x00'+p.encode()+bytes.fromhex(ch)).hexdigest() + s._ws('/login','=name='+u,'=response=00'+hh); s._rr() + return s + def _el(s,l): + if l<0x80: return bytes([l]) + elif l<0x4000: l|=0x8000; return bytes([(l>>8)&0xFF,l&0xFF]) + else: return bytes([0xC0|((l>>16)&0x0F),(l>>8)&0xFF,l&0xFF]) if l<0x200000 else bytes([0xE0|((l>>24)&0x07),(l>>16)&0xFF,(l>>8)&0xFF,l&0xFF]) + def _ww(s,w): s.s.sendall(s._el(len(w))+w.encode()) + def _ws(s,c,*ws): s._ww(c); [s._ww(w) for w in ws]; s.s.sendall(b'\x00') + def _rw(s): + f=s.s.recv(1) + if f==b'\x00': return None + lb=f[0]; l=lb + if lb&0x80: + if (lb&0xC0)==0x80: l=((lb&0x3F)<<8)+s.s.recv(1)[0] + elif (lb&0xE0)==0xC0: l=((lb&0x1F)<<16)+(s.s.recv(1)[0]<<8)+s.s.recv(1)[0] + else: l=((lb&0x0F)<<24)+(s.s.recv(1)[0]<<16)+(s.s.recv(1)[0]<<8)+s.s.recv(1)[0] + d=b'' + while len(d)=3 else '' + else: d[w]='' + rs.append(d) + elif r=='!trap': rs.append(('!trap',t)) + return rs + def cmd(s,c,**a): + ws=[c] + for k,v in a.items(): + ws.append(('?' if k.startswith('?') else '=')+k+'='+v) + s._ws(*ws) + return s._rr() + def close(s): + if s.s: s.s.close() + +api=R('remote.vpnmurahjogja.my.id',33206).c('mi4','m14') + +r = api.cmd('/system/scheduler/print') +for x in r: + if isinstance(x,dict): + print('---') + print('id:', x.get('.id'), '| name:', x.get('name')) + print('interval:', x.get('interval'), '| start-time:', x.get('start-time'), '| policy:', x.get('policy')) + oe = x.get('on-event','') + print('on-event len:', len(oe)) + print('on-event:', oe) + +api.close() diff --git a/mk-user.py b/mk-user.py new file mode 100644 index 0000000..c88f036 --- /dev/null +++ b/mk-user.py @@ -0,0 +1,76 @@ +import socket, hashlib +class R: + def __init__(s,h,p=8728): s.h=h; s.p=p; s.s=None + def c(s,u,p): + s.s=socket.socket(); s.s.settimeout(60) + s.s.connect((s.h,s.p)) + s._ws('/login','=name='+u,'=password='+p) + r=s._rr() + if any(isinstance(x,tuple) for x in r): + for x in r: + if isinstance(x,dict) and 'ret' in x: ch=x['ret'] + if ch and len(ch)==32: + hh=hashlib.md5(b'\x00'+p.encode()+bytes.fromhex(ch)).hexdigest() + s._ws('/login','=name='+u,'=response=00'+hh); s._rr() + return s + def _el(s,l): + if l<0x80: return bytes([l]) + elif l<0x4000: l|=0x8000; return bytes([(l>>8)&0xFF,l&0xFF]) + else: return bytes([0xC0|((l>>16)&0x0F),(l>>8)&0xFF,l&0xFF]) if l<0x200000 else bytes([0xE0|((l>>24)&0x07),(l>>16)&0xFF,(l>>8)&0xFF,l&0xFF]) + def _ww(s,w): s.s.sendall(s._el(len(w))+w.encode()) + def _ws(s,c,*ws): s._ww(c); [s._ww(w) for w in ws]; s.s.sendall(b'\x00') + def _rw(s): + f=s.s.recv(1) + if f==b'\x00': return None + lb=f[0]; l=lb + if lb&0x80: + if (lb&0xC0)==0x80: l=((lb&0x3F)<<8)+s.s.recv(1)[0] + elif (lb&0xE0)==0xC0: l=((lb&0x1F)<<16)+(s.s.recv(1)[0]<<8)+s.s.recv(1)[0] + else: l=((lb&0x0F)<<24)+(s.s.recv(1)[0]<<16)+(s.s.recv(1)[0]<<8)+s.s.recv(1)[0] + d=b'' + while len(d)=3 else '' + else: d[w]='' + rs.append(d) + elif r=='!trap': rs.append(('!trap',t)) + return rs + def cmd(s,c,**a): + ws=[c] + for k,v in a.items(): + ws.append(('?' if k.startswith('?') else '=')+k+'='+v) + s._ws(*ws) + return s._rr() + def close(s): + if s.s: s.s.close() + +api=R('remote.vpnmurahjogja.my.id',33206).c('mi4','m14') + +name='TMR-1' +# buat user test: limit-uptime 10s, profile hotspot +print('add:', api.cmd('/ip/hotspot/user/add', **{'name': name, 'password': 'test123', 'limit-uptime': '10s', 'profile': 'default'})) +# verifikasi +r = api.cmd('/ip/hotspot/user/print', **{'?name': name}) +for x in r: + if isinstance(x,dict): + print('USER:', x.get('name'), 'limit-uptime=', x.get('limit-uptime'), 'id=', x.get('.id')) +api.close() diff --git a/patch-notif.py b/patch-notif.py new file mode 100644 index 0000000..1edb104 --- /dev/null +++ b/patch-notif.py @@ -0,0 +1,84 @@ +import socket, hashlib +class R: + def __init__(s,h,p=8728): s.h=h; s.p=p; s.s=None + def c(s,u,p): + s.s=socket.socket(); s.s.settimeout(30) + s.s.connect((s.h,s.p)) + s._ws('/login','=name='+u,'=password='+p) + r=s._rr() + if any(isinstance(x,tuple) for x in r): + for x in r: + if isinstance(x,dict) and 'ret' in x: ch=x['ret'] + if ch and len(ch)==32: + hh=hashlib.md5(b'\x00'+p.encode()+bytes.fromhex(ch)).hexdigest() + s._ws('/login','=name='+u,'=response=00'+hh); s._rr() + return s + def _el(s,l): + if l<0x80: return bytes([l]) + elif l<0x4000: l|=0x8000; return bytes([(l>>8)&0xFF,l&0xFF]) + else: return bytes([0xC0|((l>>16)&0x0F),(l>>8)&0xFF,l&0xFF]) if l<0x200000 else bytes([0xE0|((l>>24)&0x07),(l>>16)&0xFF,(l>>8)&0xFF,l&0xFF]) + def _ww(s,w): s.s.sendall(s._el(len(w))+w.encode()) + def _ws(s,c,*ws): s._ww(c); [s._ww(w) for w in ws]; s.s.sendall(b'\x00') + def _rw(s): + f=s.s.recv(1) + if f==b'\x00': return None + lb=f[0]; l=lb + if lb&0x80: + if (lb&0xC0)==0x80: l=((lb&0x3F)<<8)+s.s.recv(1)[0] + elif (lb&0xE0)==0xC0: l=((lb&0x1F)<<16)+(s.s.recv(1)[0]<<8)+s.s.recv(1)[0] + else: l=((lb&0x0F)<<24)+(s.s.recv(1)[0]<<16)+(s.s.recv(1)[0]<<8)+s.s.recv(1)[0] + d=b'' + while len(d)=3 else '' + else: d[w]='' + rs.append(d) + elif r=='!trap': rs.append(('!trap',t)) + return rs + def cmd(s,c,**a): + ws=[c] + for k,v in a.items(): + ws.append(('?' if k.startswith('?') else '=')+k+'='+v) + s._ws(*ws) + return s._rr() + def close(s): + if s.s: s.s.close() + +OLD = '/tool fetch url=("http://192.168.100.6/notify-expire.php?key=msr-2026-reset&name=" . $name) http-method=get keep-result=no' +NEW = '/tool fetch url=("https://api.telegram.org/bot5115036608:AAGeyXVq5eNvdAkaCQHU9NfH16uAAiSKiuk/sendMessage?chat_id=-1002554941429&message_thread_id=42&text=" . $name . "+expired") mode=https http-method=get keep-result=no' + +api=R('remote.vpnmurahjogja.my.id',33206).c('mi4','m14') + +for name in ['Family', 'Bulanan2', 'Bulanan4']: + r = api.cmd('/system/scheduler/print', **{'?name': name}) + for x in r: + if isinstance(x,dict): + oe = x.get('on-event','') + if OLD in oe: + new = oe.replace(OLD, NEW) + res = api.cmd('/system/scheduler/set', **{'.id': x.get('.id'), 'on-event': new}) + ok = not any(isinstance(t,tuple) and t[0]=='!trap' for t in res) + print('%s patched %s (len %d -> %d)' % (name, 'OK' if ok else 'FAIL '+str(res), len(oe), len(new))) + else: + print('%s: OLD pattern NOT found!' % name) + +api.close() diff --git a/patch-notif2.py b/patch-notif2.py new file mode 100644 index 0000000..6d20765 --- /dev/null +++ b/patch-notif2.py @@ -0,0 +1,100 @@ +import socket, hashlib +class R: + def __init__(s,h,p=8728): s.h=h; s.p=p; s.s=None + def c(s,u,p): + s.s=socket.socket(); s.s.settimeout(60) + s.s.connect((s.h,s.p)) + s._ws('/login','=name='+u,'=password='+p) + r=s._rr() + if any(isinstance(x,tuple) for x in r): + for x in r: + if isinstance(x,dict) and 'ret' in x: ch=x['ret'] + if ch and len(ch)==32: + hh=hashlib.md5(b'\x00'+p.encode()+bytes.fromhex(ch)).hexdigest() + s._ws('/login','=name='+u,'=response=00'+hh); s._rr() + return s + def _el(s,l): + if l<0x80: return bytes([l]) + elif l<0x4000: l|=0x8000; return bytes([(l>>8)&0xFF,l&0xFF]) + else: return bytes([0xC0|((l>>16)&0x0F),(l>>8)&0xFF,l&0xFF]) if l<0x200000 else bytes([0xE0|((l>>24)&0x07),(l>>16)&0xFF,(l>>8)&0xFF,l&0xFF]) + def _ww(s,w): s.s.sendall(s._el(len(w))+w.encode()) + def _ws(s,c,*ws): s._ww(c); [s._ww(w) for w in ws]; s.s.sendall(b'\x00') + def _rw(s): + f=s.s.recv(1) + if f==b'\x00': return None + lb=f[0]; l=lb + if lb&0x80: + if (lb&0xC0)==0x80: l=((lb&0x3F)<<8)+s.s.recv(1)[0] + elif (lb&0xE0)==0xC0: l=((lb&0x1F)<<16)+(s.s.recv(1)[0]<<8)+s.s.recv(1)[0] + else: l=((lb&0x0F)<<24)+(s.s.recv(1)[0]<<16)+(s.s.recv(1)[0]<<8)+s.s.recv(1)[0] + d=b'' + while len(d)=3 else '' + else: d[w]='' + rs.append(d) + elif r=='!trap': rs.append(('!trap',t)) + return rs + def cmd(s,c,**a): + ws=[c] + for k,v in a.items(): + ws.append(('?' if k.startswith('?') else '=')+k+'='+v) + s._ws(*ws) + return s._rr() + def close(s): + if s.s: s.s.close() + +api=R('remote.vpnmurahjogja.my.id',33206).c('mi4','m14') + +# OLD fetch (tanpa tombol) - persis dari on-event +OLD = ('/tool fetch url=("https://api.telegram.org/bot5115036608:AAGeyXVq5eNvdAkaCQHU9NfH16uAAiSKiuk/' + 'sendMessage?chat_id=-1002554941429&message_thread_id=42&text=" . $name . "+expired") ' + 'mode=https http-method=get keep-result=no') + +# NEW fetch + reply_markup (inline keyboard, tombol Reset, callback_data=reset_$name) +# JSON: {"inline_keyboard":[[{"text":"๐Ÿ”„ Reset","callback_data":"reset_"}]]} +# URL-encoded: +MARKUP_PREFIX = '%7B%22inline_keyboard%22%3A%5B%5B%7B%22text%22%3A%22%F0%9F%94%84%20Reset%22%2C%22callback_data%22%3A%22reset_' +MARKUP_SUFFIX = '%22%7D%5D%5D%7D' + +NEW = ('/tool fetch url=("https://api.telegram.org/bot5115036608:AAGeyXVq5eNvdAkaCQHU9NfH16uAAiSKiuk/' + 'sendMessage?chat_id=-1002554941429&message_thread_id=42&text=" . $name . "+expired' + '&reply_markup=" . "' + MARKUP_PREFIX + '" . $name . "' + MARKUP_SUFFIX + '") ' + 'mode=https http-method=get keep-result=no') + +print('NEW snippet tail:', NEW[-120:]) +print() + +for name in ['Family', 'Bulanan2', 'Bulanan4']: + r = api.cmd('/system/scheduler/print', **{'?name': name}) + for x in r: + if isinstance(x,dict): + oe = x.get('on-event','') + if OLD in oe: + new = oe.replace(OLD, NEW) + res = api.cmd('/system/scheduler/set', **{'.id': x.get('.id'), 'on-event': new}) + ok = not any(isinstance(t,tuple) and t[0]=='!trap' for t in res) + print('%s patched %s (len %d -> %d)' % (name, 'OK' if ok else 'FAIL '+str(res), len(oe), len(new))) + else: + print('%s: OLD pattern NOT found!' % name) + +api.close() diff --git a/patch-test1.py b/patch-test1.py new file mode 100644 index 0000000..bfbc396 --- /dev/null +++ b/patch-test1.py @@ -0,0 +1,86 @@ +import socket, hashlib +class R: + def __init__(s,h,p=8728): s.h=h; s.p=p; s.s=None + def c(s,u,p): + s.s=socket.socket(); s.s.settimeout(30) + s.s.connect((s.h,s.p)) + s._ws('/login','=name='+u,'=password='+p) + r=s._rr() + if any(isinstance(x,tuple) for x in r): + for x in r: + if isinstance(x,dict) and 'ret' in x: ch=x['ret'] + if ch and len(ch)==32: + hh=hashlib.md5(b'\x00'+p.encode()+bytes.fromhex(ch)).hexdigest() + s._ws('/login','=name='+u,'=response=00'+hh); s._rr() + return s + def _el(s,l): + if l<0x80: return bytes([l]) + elif l<0x4000: l|=0x8000; return bytes([(l>>8)&0xFF,l&0xFF]) + else: return bytes([0xC0|((l>>16)&0x0F),(l>>8)&0xFF,l&0xFF]) if l<0x200000 else bytes([0xE0|((l>>24)&0x07),(l>>16)&0xFF,(l>>8)&0xFF,l&0xFF]) + def _ww(s,w): s.s.sendall(s._el(len(w))+w.encode()) + def _ws(s,c,*ws): s._ww(c); [s._ww(w) for w in ws]; s.s.sendall(b'\x00') + def _rw(s): + f=s.s.recv(1) + if f==b'\x00': return None + lb=f[0]; l=lb + if lb&0x80: + if (lb&0xC0)==0x80: l=((lb&0x3F)<<8)+s.s.recv(1)[0] + elif (lb&0xE0)==0xC0: l=((lb&0x1F)<<16)+(s.s.recv(1)[0]<<8)+s.s.recv(1)[0] + else: l=((lb&0x0F)<<24)+(s.s.recv(1)[0]<<16)+(s.s.recv(1)[0]<<8)+s.s.recv(1)[0] + d=b'' + while len(d)=3 else '' + else: d[w]='' + rs.append(d) + elif r=='!trap': rs.append(('!trap',t)) + return rs + def cmd(s,c,**a): + ws=[c] + for k,v in a.items(): + ws.append(('?' if k.startswith('?') else '=')+k+'='+v) + s._ws(*ws) + return s._rr() + def close(s): + if s.s: s.s.close() + +api=R('remote.vpnmurahjogja.my.id',33206).c('mi4','m14') + +# Ambil Family, patch, dan capture response set +r = api.cmd('/system/scheduler/print', **{'?name': 'Family'}) +x = [x for x in r if isinstance(x,dict)][0] +oe = x.get('on-event','') +OLD = '/tool fetch url=("http://192.168.100.6/notify-expire.php?key=msr-2026-reset&name=" . $name) http-method=get keep-result=no' +NEW = '/tool fetch url=("https://api.telegram.org/bot5115036608:AAGeyXVq5eNvdAkaCQHU9NfH16uAAiSKiuk/sendMessage?chat_id=-1002554941429&message_thread_id=42&text=" . $name . "+expired") mode=https http-method=get keep-result=no' +new = oe.replace(OLD, NEW) +print('old in oe:', OLD in oe) +res = api.cmd('/system/scheduler/set', **{'=.id': x.get('.id'), 'on-event': new}) +print('set response:', res) + +# baca lagi +r2 = api.cmd('/system/scheduler/print', **{'?name': 'Family'}) +for y in r2: + if isinstance(y,dict): + oe2 = y.get('on-event','') + print('after: len', len(oe2), 'telegram.org:', 'api.telegram.org' in oe2, 'old100.6:', '192.168.100.6' in oe2) + +api.close() diff --git a/patch-test2.py b/patch-test2.py new file mode 100644 index 0000000..096abb0 --- /dev/null +++ b/patch-test2.py @@ -0,0 +1,81 @@ +import socket, hashlib +class R: + def __init__(s,h,p=8728): s.h=h; s.p=p; s.s=None + def c(s,u,p): + s.s=socket.socket(); s.s.settimeout(30) + s.s.connect((s.h,s.p)) + s._ws('/login','=name='+u,'=password='+p) + r=s._rr() + if any(isinstance(x,tuple) for x in r): + for x in r: + if isinstance(x,dict) and 'ret' in x: ch=x['ret'] + if ch and len(ch)==32: + hh=hashlib.md5(b'\x00'+p.encode()+bytes.fromhex(ch)).hexdigest() + s._ws('/login','=name='+u,'=response=00'+hh); s._rr() + return s + def _el(s,l): + if l<0x80: return bytes([l]) + elif l<0x4000: l|=0x8000; return bytes([(l>>8)&0xFF,l&0xFF]) + else: return bytes([0xC0|((l>>16)&0x0F),(l>>8)&0xFF,l&0xFF]) if l<0x200000 else bytes([0xE0|((l>>24)&0x07),(l>>16)&0xFF,(l>>8)&0xFF,l&0xFF]) + def _ww(s,w): s.s.sendall(s._el(len(w))+w.encode()) + def _ws(s,c,*ws): s._ww(c); [s._ww(w) for w in ws]; s.s.sendall(b'\x00') + def _rw(s): + f=s.s.recv(1) + if f==b'\x00': return None + lb=f[0]; l=lb + if lb&0x80: + if (lb&0xC0)==0x80: l=((lb&0x3F)<<8)+s.s.recv(1)[0] + elif (lb&0xE0)==0xC0: l=((lb&0x1F)<<16)+(s.s.recv(1)[0]<<8)+s.s.recv(1)[0] + else: l=((lb&0x0F)<<24)+(s.s.recv(1)[0]<<16)+(s.s.recv(1)[0]<<8)+s.s.recv(1)[0] + d=b'' + while len(d)=3 else '' + else: d[w]='' + rs.append(d) + elif r=='!trap': rs.append(('!trap',t)) + return rs + def cmd(s,c,**a): + ws=[c] + for k,v in a.items(): + ws.append(('?' if k.startswith('?') else '=')+k+'='+v) + s._ws(*ws) + return s._rr() + def close(s): + if s.s: s.s.close() + +api=R('remote.vpnmurahjogja.my.id',33206).c('mi4','m14') + +# test set sederhana +r = api.cmd('/system/scheduler/print', **{'?name': 'Family'}) +x = [x for x in r if isinstance(x,dict)][0] +print('id:', x.get('.id')) + +# 1. coba set dengan nilai simple +res = api.cmd('/system/scheduler/set', **{'=.id': x.get('.id'), 'on-event': ':put "hello"'}) +print('test1 (simple):', res) + +# 2. coba cek apakah on-event adalah param yang benar - coba hanya ganti interval +res2 = api.cmd('/system/scheduler/set', **{'=.id': x.get('.id'), 'interval': '2m37s'}) +print('test2 (interval):', res2) + +api.close() diff --git a/patch-test3.py b/patch-test3.py new file mode 100644 index 0000000..895c10b --- /dev/null +++ b/patch-test3.py @@ -0,0 +1,75 @@ +import socket, hashlib +class R: + def __init__(s,h,p=8728): s.h=h; s.p=p; s.s=None + def c(s,u,p): + s.s=socket.socket(); s.s.settimeout(30) + s.s.connect((s.h,s.p)) + s._ws('/login','=name='+u,'=password='+p) + r=s._rr() + if any(isinstance(x,tuple) for x in r): + for x in r: + if isinstance(x,dict) and 'ret' in x: ch=x['ret'] + if ch and len(ch)==32: + hh=hashlib.md5(b'\x00'+p.encode()+bytes.fromhex(ch)).hexdigest() + s._ws('/login','=name='+u,'=response=00'+hh); s._rr() + return s + def _el(s,l): + if l<0x80: return bytes([l]) + elif l<0x4000: l|=0x8000; return bytes([(l>>8)&0xFF,l&0xFF]) + else: return bytes([0xC0|((l>>16)&0x0F),(l>>8)&0xFF,l&0xFF]) if l<0x200000 else bytes([0xE0|((l>>24)&0x07),(l>>16)&0xFF,(l>>8)&0xFF,l&0xFF]) + def _ww(s,w): s.s.sendall(s._el(len(w))+w.encode()) + def _ws(s,c,*ws): s._ww(c); [s._ww(w) for w in ws]; s.s.sendall(b'\x00') + def _rw(s): + f=s.s.recv(1) + if f==b'\x00': return None + lb=f[0]; l=lb + if lb&0x80: + if (lb&0xC0)==0x80: l=((lb&0x3F)<<8)+s.s.recv(1)[0] + elif (lb&0xE0)==0xC0: l=((lb&0x1F)<<16)+(s.s.recv(1)[0]<<8)+s.s.recv(1)[0] + else: l=((lb&0x0F)<<24)+(s.s.recv(1)[0]<<16)+(s.s.recv(1)[0]<<8)+s.s.recv(1)[0] + d=b'' + while len(d)=3 else '' + else: d[w]='' + rs.append(d) + elif r=='!trap': rs.append(('!trap',t)) + return rs + def cmd(s,c,**a): + ws=[c] + for k,v in a.items(): + ws.append(('?' if k.startswith('?') else '=')+k+'='+v) + s._ws(*ws) + return s._rr() + def close(s): + if s.s: s.s.close() + +api=R('remote.vpnmurahjogja.my.id',33206).c('mi4','m14') + +# coba set dengan numbers +res = api.cmd('/system/scheduler/set', **{'=numbers': 'Family', 'interval': '2m37s'}) +print('test numbers=name:', res) + +res2 = api.cmd('/system/scheduler/set', **{'=numbers': '5', 'interval': '2m37s'}) +print('test numbers=5:', res2) + +api.close() diff --git a/patch-test4.py b/patch-test4.py new file mode 100644 index 0000000..0c96751 --- /dev/null +++ b/patch-test4.py @@ -0,0 +1,81 @@ +import socket, hashlib +class R: + def __init__(s,h,p=8728): s.h=h; s.p=p; s.s=None + def c(s,u,p): + s.s=socket.socket(); s.s.settimeout(30) + s.s.connect((s.h,s.p)) + s._ws('/login','=name='+u,'=password='+p) + r=s._rr() + if any(isinstance(x,tuple) for x in r): + for x in r: + if isinstance(x,dict) and 'ret' in x: ch=x['ret'] + if ch and len(ch)==32: + hh=hashlib.md5(b'\x00'+p.encode()+bytes.fromhex(ch)).hexdigest() + s._ws('/login','=name='+u,'=response=00'+hh); s._rr() + return s + def _el(s,l): + if l<0x80: return bytes([l]) + elif l<0x4000: l|=0x8000; return bytes([(l>>8)&0xFF,l&0xFF]) + else: return bytes([0xC0|((l>>16)&0x0F),(l>>8)&0xFF,l&0xFF]) if l<0x200000 else bytes([0xE0|((l>>24)&0x07),(l>>16)&0xFF,(l>>8)&0xFF,l&0xFF]) + def _ww(s,w): s.s.sendall(s._el(len(w))+w.encode()) + def _ws(s,c,*ws): s._ww(c); [s._ww(w) for w in ws]; s.s.sendall(b'\x00') + def _rw(s): + f=s.s.recv(1) + if f==b'\x00': return None + lb=f[0]; l=lb + if lb&0x80: + if (lb&0xC0)==0x80: l=((lb&0x3F)<<8)+s.s.recv(1)[0] + elif (lb&0xE0)==0xC0: l=((lb&0x1F)<<16)+(s.s.recv(1)[0]<<8)+s.s.recv(1)[0] + else: l=((lb&0x0F)<<24)+(s.s.recv(1)[0]<<16)+(s.s.recv(1)[0]<<8)+s.s.recv(1)[0] + d=b'' + while len(d)=3 else '' + else: d[w]='' + rs.append(d) + elif r=='!trap': rs.append(('!trap',t)) + return rs + def cmd(s,c,**a): + ws=[c] + for k,v in a.items(): + ws.append(('?' if k.startswith('?') else '=')+k+'='+v) + s._ws(*ws) + return s._rr() + def close(s): + if s.s: s.s.close() + +api=R('remote.vpnmurahjogja.my.id',33206).c('mi4','m14') + +# test set pada user hotspot (known working?) - cek apakah cmd helper OK +r = api.cmd('/ip/hotspot/user/print', **{'?name': 'Nadia'}) +print('Nadia found:', bool([x for x in r if isinstance(x,dict)])) +res = api.cmd('/ip/hotspot/user/set', **{'=numbers': 'Nadia', 'comment': 'test-set'}) +print('user set numbers=name:', res) +# revert +res2 = api.cmd('/ip/hotspot/user/set', **{'=numbers': 'Nadia', 'comment': 'aug/20/2026 23:59:59'}) +print('user set revert:', res2) + +# sekarang coba scheduler dengan =.id +res3 = api.cmd('/system/scheduler/set', **{'=.id': '*5', 'interval': '2m37s'}) +print('sched set .id:', res3) + +api.close() diff --git a/patch-test5.py b/patch-test5.py new file mode 100644 index 0000000..d8c2561 --- /dev/null +++ b/patch-test5.py @@ -0,0 +1,81 @@ +import socket, hashlib +class R: + def __init__(s,h,p=8728): s.h=h; s.p=p; s.s=None + def c(s,u,p): + s.s=socket.socket(); s.s.settimeout(30) + s.s.connect((s.h,s.p)) + s._ws('/login','=name='+u,'=password='+p) + r=s._rr() + if any(isinstance(x,tuple) for x in r): + for x in r: + if isinstance(x,dict) and 'ret' in x: ch=x['ret'] + if ch and len(ch)==32: + hh=hashlib.md5(b'\x00'+p.encode()+bytes.fromhex(ch)).hexdigest() + s._ws('/login','=name='+u,'=response=00'+hh); s._rr() + return s + def _el(s,l): + if l<0x80: return bytes([l]) + elif l<0x4000: l|=0x8000; return bytes([(l>>8)&0xFF,l&0xFF]) + else: return bytes([0xC0|((l>>16)&0x0F),(l>>8)&0xFF,l&0xFF]) if l<0x200000 else bytes([0xE0|((l>>24)&0x07),(l>>16)&0xFF,(l>>8)&0xFF,l&0xFF]) + def _ww(s,w): s.s.sendall(s._el(len(w))+w.encode()) + def _ws(s,c,*ws): s._ww(c); [s._ww(w) for w in ws]; s.s.sendall(b'\x00') + def _rw(s): + f=s.s.recv(1) + if f==b'\x00': return None + lb=f[0]; l=lb + if lb&0x80: + if (lb&0xC0)==0x80: l=((lb&0x3F)<<8)+s.s.recv(1)[0] + elif (lb&0xE0)==0xC0: l=((lb&0x1F)<<16)+(s.s.recv(1)[0]<<8)+s.s.recv(1)[0] + else: l=((lb&0x0F)<<24)+(s.s.recv(1)[0]<<16)+(s.s.recv(1)[0]<<8)+s.s.recv(1)[0] + d=b'' + while len(d)=3 else '' + else: d[w]='' + rs.append(d) + elif r=='!trap': rs.append(('!trap',t)) + return rs + def cmd(s,c,**a): + ws=[c] + for k,v in a.items(): + ws.append(('?' if k.startswith('?') else '=')+k+'='+v) + s._ws(*ws) + return s._rr() + def close(s): + if s.s: s.s.close() + +api=R('remote.vpnmurahjogja.my.id',33206).c('mi4','m14') + +# print Family scheduler dulu utk dapat .id asli +r = api.cmd('/system/scheduler/print', **{'?name': 'Family'}) +x = [x for x in r if isinstance(x,dict)][0] +print('id:', x.get('.id')) + +# test set dengan kunci TANPA '=' +res = api.cmd('/system/scheduler/set', **{'numbers': x.get('.id'), 'interval': '2m37s'}) +print('sched set numbers(no=):', res) + +# test lain: gunakan =.id dengan key '.id' +res2 = api.cmd('/system/scheduler/set', **{'.id': x.get('.id'), 'interval': '2m37s'}) +print('sched set .id(no=):', res2) + +api.close() diff --git a/phpok.php b/phpok.php new file mode 100644 index 0000000..8c0e9ad --- /dev/null +++ b/phpok.php @@ -0,0 +1,2 @@ +/dev/null 2>&1 +sleep 1 +curl -s -m 30 "$URL" >/dev/null 2>&1 diff --git a/read-script.py b/read-script.py new file mode 100644 index 0000000..271595e --- /dev/null +++ b/read-script.py @@ -0,0 +1,96 @@ +import socket, hashlib + +class R: + def __init__(self,h,p=8728): self.h=h; self.p=p; self.s=None + def c(self,u,p): + self.s=socket.socket(socket.AF_INET,socket.SOCK_STREAM); self.s.settimeout(10) + self.s.connect((self.h,self.p)) + self._ws('/login','=name='+u,'=password='+p) + r=self._rr() + if any(isinstance(x,tuple) and x[0]=='!trap' for x in r): + for x in r: + if isinstance(x,dict) and 'ret' in x: ch=x['ret'] + if ch and len(ch)==32: + import hashlib as h + resp='00'+h.md5(b'\x00'+p.encode()+bytes.fromhex(ch)).hexdigest() + self._ws('/login','=name='+u,'=response=00'+resp) + self._rr() + return self + def _el(self,l): + if l<0x80: return bytes([l]) + elif l<0x4000: l|=0x8000; return bytes([(l>>8)&0xFF,l&0xFF]) + elif l<0x200000: l|=0xC00000; return bytes([(l>>16)&0xFF,(l>>8)&0xFF,l&0xFF]) + elif l<0x10000000: l|=0xE0000000; return bytes([(l>>24)&0xFF,(l>>16)&0xFF,(l>>8)&0xFF,l&0xFF]) + else: return bytes([0xF0,(l>>24)&0xFF,(l>>16)&0xFF,(l>>8)&0xFF,l&0xFF]) + def _ww(self,w): self.s.sendall(self._el(len(w))+w.encode('utf-8')) + def _ws(self,c,*ws): self._ww(c); [self._ww(w) for w in ws]; self.s.sendall(b'\x00') + def _rb(self): return self.s.recv(1) + def _rw(self): + f=self._rb() + if f==b'\x00': return None + lb=f[0] + if lb&0x80: + if (lb&0xC0)==0x80: l=((lb&0x3F)<<8)+self._rb()[0] + elif (lb&0xE0)==0xC0: l=((lb&0x1F)<<16)+(self._rb()[0]<<8)+self._rb()[0] + elif (lb&0xF0)==0xE0: l=((lb&0x0F)<<24)+(self._rb()[0]<<16)+(self._rb()[0]<<8)+self._rb()[0] + else: l=(self._rb()[0]<<24)+(self._rb()[0]<<16)+(self._rb()[0]<<8)+self._rb()[0] + else: l=lb + d=b'' + while len(d)=3 else '' + else: d[w]='' + rs.append(d) + elif r=='!trap': rs.append(('!trap',t)) + return rs + def cmd(self,c,**a): + ws=[c] + for k,v in a.items(): + if k.startswith('?'): ws.append(f'{k}={v}') + else: ws.append(f'={k}={v}') + self._ws(*ws) + return self._rr() + def close(self): + if self.s: self.s.close(); self.s=None + +api=R('192.168.100.2').c('mi4','m14') + +# Dump on-event Bulanan2 +for s in api.cmd('/system/scheduler/print'): + if s['name'] == 'Bulanan2': + on_event = s.get('on-event', '') + print(f"=== Bulanan2 on-event ({len(on_event)} bytes) ===") + print(on_event) + break + +# Dump on-login profile Bulanan2 +for p in api.cmd('/ip/hotspot/profile/print'): + if p['name'] == 'Bulanan2': + on_login = p.get('on-login', '') + print(f"\n=== Bulanan2 on-login ({len(on_login)} bytes) ===") + print(on_login) + break + +api.close() diff --git a/remove-binding-labibah.py b/remove-binding-labibah.py new file mode 100644 index 0000000..b17f7d9 --- /dev/null +++ b/remove-binding-labibah.py @@ -0,0 +1,118 @@ +import socket, hashlib + +class R: + def __init__(self,h,p=8728): self.h=h; self.p=p; self.s=None + def c(self,u,p): + self.s=socket.socket(socket.AF_INET,socket.SOCK_STREAM); self.s.settimeout(10) + self.s.connect((self.h,self.p)) + self._ws('/login','=name='+u,'=password='+p) + r=self._rr() + if any(isinstance(x,tuple) and x[0]=='!trap' for x in r): + for x in r: + if isinstance(x,dict) and 'ret' in x: ch=x['ret'] + if ch and len(ch)==32: + h=hashlib.md5(b'\x00'+p.encode()+bytes.fromhex(ch)).hexdigest() + self._ws('/login','=name='+u,'=response=00'+h) + self._rr() + return self + def _el(self,l): + if l<0x80: return bytes([l]) + elif l<0x4000: l|=0x8000; return bytes([(l>>8)&0xFF,l&0xFF]) + elif l<0x200000: l|=0xC00000; return bytes([(l>>16)&0xFF,(l>>8)&0xFF,l&0xFF]) + elif l<0x10000000: l|=0xE0000000; return bytes([(l>>24)&0xFF,(l>>16)&0xFF,(l>>8)&0xFF,l&0xFF]) + else: return bytes([0xF0,(l>>24)&0xFF,(l>>16)&0xFF,(l>>8)&0xFF,l&0xFF]) + def _ww(self,w): self.s.sendall(self._el(len(w))+w.encode('utf-8')) + def _ws(self,c,*ws): self._ww(c); [self._ww(w) for w in ws]; self.s.sendall(b'\x00') + def _rb(self): return self.s.recv(1) + def _rw(self): + f=self._rb() + if f==b'\x00': return None + lb=f[0] + if lb&0x80: + if (lb&0xC0)==0x80: l=((lb&0x3F)<<8)+self._rb()[0] + elif (lb&0xE0)==0xC0: l=((lb&0x1F)<<16)+(self._rb()[0]<<8)+self._rb()[0] + elif (lb&0xF0)==0xE0: l=((lb&0x0F)<<24)+(self._rb()[0]<<16)+(self._rb()[0]<<8)+self._rb()[0] + else: l=(self._rb()[0]<<24)+(self._rb()[0]<<16)+(self._rb()[0]<<8)+self._rb()[0] + else: l=lb + d=b'' + while len(d)=3 else '' + else: d[w]='' + rs.append(d) + elif r=='!trap': + print(f'TRAP: {t}') + rs.append(('!trap',t)) + return rs + def cmd(self,c,**a): + ws=[c] + for k,v in a.items(): + if k.startswith('?'): ws.append(f'{k}={v}') + else: ws.append(f'={k}={v}') + self._ws(*ws) + return self._rr() + def close(self): + if self.s: self.s.close(); self.s=None + +api=R('192.168.100.2').c('mi4','m14') + +# Try set with type=regular (RouterOS keyword) +for b in api.cmd('/ip/hotspot/ip-binding/print'): + mac = b.get('mac-address', '') + if 'CA:1A:02:C2:8C:1E' in mac: + bid = b['.id'] + print(f'Found: {bid} Type={b.get("type")}') + + # Try setting type to empty string + r = api.cmd('/ip/hotspot/ip-binding/set', **{'.id': bid, 'type': ''}) + print(f'Set type=\"\" result: {r}') + break + +# Check again +print() +for b in api.cmd('/ip/hotspot/ip-binding/print'): + mac = b.get('mac-address', '') + if 'CA:1A:02:C2:8C:1E' in mac: + print(f'After set: MAC={mac} Type="{b.get("type")}"') + + # If still bypassed, remove it entirely + if b.get('type') == 'bypassed' or not b.get('type'): + bid = b['.id'] + print(f'Still bypassed/empty. Removing binding {bid}...') + r = api.cmd('/ip/hotspot/ip-binding/remove', **{'.id': bid}) + print(f'Remove result: {r}') + break + +# Verify removal +print() +found = False +for b in api.cmd('/ip/hotspot/ip-binding/print'): + if 'CA:1A:02:C2:8C:1E' in b.get('mac-address', ''): + print(f'STILL EXISTS! MAC={b.get("mac-address")} Type={b.get("type")}') + found = True +if not found: + print('Binding CA:1A:02:C2:8C:1E removed successfully') + +api.close() diff --git a/rm-user.py b/rm-user.py new file mode 100644 index 0000000..9e104a9 --- /dev/null +++ b/rm-user.py @@ -0,0 +1,70 @@ +import socket, hashlib +class R: + def __init__(s,h,p=8728): s.h=h; s.p=p; s.s=None + def c(s,u,p): + s.s=socket.socket(); s.s.settimeout(60) + s.s.connect((s.h,s.p)) + s._ws('/login','=name='+u,'=password='+p) + r=s._rr() + if any(isinstance(x,tuple) for x in r): + for x in r: + if isinstance(x,dict) and 'ret' in x: ch=x['ret'] + if ch and len(ch)==32: + hh=hashlib.md5(b'\x00'+p.encode()+bytes.fromhex(ch)).hexdigest() + s._ws('/login','=name='+u,'=response=00'+hh); s._rr() + return s + def _el(s,l): + if l<0x80: return bytes([l]) + elif l<0x4000: l|=0x8000; return bytes([(l>>8)&0xFF,l&0xFF]) + else: return bytes([0xC0|((l>>16)&0x0F),(l>>8)&0xFF,l&0xFF]) if l<0x200000 else bytes([0xE0|((l>>24)&0x07),(l>>16)&0xFF,(l>>8)&0xFF,l&0xFF]) + def _ww(s,w): s.s.sendall(s._el(len(w))+w.encode()) + def _ws(s,c,*ws): s._ww(c); [s._ww(w) for w in ws]; s.s.sendall(b'\x00') + def _rw(s): + f=s.s.recv(1) + if f==b'\x00': return None + lb=f[0]; l=lb + if lb&0x80: + if (lb&0xC0)==0x80: l=((lb&0x3F)<<8)+s.s.recv(1)[0] + elif (lb&0xE0)==0xC0: l=((lb&0x1F)<<16)+(s.s.recv(1)[0]<<8)+s.s.recv(1)[0] + else: l=((lb&0x0F)<<24)+(s.s.recv(1)[0]<<16)+(s.s.recv(1)[0]<<8)+s.s.recv(1)[0] + d=b'' + while len(d)=3 else '' + else: d[w]='' + rs.append(d) + elif r=='!trap': rs.append(('!trap',t)) + return rs + def cmd(s,c,**a): + ws=[c] + for k,v in a.items(): + ws.append(('?' if k.startswith('?') else '=')+k+'='+v) + s._ws(*ws) + return s._rr() + def close(s): + if s.s: s.s.close() + +api=R('remote.vpnmurahjogja.my.id',33206).c('mi4','m14') +print('remove:', api.cmd('/ip/hotspot/user/remove', **{'numbers': '*B94'})) +r = api.cmd('/ip/hotspot/user/print', **{'?name': 'TMR-1'}) +print('remaining TMR-1:', len([x for x in r if isinstance(x,dict)])) +api.close() diff --git a/send-expired-tmr.py b/send-expired-tmr.py new file mode 100644 index 0000000..075032c --- /dev/null +++ b/send-expired-tmr.py @@ -0,0 +1,79 @@ +import socket, hashlib +class R: + def __init__(s,h,p=8728): s.h=h; s.p=p; s.s=None + def c(s,u,p): + s.s=socket.socket(); s.s.settimeout(60) + s.s.connect((s.h,s.p)) + s._ws('/login','=name='+u,'=password='+p) + r=s._rr() + if any(isinstance(x,tuple) for x in r): + for x in r: + if isinstance(x,dict) and 'ret' in x: ch=x['ret'] + if ch and len(ch)==32: + hh=hashlib.md5(b'\x00'+p.encode()+bytes.fromhex(ch)).hexdigest() + s._ws('/login','=name='+u,'=response=00'+hh); s._rr() + return s + def _el(s,l): + if l<0x80: return bytes([l]) + elif l<0x4000: l|=0x8000; return bytes([(l>>8)&0xFF,l&0xFF]) + else: return bytes([0xC0|((l>>16)&0x0F),(l>>8)&0xFF,l&0xFF]) if l<0x200000 else bytes([0xE0|((l>>24)&0x07),(l>>16)&0xFF,(l>>8)&0xFF,l&0xFF]) + def _ww(s,w): s.s.sendall(s._el(len(w))+w.encode()) + def _ws(s,c,*ws): s._ww(c); [s._ww(w) for w in ws]; s.s.sendall(b'\x00') + def _rw(s): + f=s.s.recv(1) + if f==b'\x00': return None + lb=f[0]; l=lb + if lb&0x80: + if (lb&0xC0)==0x80: l=((lb&0x3F)<<8)+s.s.recv(1)[0] + elif (lb&0xE0)==0xC0: l=((lb&0x1F)<<16)+(s.s.recv(1)[0]<<8)+s.s.recv(1)[0] + else: l=((lb&0x0F)<<24)+(s.s.recv(1)[0]<<16)+(s.s.recv(1)[0]<<8)+s.s.recv(1)[0] + d=b'' + while len(d)=3 else '' + else: d[w]='' + rs.append(d) + elif r=='!trap': rs.append(('!trap',t)) + return rs + def cmd(s,c,**a): + ws=[c] + for k,v in a.items(): + ws.append(('?' if k.startswith('?') else '=')+k+'='+v) + s._ws(*ws) + return s._rr() + def close(s): + if s.s: s.s.close() + +api=R('remote.vpnmurahjogja.my.id',33206).c('mi4','m14') + +name = 'TMR-1' +MARKUP_PREFIX = '%7B%22inline_keyboard%22%3A%5B%5B%7B%22text%22%3A%22%F0%9F%94%84%20Reset%22%2C%22callback_data%22%3A%22reset_' +MARKUP_SUFFIX = '%22%7D%5D%5D%7D' +markup = MARKUP_PREFIX + name + MARKUP_SUFFIX +url = ('https://api.telegram.org/bot5115036608:AAGeyXVq5eNvdAkaCQHU9NfH16uAAiSKiuk/' + 'sendMessage?chat_id=-1002554941429&message_thread_id=42&text=' + name + '+expired' + '&reply_markup=' + markup) + +print('sending...') +res = api.cmd('/tool/fetch', **{'url': url, 'mode': 'https', 'http-method': 'get', 'keep-result': 'no'}) +print('fetch:', [x.get('status') for x in res if isinstance(x,dict)]) +api.close() diff --git a/telegram-cb.php b/telegram-cb.php new file mode 100644 index 0000000..7543f60 --- /dev/null +++ b/telegram-cb.php @@ -0,0 +1,129 @@ + $url, + CURLOPT_RETURNTRANSFER => true, + CURLOPT_TIMEOUT => 15, + CURLOPT_SSL_VERIFYPEER => false, + ); + if (!empty($params)) { + $opts[CURLOPT_POST] = true; + $opts[CURLOPT_POSTFIELDS] = http_build_query($params); + } + curl_setopt_array($ch, $opts); + $r = curl_exec($ch); + curl_close($ch); + return json_decode($r, true); +} + +if (isset($_GET['test'])) { + $username = preg_replace('/[^a-zA-Z0-9_.@-]/', '', $_GET['test']); + if (!$username) { die('invalid username'); } + include_once(__DIR__ . '/lib/routeros_api.class.php'); + $api = new RouterosAPI(); + $api->debug = false; + $api->port = 33206; + if (!$api->connect('remote.vpnmurahjogja.my.id', 'mi4', 'm14')) { die('conn fail'); } + $found = $api->comm('/ip/hotspot/user/print', array('?name' => $username)); + if (empty($found)) { $api->disconnect(); die('user not found'); } + $uid = $found[0]['.id']; + $api->comm('/ip/hotspot/user/set', array('.id' => $uid, 'limit-uptime' => '0', 'comment' => '')); + $api->comm('/ip/hotspot/user/reset-counters', array('.id' => $uid)); + $sch = $api->comm('/system/scheduler/print', array('?name' => $username)); + if (!empty($sch) && isset($sch[0]['.id'])) { + $api->comm('/system/scheduler/remove', array('.id' => $sch[0]['.id'])); + } + $api->disconnect(); + tg_api('sendMessage', array( + 'chat_id' => '-1002554941429', + 'message_thread_id' => 42, + 'text' => "โœ… *{$username}* has been reset\nReady to use again", + 'parse_mode' => 'Markdown', + )); + echo "reset ok: {$username}"; + exit; +} + +$updates = tg_api('getUpdates', array('timeout' => 5, 'allowed_updates' => '["callback_query"]')); +if (!$updates || !isset($updates['ok']) || !$updates['ok']) { echo "no updates"; exit; } + +$processed = 0; +$last_id = 0; + +include_once(__DIR__ . '/lib/routeros_api.class.php'); + +foreach ($updates['result'] as $u) { + $cb = isset($u['callback_query']) ? $u['callback_query'] : null; + if (!$cb) continue; + + $data = isset($cb['data']) ? $cb['data'] : ''; + $cb_id = isset($cb['id']) ? $cb['id'] : ''; + $last_id = max($last_id, $u['update_id']); + + if (substr($data, 0, 6) !== 'reset_') continue; + + $username = substr($data, 6); + $username = preg_replace('/[^a-zA-Z0-9_.@-]/', '', $username); + if (!$username) continue; + + $api = new RouterosAPI(); + $api->debug = false; + $api->port = 33206; + if (!$api->connect('remote.vpnmurahjogja.my.id', 'mi4', 'm14')) { + echo "conn fail for {$username} "; + continue; + } + + $found = $api->comm('/ip/hotspot/user/print', array('?name' => $username)); + if (empty($found)) { + $api->disconnect(); + tg_api('answerCallbackQuery', array( + 'callback_query_id' => $cb_id, + 'text' => "User {$username} not found", + 'show_alert' => true, + )); + continue; + } + + $uid = $found[0]['.id']; + $api->comm('/ip/hotspot/user/set', array('.id' => $uid, 'limit-uptime' => '0', 'comment' => '')); + $api->comm('/ip/hotspot/user/reset-counters', array('.id' => $uid)); + + $sch = $api->comm('/system/scheduler/print', array('?name' => $username)); + if (!empty($sch) && isset($sch[0]['.id'])) { + $api->comm('/system/scheduler/remove', array('.id' => $sch[0]['.id'])); + } + + $api->disconnect(); + + tg_api('answerCallbackQuery', array( + 'callback_query_id' => $cb_id, + 'text' => "โœ… {$username} reset!", + 'show_alert' => false, + )); + + tg_api('sendMessage', array( + 'chat_id' => '-1002554941429', + 'message_thread_id' => 42, + 'text' => "โœ… *{$username}* has been reset\nReady to use again", + 'parse_mode' => 'Markdown', + )); + + $processed++; +} + +if ($last_id > 0) { + tg_api('getUpdates', array('offset' => $last_id + 1, 'timeout' => 1)); +} + +echo "processed: {$processed}"; diff --git a/tes-domain.py b/tes-domain.py new file mode 100644 index 0000000..d5d951c --- /dev/null +++ b/tes-domain.py @@ -0,0 +1,84 @@ +import socket, hashlib +class R: + def __init__(s,h,p=8728): s.h=h; s.p=p; s.s=None + def c(s,u,p): + s.s=socket.socket(); s.s.settimeout(30) + s.s.connect((s.h,s.p)) + s._ws('/login','=name='+u,'=password='+p) + r=s._rr() + if any(isinstance(x,tuple) for x in r): + for x in r: + if isinstance(x,dict) and 'ret' in x: ch=x['ret'] + if ch and len(ch)==32: + hh=hashlib.md5(b'\x00'+p.encode()+bytes.fromhex(ch)).hexdigest() + s._ws('/login','=name='+u,'=response=00'+hh); s._rr() + return s + def _el(s,l): + if l<0x80: return bytes([l]) + elif l<0x4000: l|=0x8000; return bytes([(l>>8)&0xFF,l&0xFF]) + else: return bytes([0xC0|((l>>16)&0x0F),(l>>8)&0xFF,l&0xFF]) if l<0x200000 else bytes([0xE0|((l>>24)&0x07),(l>>16)&0xFF,(l>>8)&0xFF,l&0xFF]) + def _ww(s,w): s.s.sendall(s._el(len(w))+w.encode()) + def _ws(s,c,*ws): s._ww(c); [s._ww(w) for w in ws]; s.s.sendall(b'\x00') + def _rw(s): + f=s.s.recv(1) + if f==b'\x00': return None + lb=f[0]; l=lb + if lb&0x80: + if (lb&0xC0)==0x80: l=((lb&0x3F)<<8)+s.s.recv(1)[0] + elif (lb&0xE0)==0xC0: l=((lb&0x1F)<<16)+(s.s.recv(1)[0]<<8)+s.s.recv(1)[0] + else: l=((lb&0x0F)<<24)+(s.s.recv(1)[0]<<16)+(s.s.recv(1)[0]<<8)+s.s.recv(1)[0] + d=b'' + while len(d)=3 else '' + else: d[w]='' + rs.append(d) + elif r=='!trap': rs.append(('!trap',t)) + return rs + def cmd(s,c,**a): + ws=[c] + for k,v in a.items(): + ws.append(('?' if k.startswith('?') else '=')+k+'='+v) + s._ws(*ws) + return s._rr() + def close(s): + if s.s: s.s.close() + +api=R('remote.vpnmurahjogja.my.id',33206).c('mi4','m14') + +print('=== ping 8.8.8.8 (internet?) ===') +r = api.cmd('/ping', address='8.8.8.8', count='2', interval='0.3') +got = [x for x in r if isinstance(x,dict) and 'received' in x] +print(' loss=%s' % (got[-1].get('packet-loss','') if got else str(r)[:100])) + +print() +print('=== ping msr.s.dimanaaja.biz.id (DNS) ===') +r = api.cmd('/ping', address='msr.s.dimanaaja.biz.id', count='2', interval='0.3') +got = [x for x in r if isinstance(x,dict) and 'received' in x] +print(' loss=%s' % (got[-1].get('packet-loss','') if got else str(r)[:200])) + +print() +print('=== fetch https://msr.s.dimanaaja.biz.id/notify-expire.php?key=msr-2026-reset&name=TESTNOTIF ===') +r = api.cmd('/tool/fetch', **{'url': 'https://msr.s.dimanaaja.biz.id/notify-expire.php?key=msr-2026-reset&name=TESTNOTIF', 'http-method': 'get', 'keep-result': 'yes'}) +print(' -> %s' % str(r)[:500]) + +api.close() diff --git a/tes-hulu.py b/tes-hulu.py new file mode 100644 index 0000000..eab8924 --- /dev/null +++ b/tes-hulu.py @@ -0,0 +1,89 @@ +import socket, hashlib +class R: + def __init__(s,h,p=8728): s.h=h; s.p=p; s.s=None + def c(s,u,p): + s.s=socket.socket(); s.s.settimeout(30) + s.s.connect((s.h,s.p)) + s._ws('/login','=name='+u,'=password='+p) + r=s._rr() + if any(isinstance(x,tuple) for x in r): + for x in r: + if isinstance(x,dict) and 'ret' in x: ch=x['ret'] + if ch and len(ch)==32: + hh=hashlib.md5(b'\x00'+p.encode()+bytes.fromhex(ch)).hexdigest() + s._ws('/login','=name='+u,'=response=00'+hh); s._rr() + return s + def _el(s,l): + if l<0x80: return bytes([l]) + elif l<0x4000: l|=0x8000; return bytes([(l>>8)&0xFF,l&0xFF]) + else: return bytes([0xC0|((l>>16)&0x0F),(l>>8)&0xFF,l&0xFF]) if l<0x200000 else bytes([0xE0|((l>>24)&0x07),(l>>16)&0xFF,(l>>8)&0xFF,l&0xFF]) + def _ww(s,w): s.s.sendall(s._el(len(w))+w.encode()) + def _ws(s,c,*ws): s._ww(c); [s._ww(w) for w in ws]; s.s.sendall(b'\x00') + def _rw(s): + f=s.s.recv(1) + if f==b'\x00': return None + lb=f[0]; l=lb + if lb&0x80: + if (lb&0xC0)==0x80: l=((lb&0x3F)<<8)+s.s.recv(1)[0] + elif (lb&0xE0)==0xC0: l=((lb&0x1F)<<16)+(s.s.recv(1)[0]<<8)+s.s.recv(1)[0] + else: l=((lb&0x0F)<<24)+(s.s.recv(1)[0]<<16)+(s.s.recv(1)[0]<<8)+s.s.recv(1)[0] + d=b'' + while len(d)=3 else '' + else: d[w]='' + rs.append(d) + elif r=='!trap': rs.append(('!trap',t)) + return rs + def cmd(s,c,**a): + ws=[c] + for k,v in a.items(): + ws.append(('?' if k.startswith('?') else '=')+k+'='+v) + s._ws(*ws) + return s._rr() + def close(s): + if s.s: s.s.close() + +api=R('remote.vpnmurahjogja.my.id',33206).c('mi4','m14') + +# Add temporary route to Docker host subnets via 192.168.100.1 (the upstream gateway) +for net, gw in [('192.168.35.133/32','192.168.100.1'), ('192.168.9.253/32','192.168.100.1'), ('100.100.31.46/32','192.168.100.1')]: + print(f'--- route {net} via {gw} ---') + r = api.cmd('/ip/route/add', **{'dst-address': net, 'gateway': gw, 'comment': 'tmp-test'}) + print(' add:', r if r else 'ok') + +import time +time.sleep(2) + +# ping test +for ip in ['192.168.35.133','192.168.9.253','100.100.31.46']: + r = api.cmd('/ping', address=ip, count='2') + ok = any(isinstance(x,dict) and x.get('packet-loss')=='0%' for x in r) + print(f'ping {ip}: loss = {[x.get("packet-loss") for x in r if isinstance(x,dict)]}') + +# Remove tmp routes +r = api.cmd('/ip/route/print', **{'?comment': 'tmp-test'}) +for rw in r: + if isinstance(rw, dict): + api.cmd('/ip/route/remove', **{'=.id': rw.get('.id')}) + +api.close() diff --git a/tes-routing.py b/tes-routing.py new file mode 100644 index 0000000..3ffe1fa --- /dev/null +++ b/tes-routing.py @@ -0,0 +1,84 @@ +import socket, hashlib +class R: + def __init__(s,h,p=8728): s.h=h; s.p=p; s.s=None + def c(s,u,p): + s.s=socket.socket(); s.s.settimeout(25) + s.s.connect((s.h,s.p)) + s._ws('/login','=name='+u,'=password='+p) + r=s._rr() + if any(isinstance(x,tuple) for x in r): + for x in r: + if isinstance(x,dict) and 'ret' in x: ch=x['ret'] + if ch and len(ch)==32: + hh=hashlib.md5(b'\x00'+p.encode()+bytes.fromhex(ch)).hexdigest() + s._ws('/login','=name='+u,'=response=00'+hh); s._rr() + return s + def _el(s,l): + if l<0x80: return bytes([l]) + elif l<0x4000: l|=0x8000; return bytes([(l>>8)&0xFF,l&0xFF]) + else: return bytes([0xC0|((l>>16)&0x0F),(l>>8)&0xFF,l&0xFF]) if l<0x200000 else bytes([0xE0|((l>>24)&0x07),(l>>16)&0xFF,(l>>8)&0xFF,l&0xFF]) + def _ww(s,w): s.s.sendall(s._el(len(w))+w.encode()) + def _ws(s,c,*ws): s._ww(c); [s._ww(w) for w in ws]; s.s.sendall(b'\x00') + def _rw(s): + f=s.s.recv(1) + if f==b'\x00': return None + lb=f[0]; l=lb + if lb&0x80: + if (lb&0xC0)==0x80: l=((lb&0x3F)<<8)+s.s.recv(1)[0] + elif (lb&0xE0)==0xC0: l=((lb&0x1F)<<16)+(s.s.recv(1)[0]<<8)+s.s.recv(1)[0] + else: l=((lb&0x0F)<<24)+(s.s.recv(1)[0]<<16)+(s.s.recv(1)[0]<<8)+s.s.recv(1)[0] + d=b'' + while len(d)=3 else '' + else: d[w]='' + rs.append(d) + elif r=='!trap': rs.append(('!trap',t)) + return rs + def cmd(s,c,**a): + ws=[c] + for k,v in a.items(): + ws.append(('?' if k.startswith('?') else '=')+k+'='+v) + s._ws(*ws) + return s._rr() + def close(s): + if s.s: s.s.close() + +api=R('remote.vpnmurahjogja.my.id',33206).c('mi4','m14') + +for ip in ['192.168.9.253', '192.168.35.133', '100.100.31.46', '192.168.100.6']: + r = api.cmd('/ping', address=ip, count='2', interval='0.3') + got = [x for x in r if isinstance(x,dict) and 'received' in x] + if got: + last = got[-1] + loss = last.get('packet-loss','') + print('ping %-18s -> loss=%s' % (ip, loss)) + else: + print('ping %-18s -> %s' % (ip, str(r)[:120])) + +# Test fetch to host LAN IP on port 80 (traefik) +for url in ['http://192.168.9.253/notify-expire.php?key=msr-2026-reset&name=TESTNOTIF']: + r = api.cmd('/tool/fetch', **{'url': url, 'http-method': 'get', 'keep-result': 'yes'}) + print('fetch %s' % url) + print(' -> %s' % str(r)[:400]) + +api.close() diff --git a/tes-tg.py b/tes-tg.py new file mode 100644 index 0000000..26b6b23 --- /dev/null +++ b/tes-tg.py @@ -0,0 +1,80 @@ +import socket, hashlib +class R: + def __init__(s,h,p=8728): s.h=h; s.p=p; s.s=None + def c(s,u,p): + s.s=socket.socket(); s.s.settimeout(30) + s.s.connect((s.h,s.p)) + s._ws('/login','=name='+u,'=password='+p) + r=s._rr() + if any(isinstance(x,tuple) for x in r): + for x in r: + if isinstance(x,dict) and 'ret' in x: ch=x['ret'] + if ch and len(ch)==32: + hh=hashlib.md5(b'\x00'+p.encode()+bytes.fromhex(ch)).hexdigest() + s._ws('/login','=name='+u,'=response=00'+hh); s._rr() + return s + def _el(s,l): + if l<0x80: return bytes([l]) + elif l<0x4000: l|=0x8000; return bytes([(l>>8)&0xFF,l&0xFF]) + else: return bytes([0xC0|((l>>16)&0x0F),(l>>8)&0xFF,l&0xFF]) if l<0x200000 else bytes([0xE0|((l>>24)&0x07),(l>>16)&0xFF,(l>>8)&0xFF,l&0xFF]) + def _ww(s,w): s.s.sendall(s._el(len(w))+w.encode()) + def _ws(s,c,*ws): s._ww(c); [s._ww(w) for w in ws]; s.s.sendall(b'\x00') + def _rw(s): + f=s.s.recv(1) + if f==b'\x00': return None + lb=f[0]; l=lb + if lb&0x80: + if (lb&0xC0)==0x80: l=((lb&0x3F)<<8)+s.s.recv(1)[0] + elif (lb&0xE0)==0xC0: l=((lb&0x1F)<<16)+(s.s.recv(1)[0]<<8)+s.s.recv(1)[0] + else: l=((lb&0x0F)<<24)+(s.s.recv(1)[0]<<16)+(s.s.recv(1)[0]<<8)+s.s.recv(1)[0] + d=b'' + while len(d)=3 else '' + else: d[w]='' + rs.append(d) + elif r=='!trap': rs.append(('!trap',t)) + return rs + def cmd(s,c,**a): + ws=[c] + for k,v in a.items(): + ws.append(('?' if k.startswith('?') else '=')+k+'='+v) + s._ws(*ws) + return s._rr() + def close(s): + if s.s: s.s.close() + +api=R('remote.vpnmurahjogja.my.id',33206).c('mi4','m14') + +# Test 1: router resolve + fetch api.telegram.org +print('=== DNS resolve api.telegram.org ===') +r = api.cmd('/tool/fetch', url='https://api.telegram.org/bot5115036608:AAGeyXVq5eNvdAkaCQHU9NfH16uAAiSKiuk/getMe', mode='https', **{'http-method': 'get', 'keep-result': 'no'}) +print(' ', r) +if any(isinstance(x,tuple) and x[0]=='!trap' for x in r): + print(' -> https fetch gagal') + +print() +print('=== fetch ke http (no ssl) di api.telegram.org ===') +r = api.cmd('/tool/fetch', url='http://api.telegram.org/bot5115036608:AAGeyXVq5eNvdAkaCQHU9NfH16uAAiSKiuk/getMe', mode='http', **{'http-method': 'get', 'keep-result': 'no'}) +print(' ', r) + +api.close() diff --git a/test-1min.py b/test-1min.py new file mode 100644 index 0000000..ff9b8e7 --- /dev/null +++ b/test-1min.py @@ -0,0 +1,142 @@ +import socket, hashlib, time +from datetime import datetime, timedelta + +class R: + def __init__(self,h,p=8728): self.h=h; self.p=p; self.s=None + def c(self,u,p): + self.s=socket.socket(socket.AF_INET,socket.SOCK_STREAM); self.s.settimeout(10) + self.s.connect((self.h,self.p)) + self._ws('/login','=name='+u,'=password='+p) + r=self._rr() + if any(isinstance(x,tuple) and x[0]=='!trap' for x in r): + for x in r: + if isinstance(x,dict) and 'ret' in x: ch=x['ret'] + if ch and len(ch)==32: + h=hashlib.md5(b'\x00'+p.encode()+bytes.fromhex(ch)).hexdigest() + self._ws('/login','=name='+u,'=response=00'+h) + self._rr() + return self + def _el(self,l): + if l<0x80: return bytes([l]) + elif l<0x4000: l|=0x8000; return bytes([(l>>8)&0xFF,l&0xFF]) + elif l<0x200000: l|=0xC00000; return bytes([(l>>16)&0xFF,(l>>8)&0xFF,l&0xFF]) + elif l<0x10000000: l|=0xE0000000; return bytes([(l>>24)&0xFF,(l>>16)&0xFF,(l>>8)&0xFF,l&0xFF]) + else: return bytes([0xF0,(l>>24)&0xFF,(l>>16)&0xFF,(l>>8)&0xFF,l&0xFF]) + def _ww(self,w): self.s.sendall(self._el(len(w))+w.encode('utf-8')) + def _ws(self,c,*ws): self._ww(c); [self._ww(w) for w in ws]; self.s.sendall(b'\x00') + def _rb(self): return self.s.recv(1) + def _rw(self): + f=self._rb() + if f==b'\x00': return None + lb=f[0] + if lb&0x80: + if (lb&0xC0)==0x80: l=((lb&0x3F)<<8)+self._rb()[0] + elif (lb&0xE0)==0xC0: l=((lb&0x1F)<<16)+(self._rb()[0]<<8)+self._rb()[0] + elif (lb&0xF0)==0xE0: l=((lb&0x0F)<<24)+(self._rb()[0]<<16)+(self._rb()[0]<<8)+self._rb()[0] + else: l=(self._rb()[0]<<24)+(self._rb()[0]<<16)+(self._rb()[0]<<8)+self._rb()[0] + else: l=lb + d=b'' + while len(d)=3 else '' + else: d[w]='' + rs.append(d) + elif r=='!trap': rs.append(('!trap',t)) + return rs + def cmd(self,c,**a): + ws=[c] + for k,v in a.items(): + if k.startswith('?'): ws.append(f'{k}={v}') + else: ws.append(f'={k}={v}') + self._ws(*ws) + return self._rr() + def close(self): + if self.s: self.s.close(); self.s=None + +api=R('192.168.100.2').c('mi4','m14') + +# Get router time +r = api.cmd('/system/clock/print')[0] +print(f"Router time: {r['date']} {r['time']}") + +# Parse router time +months = {'jan':'jan','feb':'feb','mar':'mar','apr':'apr','may':'may','jun':'jun', + 'jul':'jul','aug':'aug','sep':'sep','oct':'oct','nov':'nov','dec':'dec'} +parts = r['date'].split('/') +router_date = f"{parts[0]}/{parts[1]}/{parts[2]}" +router_time = r['time'] + +# Create comment: 2 minutes in the past +past_comment = f"{router_date} 00:00:00" +print(f"Setting user comment (past): {past_comment}") + +# Find next scheduler run +for item in api.cmd('/system/scheduler/print'): + name = item['name'] + if 'Bulanan2' in name: + next_run = item.get('next-run', '?') + run_before = int(item.get('run-count', '0')) + print(f"Bulanan2: next-run={next_run} run-count={run_before}") + +# Create test user +test_name = "tes1menit" +r = api.cmd('/ip/hotspot/user/add', **{ + 'name': test_name, + 'password': 'x', + 'profile': 'Bulanan2', + 'comment': past_comment +}) +print(f"Created user: {test_name}") + +# Wait for scheduler +wait_sec = 180 +print(f"\nWaiting {wait_sec}s for scheduler to run...") +for i in range(wait_sec): + time.sleep(1) + if i > 0 and i % 30 == 0: + r = api.cmd('/ip/hotspot/user/print', **{'?name': test_name}) + if r: + print(f" [{i}s] User exists. limit-uptime={r[0].get('limit-uptime','')}") + else: + print(f" [{i}s] User REMOVED!") + break + +# Final check +print() +r = api.cmd('/ip/hotspot/user/print', **{'?name': test_name}) +if r: + u = r[0] + print(f"User still exists. limit-uptime={u.get('limit-uptime')} comment={u.get('comment')}") + if u.get('limit-uptime') == '1s': + print("=> EXPIRED (limit-uptime=1s) โœ…") + else: + print("=> NOT EXPIRED yet") + # Cleanup + api.cmd('/ip/hotspot/user/remove', **{'.id': u['.id']}) + print("Cleaned up.") +else: + print("User was REMOVED by scheduler. EXPIRY WORKING โœ…") + +api.close() diff --git a/test-aktif-1menit.py b/test-aktif-1menit.py new file mode 100644 index 0000000..2334bae --- /dev/null +++ b/test-aktif-1menit.py @@ -0,0 +1,146 @@ +import socket, hashlib, time + +s = socket.socket(socket.AF_INET, socket.SOCK_STREAM) +s.settimeout(10) +s.connect(('192.168.100.2', 8728)) + +def el(l): + if l<0x80: return bytes([l]) + elif l<0x4000: l|=0x8000; return bytes([(l>>8)&0xFF,l&0xFF]) + elif l<0x200000: l|=0xC00000; return bytes([(l>>16)&0xFF,(l>>8)&0xFF,l&0xFF]) + elif l<0x10000000: l|=0xE0000000; return bytes([(l>>24)&0xFF,(l>>16)&0xFF,(l>>8)&0xFF,l&0xFF]) + else: return bytes([0xF0,(l>>24)&0xFF,(l>>16)&0xFF,(l>>8)&0xFF,l&0xFF]) +def ww(w): s.sendall(el(len(w))+(w.encode() if isinstance(w,str) else w)) +def ws(c,*a): ww(c); [ww(x) for x in a]; s.sendall(b'\x00') +def rb(): return s.recv(1) +def rw(): + f=rb() + if f==b'\x00': return None + lb=f[0] + if lb&0x80: + if (lb&0xC0)==0x80: l=((lb&0x3F)<<8)+rb()[0] + elif (lb&0xE0)==0xC0: l=((lb&0x1F)<<16)+(rb()[0]<<8)+rb()[0] + elif (lb&0xF0)==0xE0: l=((lb&0x0F)<<24)+(rb()[0]<<16)+(rb()[0]<<8)+rb()[0] + else: l=(rb()[0]<<24)+(rb()[0]<<16)+(rb()[0]<<8)+rb()[0] + else: l=lb + d=b'' + while len(d)=3 else '' + else: d[w]='' + rs.append(d) + elif r=='!trap': rs.append(('!trap',t)) + return rs +def cmd(c,**a): + w=[c] + for k,v in a.items(): + if k.startswith('?'): w.append(f'{k}={v}') + else: w.append(f'={k}={v}') + ws(*w) + return rr() + +ws('/login','=name=mi4','=password=m14') +r=rr() +if any(isinstance(x,tuple) and x[0]=='!trap' for x in r): + for x in r: + if isinstance(x,dict) and 'ret' in x: ch=x['ret'] + if ch and len(ch)==32: + resp='00'+hashlib.md5(b'\x00'+b'm14'+bytes.fromhex(ch)).hexdigest() + ws('/login','=name=mi4',f'=response={resp}') + rr() + +# Get router time +rt=cmd('/system/clock/print')[0] +date_str=rt['date'] +time_str=rt['time'] +print(f"Router time: {date_str} {time_str}") + +# Parse time, add 1 minute +h=int(time_str[0:2]); m=int(time_str[3:5]); s=int(time_str[6:8]) +m+=1 +if m>=60: m=0; h+=1 +if h>=24: h=0 +exp_time=f"{h:02d}:{m:02d}:00" +exp_comment=f"{date_str} {exp_time}" +print(f"Expiry time (1 menit): {exp_comment}") + +# Remove old labibah_tes if exists +for u in cmd('/ip/hotspot/user/print',**{'?name':'labibah_tes'}): + cmd('/ip/hotspot/user/remove',**{'.id':u['.id']}) + print("Removed old labibah_tes") + +# Create new user - active for 1 minute +cmd('/ip/hotspot/user/add',**{ + 'name':'labibah_tes', + 'password':'labibah_tes', + 'profile':'Bulanan2', + 'mac-address':'CA:1A:02:C2:8C:1E', + 'comment':exp_comment, + 'limit-uptime':'' +}) +print(f"\nCreated user: labibah_tes / labibah_tes") +print(f" Expiry comment: {exp_comment}") +print(f" Status: AKTIF (belum expired - bisa login sekarang)") +print(f" Prediksi: setelah jam {exp_time}, scheduler akan expire user ini") +print(f" (Bulanan2 next-run sekitar ~2-3 menit sekali)\n") + +# Find Bulanan2 next run +for sitem in cmd('/system/scheduler/print'): + if sitem['name']=='Bulanan2': + print(f"Bulanan2 next-run: {sitem.get('next-run')}") + +# Wait and monitor +print(f"\nMemantau setiap 10 detik selama 180 detik...") +for i in range(180): + time.sleep(1) + if i%10==0: + for u in cmd('/ip/hotspot/user/print',**{'?name':'labibah_tes'}): + lu=u.get('limit-uptime','(none)') + co=u.get('comment','') + active_found=False + for a in cmd('/ip/hotspot/active/print',**{'?user':'labibah_tes'}): + active_found=True + status="TERSAMBUNG" if active_found else "offline" + print(f" [{i:3d}s] uptime={lu} status={status}") + break + else: + print(f" [{i:3d}s] User REMOVED!") + break + +# Final +print("\n=== HASIL AKHIR ===") +for u in cmd('/ip/hotspot/user/print',**{'?name':'labibah_tes'}): + print(f" User: {u['name']}") + print(f" Comment: {u.get('comment')}") + print(f" limit-uptime: {u.get('limit-uptime','(none)')}") + if u.get('limit-uptime')=='1s': print(" => โœ… EXPIRED!") + elif u.get('limit-uptime')=='(none)': print(" => Masih AKTIF") + cmd('/ip/hotspot/user/remove',**{'.id':u['.id']}) + print(" Cleaned up") + break +else: + print(" User sudah dihapus oleh scheduler โœ…") + +s.close() diff --git a/test-api-set.py b/test-api-set.py new file mode 100644 index 0000000..7c8fd04 --- /dev/null +++ b/test-api-set.py @@ -0,0 +1,37 @@ +import librouteros + +api = librouteros.connect('192.168.100.2', 'mi4', 'm14', port=8728) + +# Test 1: Set comment on Bulanan2 +items = list(api('/system/scheduler/print')) +for item in items: + if item['name'] == 'Bulanan2': + print(f'Bulanan2 .id = {item[".id"]}') + print(f'Current comment: {item.get("comment")}') + + # Set comment + result = list(api('/system/scheduler/set', **{'.id': item['.id'], 'comment': 'TEST_FIX_123'})) + print(f'Set result: {result}') + break + +# Re-read +items = list(api('/system/scheduler/print')) +for item in items: + if item['name'] == 'Bulanan2': + print(f'Comment after set: {item.get("comment")}') + +# Test 2: Set on-event with simple value +simple_script = ':put "hello world"' +items = list(api('/system/scheduler/print', **{'?name': 'Bulanan2'})) +if items: + item = items[0] + print(f'Setting on-event to simple script...') + result = list(api('/system/scheduler/set', **{'.id': item['.id'], 'on-event': simple_script, 'comment': 'Monitor Profile Bulanan2'})) + print(f'Result: {result}') + + # Re-read + items = list(api('/system/scheduler/print', **{'?name': 'Bulanan2'})) + if items: + print(f'On-event after: {items[0].get("on-event")[:100]}') + +api.close() diff --git a/test-e2e.py b/test-e2e.py new file mode 100644 index 0000000..2ccb0e6 --- /dev/null +++ b/test-e2e.py @@ -0,0 +1,128 @@ +import socket, hashlib, 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}') + 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}") + 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 current time and scheduler next-run +clock = api.cmd('/system/clock/print')[0] +print(f"Time: {clock['date']} {clock['time']}") + +# Find Bulanan2's next-run +for item in api.cmd('/system/scheduler/print'): + if item['name'] == 'Bulanan2': + next_run = item.get('next-run', '?') + print(f"Bulanan2 next-run: {next_run}") + run_before = int(item.get('run-count', '0')) + +# Create test user +test_name = "e2e_test" +api.cmd('/ip/hotspot/user/add', **{'name': test_name, 'profile': 'Bulanan2', 'password': 'x', 'comment': 'jul/06/2026 00:00:00'}) +print(f"\nCreated {test_name}") + +# Wait for scheduler to run +wait_sec = 150 # 2.5 min +print(f"Waiting {wait_sec}s for scheduler...") +for i in range(wait_sec): + time.sleep(1) + if i % 30 == 0: + print(f" waited {i}s...") + +# Check result +users = list(api.cmd('/ip/hotspot/user/print', **{'?name': test_name})) +for item in api.cmd('/system/scheduler/print'): + if item['name'] == 'Bulanan2': + run_after = int(item.get('run-count', '0')) + print(f"Bulanan2 runs: {run_before} -> {run_after} (delta={run_after-run_before})") + +if not users: + print(f"\nResult: {test_name} was REMOVED - EXPIRY SYSTEM IS WORKING!") +else: + user = users[0] + lu = user.get('limit-uptime', '?') + print(f"\nResult: {test_name} still exists (limit-uptime={lu})") + if lu == '1s': + print("limit-uptime=1s - partial: expired but not removed (depends on profile mode)") + # Cleanup + api.cmd('/ip/hotspot/user/remove', **{'.id': user['.id']}) + +api.close() diff --git a/test-escape.py b/test-escape.py new file mode 100644 index 0000000..49b3c9d --- /dev/null +++ b/test-escape.py @@ -0,0 +1,37 @@ +import paramiko, time + +client = paramiko.SSHClient() +client.set_missing_host_key_policy(paramiko.AutoAddPolicy()) +client.connect('192.168.100.2', username='mi4', password='m14', look_for_keys=False, allow_agent=False) + +# Simple test +simple_code = ':put "hello world" ; :local x 5 ; :put $x' +escaped = simple_code.replace('"', '\\"').replace('$', '\\$').replace('\\', '\\\\') +print(f"Original: {simple_code}") +print(f"Escaped: {escaped}") + +chan = client.invoke_shell() +chan.settimeout(5) +time.sleep(1.5) + +out = b'' +while chan.recv_ready(): out += chan.recv(65535) + +# Create test script +chan.send(f'/system script add name=test_esc source="{escaped}"\n') +time.sleep(2) +out = b'' +while chan.recv_ready(): out += chan.recv(65535) +print(f"\nCreate result: {out.decode(errors='replace')[:200]}") + +# Read it back +stdin, stdout, stderr = client.exec_command(':put [/system script get test_esc source]') +time.sleep(0.5) +result = stdout.read().decode().strip() +print(f"\nStored source: {result}") + +# Cleanup +chan.send('/system script remove test_esc\n') +time.sleep(1) +chan.close() +client.close() diff --git a/test-expiry.py b/test-expiry.py new file mode 100644 index 0000000..615dc6a --- /dev/null +++ b/test-expiry.py @@ -0,0 +1,141 @@ +import socket, hashlib, 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') + +# 1. Create test user with past expiry +test_name = "test_expiry_verify" +print(f"Creating test user '{test_name}' with past expiry...") +api.cmd('/ip/hotspot/user/add', **{ + 'name': test_name, + 'profile': 'Bulanan2', + 'password': 'test123', + 'comment': 'jul/06/2026 00:00:00', # yesterday + 'limit-uptime': '1h', +}) + +# Verify +users = list(api.cmd('/ip/hotspot/user/print', **{'?name': test_name})) +if users: + print(f" Created: {users[0].get('name')} comment={users[0].get('comment')}") + +# 2. Get scheduler run count before +sched_before = {} +for item in api.cmd('/system/scheduler/print'): + sched_before[item['name']] = item.get('run-count', '0') +print(f"\n DevB2 run count before: {sched_before.get('DevB2', '?')}") + +# 3. Wait for scheduler to run (it runs every ~2.5 min) +print("\n Waiting 10 seconds for scheduler to execute...") +time.sleep(10) + +# 4. Check if user still exists +users_after = list(api.cmd('/ip/hotspot/user/print', **{'?name': test_name})) +sched_after = {} +for item in api.cmd('/system/scheduler/print'): + sched_after[item['name']] = item.get('run-count', '0') +print(f" DevB2 run count after: {sched_after.get('DevB2', '?')}") + +if users_after: + user = users_after[0] + print(f"\n User STILL EXISTS: {user.get('name')}") + print(f" Comment: {user.get('comment')}") + print(f" limit-uptime: {user.get('limit-uptime')}") + + # Check if limit-uptime was changed to 1s + if user.get('limit-uptime') == '1s': + print(" EXPIRED: limit-uptime=1s - expiry system is WORKING!") + else: + print(f" NOT YET EXPIRED: limit-uptime={user.get('limit-uptime')}") + print(" (may need to wait for next scheduler cycle)") +else: + print(f"\n User was REMOVED! Expiry system is WORKING!") + +# Cleanup +if users_after: + api.cmd('/ip/hotspot/user/remove', **{'.id': users_after[0]['.id']}) + print(f"\n Cleaned up test user") + +api.close() diff --git a/test-fix-local.py b/test-fix-local.py new file mode 100644 index 0000000..f9143cc --- /dev/null +++ b/test-fix-local.py @@ -0,0 +1,49 @@ +# Test the fix_script logic locally +def fix_script(s): + s = s.replace(':pickk', ':pick') + s = s.replace(':pick', '\x00PICKGUARD\x00') + s = s.replace(':pic', ':pick') + s = s.replace('\x00PICKGUARD\x00', ':pick') + return s + +# Test cases +test1 = ':local days [ :pic $d 4 6 ];' # original typo +test2 = ':local days [ :pickk $d 4 6 ];' # current broken state +test3 = ':local days [ :pick $d 4 6 ];' # already correct +test4 = ':local gettime [:pick $comment 12 20];' # correct $comment +test5 = ':local gettime [:pic $comment 12 20];' # original typo $comment +test6 = test1 + ' ' + test4 # mixed + +for name, t in [('test1 orig typo', test1), ('test2 double-k', test2), + ('test3 correct', test3), ('test4 comment ok', test4), + ('test5 comment typo', test5), ('test6 mixed', test6)]: + result = fix_script(t) + pic = result.count(':pic') + pick = result.count(':pick') + pickk = result.count(':pickk') + print(f"{name}: pic={pic} pick={pick} pickk={pickk}") + if result != 'ERROR': + if pic == 0 and pick > 0: + print(f" OK: {result}") + else: + print(f" FAIL: {result}") + +# Test with actual long script (simulated) +long_script = ';'.join([ + ':local dateint do={:local montharray ( "jan","feb","mar","apr","may","jun","jul","aug","sep","oct","nov","dec" );:local days [ :pickk $d 4 6 ];:local month [ :pickk $d 0 3 ];:local year [ :pickk $d 7 11 ];:local monthint ([ :find $montharray $month]);:local month ($monthint + 1);:if ( [len $month] = 1) do={:local zero ("0");:return [:tonum ("$year$zero$month$days")];} else={:return [:tonum ("$year$month$days")];}}', + ':local timeint do={ :local hours [ :pickk $t 0 2 ]; :local minutes [ :pickk $t 3 5 ]; :return ($hours * 60 + $minutes) ; }', + ':local date [ /system clock get date ]', + ':local time [ /system clock get time ]', + ':local today [$dateint d=$date]', + ':local curtime [$timeint t=$time]', + ':foreach i in [ /ip hotspot user find where profile="test" ] do={ :local comment [ /ip hotspot user get $i comment]; :local name [ /ip hotspot user get $i name]; :local gettime [:pick $comment 12 20]; :if ([:pick $comment 3] = "/" and [:pick $comment 6] = "/") do={:local expd [$dateint d=$comment] ; :local expt [$timeint t=$gettime] ; :if (($expd < $today and $expt < $curtime)) do={ /ip hotspot user set limit-uptime=1s $i } } }' +]) + +print(f"\nLong script test:") +print(f" Before: pic={long_script.count(':pic')} pick={long_script.count(':pick')} pickk={long_script.count(':pickk')}") +result = fix_script(long_script) +print(f" After: pic={result.count(':pic')} pick={result.count(':pick')} pickk={result.count(':pickk')}") +if result.count(':pic') == 0: + print(" PASS: No :pic remaining") +else: + print(" FAIL: :pic still present") diff --git a/test-notif.py b/test-notif.py new file mode 100644 index 0000000..221b15f --- /dev/null +++ b/test-notif.py @@ -0,0 +1,83 @@ +import socket, hashlib +class R: + def __init__(s,h,p=8728): s.h=h; s.p=p; s.s=None + def c(s,u,p): + s.s=socket.socket(); s.s.settimeout(20) + s.s.connect((s.h,s.p)) + s._ws('/login','=name='+u,'=password='+p) + r=s._rr() + if any(isinstance(x,tuple) for x in r): + for x in r: + if isinstance(x,dict) and 'ret' in x: ch=x['ret'] + if ch and len(ch)==32: + hh=hashlib.md5(b'\x00'+p.encode()+bytes.fromhex(ch)).hexdigest() + s._ws('/login','=name='+u,'=response=00'+hh); s._rr() + return s + def _el(s,l): + if l<0x80: return bytes([l]) + elif l<0x4000: l|=0x8000; return bytes([(l>>8)&0xFF,l&0xFF]) + else: return bytes([0xC0|((l>>16)&0x0F),(l>>8)&0xFF,l&0xFF]) if l<0x200000 else bytes([0xE0|((l>>24)&0x07),(l>>16)&0xFF,(l>>8)&0xFF,l&0xFF]) + def _ww(s,w): s.s.sendall(s._el(len(w))+w.encode()) + def _ws(s,c,*ws): s._ww(c); [s._ww(w) for w in ws]; s.s.sendall(b'\x00') + def _rw(s): + f=s.s.recv(1) + if f==b'\x00': return None + lb=f[0]; l=lb + if lb&0x80: + if (lb&0xC0)==0x80: l=((lb&0x3F)<<8)+s.s.recv(1)[0] + elif (lb&0xE0)==0xC0: l=((lb&0x1F)<<16)+(s.s.recv(1)[0]<<8)+s.s.recv(1)[0] + else: l=((lb&0x0F)<<24)+(s.s.recv(1)[0]<<16)+(s.s.recv(1)[0]<<8)+s.s.recv(1)[0] + d=b'' + while len(d)=3 else '' + else: d[w]='' + rs.append(d) + elif r=='!trap': rs.append(('!trap',t)) + return rs + def cmd(s,c,**a): + ws=[c] + for k,v in a.items(): + ws.append(('?' if k.startswith('?') else '=')+k+'='+v) + s._ws(*ws) + return s._rr() + def close(s): + if s.s: s.s.close() + +api=R('remote.vpnmurahjogja.my.id',33206).c('mi4','m14') + +print('=== TEST 1: /tool fetch from router to notify-expire.php ===') +# Use a real test user? Use name=TESTNOTIF +r = api.cmd('/tool/fetch', **{'url': 'http://192.168.100.6/notify-expire.php?key=msr-2026-reset&name=TESTNOTIF', 'http-method': 'get', 'keep-result': 'yes'}) +print(' Result: %s' % str(r)) +print() + +print('=== TEST 2: Can router reach 192.168.100.6? (ping) ===') +r2 = api.cmd('/ping', address='192.168.100.6', count='2') +print(' Result: %s' % str(r2)) +print() + +print('=== TEST 3: Container reachability (ping container gateway) ===') +r3 = api.cmd('/ping', address='192.168.100.6', count='2', interval='0.5') +print(' Result: %s' % str(r3)) + +api.close() diff --git a/test-replace.py b/test-replace.py new file mode 100644 index 0000000..85083a2 --- /dev/null +++ b/test-replace.py @@ -0,0 +1,22 @@ +s = ':put "hello" ; :local x 5 ; :put $x' +print('orig:', repr(s)) +print('orig raw:', s) + +# The original escape logic +escaped = s.replace('\\', '\\\\').replace('"', '\\"').replace('$', '\\$') +print('escaped repr:', repr(escaped)) +print('escaped raw:', escaped) + +# What RouterOS expects: +# In RouterOS CLI: /system script add source=":put \"hello\" ; :local x 5 ; :put \$x" +# The CLI parser sees: \", \$, and stores: :put "hello" ; :local x 5 ; :put $x + +# Test: what does the CLI actually store? +# Note: the escaped value needs to be: +# - \" for literal double quote +# - \$ for literal dollar sign +# - \\ for literal backslash + +correct = s.replace('"', '\\"').replace('$', '\\$') +print('\ncorrect repr:', repr(correct)) +print('correct raw:', correct) diff --git a/test-reset-btn.py b/test-reset-btn.py new file mode 100644 index 0000000..8df2356 --- /dev/null +++ b/test-reset-btn.py @@ -0,0 +1,88 @@ +import socket, hashlib +class R: + def __init__(s,h,p=8728): s.h=h; s.p=p; s.s=None + def c(s,u,p): + s.s=socket.socket(); s.s.settimeout(60) + s.s.connect((s.h,s.p)) + s._ws('/login','=name='+u,'=password='+p) + r=s._rr() + if any(isinstance(x,tuple) for x in r): + for x in r: + if isinstance(x,dict) and 'ret' in x: ch=x['ret'] + if ch and len(ch)==32: + hh=hashlib.md5(b'\x00'+p.encode()+bytes.fromhex(ch)).hexdigest() + s._ws('/login','=name='+u,'=response=00'+hh); s._rr() + return s + def _el(s,l): + if l<0x80: return bytes([l]) + elif l<0x4000: l|=0x8000; return bytes([(l>>8)&0xFF,l&0xFF]) + else: return bytes([0xC0|((l>>16)&0x0F),(l>>8)&0xFF,l&0xFF]) if l<0x200000 else bytes([0xE0|((l>>24)&0x07),(l>>16)&0xFF,(l>>8)&0xFF,l&0xFF]) + def _ww(s,w): s.s.sendall(s._el(len(w))+w.encode()) + def _ws(s,c,*ws): s._ww(c); [s._ww(w) for w in ws]; s.s.sendall(b'\x00') + def _rw(s): + f=s.s.recv(1) + if f==b'\x00': return None + lb=f[0]; l=lb + if lb&0x80: + if (lb&0xC0)==0x80: l=((lb&0x3F)<<8)+s.s.recv(1)[0] + elif (lb&0xE0)==0xC0: l=((lb&0x1F)<<16)+(s.s.recv(1)[0]<<8)+s.s.recv(1)[0] + else: l=((lb&0x0F)<<24)+(s.s.recv(1)[0]<<16)+(s.s.recv(1)[0]<<8)+s.s.recv(1)[0] + d=b'' + while len(d)=3 else '' + else: d[w]='' + rs.append(d) + elif r=='!trap': rs.append(('!trap',t)) + return rs + def cmd(s,c,**a): + ws=[c] + for k,v in a.items(): + ws.append(('?' if k.startswith('?') else '=')+k+'='+v) + s._ws(*ws) + return s._rr() + def close(s): + if s.s: s.s.close() + +api=R('remote.vpnmurahjogja.my.id',33206).c('mi4','m14') + +name = 'TEST-RESET' +MARKUP_PREFIX = '%7B%22inline_keyboard%22%3A%5B%5B%7B%22text%22%3A%22%F0%9F%94%84%20Reset%22%2C%22callback_data%22%3A%22reset_' +MARKUP_SUFFIX = '%22%7D%5D%5D%7D' + +# Simulasikan URL persis seperti on-event menghasilkan +markup = MARKUP_PREFIX + name + MARKUP_SUFFIX +url = ('https://api.telegram.org/bot5115036608:AAGeyXVq5eNvdAkaCQHU9NfH16uAAiSKiuk/' + 'sendMessage?chat_id=-1002554941429&message_thread_id=42&text=' + name + '+expired' + '&reply_markup=' + markup) + +print('URL len:', len(url)) +print('fetching...') +res = api.cmd('/tool/fetch', **{'url': url, 'mode': 'https', 'http-method': 'get', 'keep-result': 'yes'}) +print('fetch:', res) +import time +time.sleep(3) +files = api.cmd('/file/print', **{'?name': 'TEST*'}) +for f in files: + if isinstance(f,dict): + print('FILE:', f.get('name'), f.get('size')) +api.close() diff --git a/test-short.py b/test-short.py new file mode 100644 index 0000000..3a753c7 --- /dev/null +++ b/test-short.py @@ -0,0 +1,120 @@ +import socket, time, 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(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 + +# ===== TEST ===== +api = RouterOSApi('192.168.100.2') +api.connect('mi4', 'm14') + +# Get Bulanan2 +resp = api.cmd('/system/scheduler/print', **{'?name': 'Bulanan2'}) +item = resp[0] +oid = item['.id'] +current_oe = item.get('on-event', '') +print(f"Current on-event length: {len(current_oe)}") +print(f"Current .id: {oid}") + +# Test: set to very short script +short_script = ':put "test123"' +print(f"\nSetting on-event to: '{short_script}' (len={len(short_script)})") +set_resp = api.cmd('/system/scheduler/set', **{'.id': oid, 'on-event': short_script}) +for r in set_resp: + print(f" Response: {r}") + +# Verify via fresh API call +resp2 = api.cmd('/system/scheduler/print', **{'?name': 'Bulanan2'}) +oe2 = resp2[0].get('on-event', '') +print(f"\nVerification:") +print(f" After set: '{oe2[:80]}'") +print(f" Match: {oe2 == short_script}") +print(f" len: {len(oe2)}") + +# Now set it BACK to original +print(f"\nRestoring original on-event...") +api.cmd('/system/scheduler/set', **{'.id': oid, 'on-event': current_oe}) +resp3 = api.cmd('/system/scheduler/print', **{'?name': 'Bulanan2'}) +oe3 = resp3[0].get('on-event', '') +print(f" Restored len: {len(oe3)}") +print(f" Match original: {oe3 == current_oe}") + +api.close() diff --git a/test-tg-notif.py b/test-tg-notif.py new file mode 100644 index 0000000..d27fc92 --- /dev/null +++ b/test-tg-notif.py @@ -0,0 +1,139 @@ +import socket, hashlib, time + +class R: + def __init__(self,h,p=8728): self.h=h; self.p=p; self.s=None + def c(self,u,p): + self.s=socket.socket(socket.AF_INET,socket.SOCK_STREAM); self.s.settimeout(15) + self.s.connect((self.h,self.p)) + self._ws('/login','=name='+u,'=password='+p) + r=self._rr() + if any(isinstance(x,tuple) and x[0]=='!trap' for x in r): + for x in r: + if isinstance(x,dict) and 'ret' in x: ch=x['ret'] + if ch and len(ch)==32: + h=hashlib.md5(b'\x00'+p.encode()+bytes.fromhex(ch)).hexdigest() + self._ws('/login','=name='+u,'=response=00'+h) + self._rr() + return self + def _el(self,l): + if l<0x80: return bytes([l]) + elif l<0x4000: l|=0x8000; return bytes([(l>>8)&0xFF,l&0xFF]) + elif l<0x200000: l|=0xC00000; return bytes([(l>>16)&0xFF,(l>>8)&0xFF,l&0xFF]) + elif l<0x10000000: l|=0xE0000000; return bytes([(l>>24)&0xFF,(l>>16)&0xFF,(l>>8)&0xFF,l&0xFF]) + else: return bytes([0xF0,(l>>24)&0xFF,(l>>16)&0xFF,(l>>8)&0xFF,l&0xFF]) + def _ww(self,w): self.s.sendall(self._el(len(w))+w.encode('utf-8')) + def _ws(self,c,*ws): self._ww(c); [self._ww(w) for w in ws]; self.s.sendall(b'\x00') + def _rb(self): return self.s.recv(1) + def _rw(self): + f=self._rb() + if f==b'\x00': return None + lb=f[0] + if lb&0x80: + if (lb&0xC0)==0x80: l=((lb&0x3F)<<8)+self._rb()[0] + elif (lb&0xE0)==0xC0: l=((lb&0x1F)<<16)+(self._rb()[0]<<8)+self._rb()[0] + elif (lb&0xF0)==0xE0: l=((lb&0x0F)<<24)+(self._rb()[0]<<16)+(self._rb()[0]<<8)+self._rb()[0] + else: l=(self._rb()[0]<<24)+(self._rb()[0]<<16)+(self._rb()[0]<<8)+self._rb()[0] + else: l=lb + d=b'' + while len(d)=3 else '' + else: d[w]='' + rs.append(d) + elif r=='!trap': rs.append(('!trap',t)) + return rs + def cmd(self,c,**a): + ws=[c] + for k,v in a.items(): + if k.startswith('?'): ws.append(f'{k}={v}') + else: ws.append(f'={k}={v}') + self._ws(*ws) + return self._rr() + def close(self): + if self.s: self.s.close(); self.s=None + +api=R('192.168.100.2').c('mi4','m14') + +rt=api.cmd('/system/clock/print')[0] +print(f"Router time: {rt['date']} {rt['time']}") + +# Cari Family scheduler next-run +for si in api.cmd('/system/scheduler/print'): + if si['name']=='Family': + next_run=si.get('next-run') + run_before=int(si.get('run-count',0)) + print(f"Family next: {next_run} run: {run_before}") + break + +# Hapus user test sebelumnya kalo ada +for u in api.cmd('/ip/hotspot/user/print',**{'?name':'tg_test_family'}): + api.cmd('/ip/hotspot/user/remove',**{'.id':u['.id']}) + +# Buat user Family dgn expiry masa lalu +api.cmd('/ip/hotspot/user/add',**{ + 'name':'tg_test_family', + 'password':'x', + 'profile':'Family', + 'comment':'jul/06/2026 00:00:00' +}) +print("\nCreated tg_test_family on Family profile (expired jul/06)") + +# Verifikasi +for u in api.cmd('/ip/hotspot/user/print',**{'?name':'tg_test_family'}): + print(f" comment: {u.get('comment')} uptime: {u.get('limit-uptime')}") + +# Tunggu scheduler Family jalan (interval ~2m37s, max 3 menit) +print(f"\nMenunggu scheduler Family (max 180 detik)...") +for i in range(180): + time.sleep(1) + if i%30==0: + for u in api.cmd('/ip/hotspot/user/print',**{'?name':'tg_test_family'}): + lu=u.get('limit-uptime','') + if lu=='1s': + print(f" [{i}s] EXPIRED! limit-uptime=1s. Notif Telegram terkirim โœ…") + break + else: + print(f" [{i}s] User REMOVED oleh scheduler โœ…") + break + # Cek run count + for si in api.cmd('/system/scheduler/print'): + if si['name']=='Family': + run_now=int(si.get('run-count',0)) + if run_now>run_before: + print(f" [{i}s] Family scheduler jalan (run={run_now})") + run_before=run_now + break + +print("\n=== HASIL ===") +for u in api.cmd('/ip/hotspot/user/print',**{'?name':'tg_test_family'}): + print(f"User: {u['name']} uptime={u.get('limit-uptime')}") + if u.get('limit-uptime')=='1s': + print("=> EXPIRED - notif Telegram seharusnya terkirim ke chat -765474293 โœ…") + api.cmd('/ip/hotspot/user/remove',**{'.id':u['.id']}) + break +else: + print("User sudah dihapus scheduler - notif Telegram terkirim โœ…") + +api.close() diff --git a/tg-check.py b/tg-check.py new file mode 100644 index 0000000..3c991cf --- /dev/null +++ b/tg-check.py @@ -0,0 +1,103 @@ +import socket, hashlib + +class R: + def __init__(self,h,p=8728): self.h=h; self.p=p; self.s=None + def c(self,u,p): + self.s=socket.socket(socket.AF_INET,socket.SOCK_STREAM); self.s.settimeout(15) + self.s.connect((self.h,self.p)) + self._ws('/login','=name='+u,'=password='+p) + r=self._rr() + if any(isinstance(x,tuple) and x[0]=='!trap' for x in r): + for x in r: + if isinstance(x,dict) and 'ret' in x: ch=x['ret'] + if ch and len(ch)==32: + h=hashlib.md5(b'\x00'+p.encode()+bytes.fromhex(ch)).hexdigest() + self._ws('/login','=name='+u,'=response=00'+h) + self._rr() + return self + def _el(self,l): + if l<0x80: return bytes([l]) + elif l<0x4000: l|=0x8000; return bytes([(l>>8)&0xFF,l&0xFF]) + elif l<0x200000: l|=0xC00000; return bytes([(l>>16)&0xFF,(l>>8)&0xFF,l&0xFF]) + elif l<0x10000000: l|=0xE0000000; return bytes([(l>>24)&0xFF,(l>>16)&0xFF,(l>>8)&0xFF,l&0xFF]) + else: return bytes([0xF0,(l>>24)&0xFF,(l>>16)&0xFF,(l>>8)&0xFF,l&0xFF]) + def _ww(self,w): self.s.sendall(self._el(len(w))+w.encode('utf-8')) + def _ws(self,c,*ws): self._ww(c); [self._ww(w) for w in ws]; self.s.sendall(b'\x00') + def _rb(self): return self.s.recv(1) + def _rw(self): + f=self._rb() + if f==b'\x00': return None + lb=f[0] + if lb&0x80: + if (lb&0xC0)==0x80: l=((lb&0x3F)<<8)+self._rb()[0] + elif (lb&0xE0)==0xC0: l=((lb&0x1F)<<16)+(self._rb()[0]<<8)+self._rb()[0] + elif (lb&0xF0)==0xE0: l=((lb&0x0F)<<24)+(self._rb()[0]<<16)+(self._rb()[0]<<8)+self._rb()[0] + else: l=(self._rb()[0]<<24)+(self._rb()[0]<<16)+(self._rb()[0]<<8)+self._rb()[0] + else: l=lb + d=b'' + while len(d)=3 else '' + else: d[w]='' + rs.append(d) + elif r=='!trap': rs.append(('!trap',t)) + return rs + def cmd(self,c,**a): + ws=[c] + for k,v in a.items(): + if k.startswith('?'): ws.append(f'{k}={v}') + else: ws.append(f'={k}={v}') + self._ws(*ws) + return self._rr() + def close(self): + if self.s: self.s.close(); self.s=None + +api=R('192.168.100.2').c('mi4','m14') + +print("=== Scheduler Telegram Check ===") +for si in api.cmd('/system/scheduler/print'): + name=si['name'] + if name in ['Log','tunnelbroker','backup-relay']: continue + oe=si.get('on-event','') + has_tg='5115036608' in oe + print(f"{name}: TG={'YES' if has_tg else 'no'}") + if has_tg: + for i,l in enumerate(oe.split('\n')): + if '5115036608' in l: + print(f" TG: {l.strip()[:140]}") + +print("\n=== Scripts with Telegram ===") +for sc in api.cmd('/system/script/print'): + src=sc.get('source','') + if '5115036608' in src: + print(f"{sc['name']}: has Telegram") + +print("\n=== Family on-event (full) ===") +for si in api.cmd('/system/scheduler/print'): + if si['name']=='Family': + print(si.get('on-event','')) + break + +api.close() diff --git a/tg-test-simple.py b/tg-test-simple.py new file mode 100644 index 0000000..29244c9 --- /dev/null +++ b/tg-test-simple.py @@ -0,0 +1,123 @@ +import socket, hashlib, time + +class R: + def __init__(self,h,p=8728): self.h=h; self.p=p; self.s=None + def c(self,u,p): + self.s=socket.socket(socket.AF_INET,socket.SOCK_STREAM); self.s.settimeout(15) + self.s.connect((self.h,self.p)) + self._ws('/login','=name='+u,'=password='+p) + r=self._rr() + if any(isinstance(x,tuple) and x[0]=='!trap' for x in r): + for x in r: + if isinstance(x,dict) and 'ret' in x: ch=x['ret'] + if ch and len(ch)==32: + h=hashlib.md5(b'\x00'+p.encode()+bytes.fromhex(ch)).hexdigest() + self._ws('/login','=name='+u,'=response=00'+h) + self._rr() + return self + def _el(self,l): + if l<0x80: return bytes([l]) + elif l<0x4000: l|=0x8000; return bytes([(l>>8)&0xFF,l&0xFF]) + elif l<0x200000: l|=0xC00000; return bytes([(l>>16)&0xFF,(l>>8)&0xFF,l&0xFF]) + elif l<0x10000000: l|=0xE0000000; return bytes([(l>>24)&0xFF,(l>>16)&0xFF,(l>>8)&0xFF,l&0xFF]) + else: return bytes([0xF0,(l>>24)&0xFF,(l>>16)&0xFF,(l>>8)&0xFF,l&0xFF]) + def _ww(self,w): self.s.sendall(self._el(len(w))+w.encode('utf-8')) + def _ws(self,c,*ws): self._ww(c); [self._ww(w) for w in ws]; self.s.sendall(b'\x00') + def _rb(self): return self.s.recv(1) + def _rw(self): + f=self._rb() + if f==b'\x00': return None + lb=f[0] + if lb&0x80: + if (lb&0xC0)==0x80: l=((lb&0x3F)<<8)+self._rb()[0] + elif (lb&0xE0)==0xC0: l=((lb&0x1F)<<16)+(self._rb()[0]<<8)+self._rb()[0] + elif (lb&0xF0)==0xE0: l=((lb&0x0F)<<24)+(self._rb()[0]<<16)+(self._rb()[0]<<8)+self._rb()[0] + else: l=(self._rb()[0]<<24)+(self._rb()[0]<<16)+(self._rb()[0]<<8)+self._rb()[0] + else: l=lb + d=b'' + while len(d)=3 else '' + else: d[w]='' + rs.append(d) + elif r=='!trap': rs.append(('!trap',t)) + return rs + def cmd(self,c,**a): + ws=[c] + for k,v in a.items(): + if k.startswith('?'): ws.append(f'{k}={v}') + else: ws.append(f'={k}={v}') + self._ws(*ws) + return self._rr() + def close(self): + if self.s: self.s.close(); self.s=None + +api=R('192.168.100.2').c('mi4','m14') + +# Cek waktu & Family scheduler +rt=api.cmd('/system/clock/print')[0] +print(f"Waktu router: {rt['date']} {rt['time']}") + +for si in api.cmd('/system/scheduler/print'): + if si['name']=='Family': + print(f"Family: run={si.get('run-count')} next={si.get('next-run')}") + break + +# Hapus user test sebelumnya +for u in api.cmd('/ip/hotspot/user/print',**{'?name':'tg_test_family'}): + api.cmd('/ip/hotspot/user/remove',**{'.id':u['.id']}) + +# Buat user Family expired +api.cmd('/ip/hotspot/user/add',**{ + 'name':'tg_test_family', + 'password':'x', + 'profile':'Family', + 'comment':'jul/06/2026 00:00:00' +}) +print("User tg_test_family dibuat (sudah expired)") + +# Tunggu 3 menit (Family interval ~2m37s) +print("Tunggu 180 detik...") +api.close() + +time.sleep(180) + +# Buka koneksi baru untuk cek +api2=R('192.168.100.2').c('mi4','m14') + +for u in api2.cmd('/ip/hotspot/user/print',**{'?name':'tg_test_family'}): + print(f"User ditemukan: limit-uptime={u.get('limit-uptime')}") + if u.get('limit-uptime')=='1s': + print("=> EXPIRED. Notif Telegram terkirim ke chat -765474293") + api2.cmd('/ip/hotspot/user/remove',**{'.id':u['.id']}) + break +else: + print("User SUDAH DIHAPUS oleh scheduler. Notif Telegram terkirim!") + +for si in api2.cmd('/system/scheduler/print'): + if si['name']=='Family': + print(f"Family run count now: {si.get('run-count')}") + +api2.close() diff --git a/unbypass-labibah.py b/unbypass-labibah.py new file mode 100644 index 0000000..1a8289d --- /dev/null +++ b/unbypass-labibah.py @@ -0,0 +1,94 @@ +import socket, hashlib + +class R: + def __init__(self,h,p=8728): self.h=h; self.p=p; self.s=None + def c(self,u,p): + self.s=socket.socket(socket.AF_INET,socket.SOCK_STREAM); self.s.settimeout(10) + self.s.connect((self.h,self.p)) + self._ws('/login','=name='+u,'=password='+p) + r=self._rr() + if any(isinstance(x,tuple) and x[0]=='!trap' for x in r): + for x in r: + if isinstance(x,dict) and 'ret' in x: ch=x['ret'] + if ch and len(ch)==32: + h=hashlib.md5(b'\x00'+p.encode()+bytes.fromhex(ch)).hexdigest() + self._ws('/login','=name='+u,'=response=00'+h) + self._rr() + return self + def _el(self,l): + if l<0x80: return bytes([l]) + elif l<0x4000: l|=0x8000; return bytes([(l>>8)&0xFF,l&0xFF]) + elif l<0x200000: l|=0xC00000; return bytes([(l>>16)&0xFF,(l>>8)&0xFF,l&0xFF]) + elif l<0x10000000: l|=0xE0000000; return bytes([(l>>24)&0xFF,(l>>16)&0xFF,(l>>8)&0xFF,l&0xFF]) + else: return bytes([0xF0,(l>>24)&0xFF,(l>>16)&0xFF,(l>>8)&0xFF,l&0xFF]) + def _ww(self,w): self.s.sendall(self._el(len(w))+w.encode('utf-8')) + def _ws(self,c,*ws): self._ww(c); [self._ww(w) for w in ws]; self.s.sendall(b'\x00') + def _rb(self): return self.s.recv(1) + def _rw(self): + f=self._rb() + if f==b'\x00': return None + lb=f[0] + if lb&0x80: + if (lb&0xC0)==0x80: l=((lb&0x3F)<<8)+self._rb()[0] + elif (lb&0xE0)==0xC0: l=((lb&0x1F)<<16)+(self._rb()[0]<<8)+self._rb()[0] + elif (lb&0xF0)==0xE0: l=((lb&0x0F)<<24)+(self._rb()[0]<<16)+(self._rb()[0]<<8)+self._rb()[0] + else: l=(self._rb()[0]<<24)+(self._rb()[0]<<16)+(self._rb()[0]<<8)+self._rb()[0] + else: l=lb + d=b'' + while len(d)=3 else '' + else: d[w]='' + rs.append(d) + elif r=='!trap': rs.append(('!trap',t)) + return rs + def cmd(self,c,**a): + ws=[c] + for k,v in a.items(): + if k.startswith('?'): ws.append(f'{k}={v}') + else: ws.append(f'={k}={v}') + self._ws(*ws) + return self._rr() + def close(self): + if self.s: self.s.close(); self.s=None + +api=R('192.168.100.2').c('mi4','m14') + +for b in api.cmd('/ip/hotspot/ip-binding/print'): + mac = b.get('mac-address', '') + if 'CA:1A:02:C2:8C:1E' in mac: + bid = b['.id'] + print(f'Found: {bid} MAC={mac} Type={b.get("type")} Server={b.get("server")}') + api.cmd('/ip/hotspot/ip-binding/set', **{'.id': bid, 'type': ''}) + print('Changed to regular (login required)') + break + +print() +for b in api.cmd('/ip/hotspot/ip-binding/print'): + mac = b.get('mac-address', '') + if 'CA:1A:02:C2:8C:1E' in mac: + print(f'Verify: MAC={mac} Type="{b.get("type")}"') + +api.close() diff --git a/update-scheduler-family-notify.py b/update-scheduler-family-notify.py new file mode 100644 index 0000000..8ea545b --- /dev/null +++ b/update-scheduler-family-notify.py @@ -0,0 +1,123 @@ +import socket, hashlib + +class R: + def __init__(self,h,p=8728): self.h=h; self.p=p; self.s=None + def c(self,u,p): + self.s=socket.socket(socket.AF_INET,socket.SOCK_STREAM); self.s.settimeout(15) + self.s.connect((self.h,self.p)) + self._ws('/login','=name='+u,'=password='+p) + r=self._rr() + if any(isinstance(x,tuple) for x in r): + for x in r: + if isinstance(x,dict) and 'ret' in x: ch=x['ret'] + if ch and len(ch)==32: + h=hashlib.md5(b'\x00'+p.encode()+bytes.fromhex(ch)).hexdigest() + self._ws('/login','=name='+u,'=response=00'+h) + self._rr() + return self + def _el(self,l): + if l<0x80: return bytes([l]) + elif l<0x4000: l|=0x8000; return bytes([(l>>8)&0xFF,l&0xFF]) + else: return bytes([0xC0|((l>>16)&0x0F),(l>>8)&0xFF,l&0xFF]) if l<0x200000 else bytes([0xE0|((l>>24)&0x07),(l>>16)&0xFF,(l>>8)&0xFF,l&0xFF]) + def _ww(self,w): self.s.sendall(self._el(len(w))+w.encode('utf-8')) + def _ws(self,c,*ws): self._ww(c); [self._ww(w) for w in ws]; self.s.sendall(b'\x00') + def _rw(self): + f=self._rb() + if f==b'\x00': return None + lb=f[0]; l=lb + if lb&0x80: + if (lb&0xC0)==0x80: l=((lb&0x3F)<<8)+self._rb()[0] + elif (lb&0xE0)==0xC0: l=((lb&0x1F)<<16)+(self._rb()[0]<<8)+self._rb()[0] + else: l=((lb&0x0F)<<24)+(self._rb()[0]<<16)+(self._rb()[0]<<8)+self._rb()[0] + d=b'' + while len(d)=3 else '' + else: d[w]='' + rs.append(d) + elif r=='!trap': rs.append(('!trap',t)) + return rs + def cmd(self,c,**a): + ws=[c] + for k,v in a.items(): ws.append(('?' if k.startswith('?') else '=')+k+'='+v) + self._ws(*ws) + return self._rr() + def close(self): + if self.s: self.s.close(); self.s=None + +api=R('192.168.100.2').c('mi4','m14') + +print("=== Current Family scheduler ===") +for si in api.cmd('/system/scheduler/print'): + if si['name'] == 'Family': + oe = si.get('on-event', '') + for line in oe.split(';'): + l = line.strip() + if l: + print(" %s" % l[:150]) + break + +# Update the fetch URL +# Old: /tool fetch url=("https://api.telegram.org/bot" . $bot . "/sendMessage?message_thread_id=42&chat_id=" . $chat . "&text=" . $msg) http-method=get keep-result=no +# New: /tool fetch url=("http://192.168.100.6/notify-expire.php?key=msr-2026-reset&name=" . $name) http-method=get keep-result=no + +for si in api.cmd('/system/scheduler/print'): + if si['name'] == 'Family': + oe = si.get('on-event', '') + old_line = '/tool fetch url=("https://api.telegram.org/bot" . $bot . "/sendMessage?message_thread_id=42&chat_id=" . $chat . "&text=" . $msg) http-method=get keep-result=no' + new_line = '/tool fetch url=("http://192.168.100.6/notify-expire.php?key=msr-2026-reset&name=" . $name) http-method=get keep-result=no' + + if old_line in oe: + new_oe = oe.replace(old_line, new_line) + api.cmd('/system/scheduler/set', numbers=si['.id'], **{'on-event': new_oe}) + print("\nUpdated Family scheduler!") + else: + print("\nOld pattern not found. Current fetch line:") + for line in oe.split(';'): + if 'fetch' in line: + print(" [%s]" % line.strip()[:200]) + break + +# Add callback polling scheduler +# Check if it already exists +exists = False +for si in api.cmd('/system/scheduler/print'): + if si['name'] == 'tg-cb-poll': + exists = True + print("\ntg-cb-poll scheduler already exists") + break + +if not exists: + api.cmd('/system/scheduler/add', + name='tg-cb-poll', + interval='00:01:00', + on_event=':delay 10s;/tool fetch url="http://192.168.100.6/process/telegram-cb.php?key=msr-2026-reset" http-method=get keep-result=no', + starttime='startup', + disabled='no') + print("\nCreated tg-cb-poll scheduler (every 60s)") + +print("\n=== Final scheduler list ===") +for si in api.cmd('/system/scheduler/print'): + print(" %s | %s | interval=%s | disabled=%s" % (si['name'], si.get('.id',''), si.get('interval',''), si.get('disabled',''))) + +api.close() diff --git a/update-scheduler-family.py b/update-scheduler-family.py new file mode 100644 index 0000000..bfbb5be --- /dev/null +++ b/update-scheduler-family.py @@ -0,0 +1,116 @@ +import socket, hashlib + +class R: + def __init__(self,h,p=8728): self.h=h; self.p=p; self.s=None + def c(self,u,p): + self.s=socket.socket(socket.AF_INET,socket.SOCK_STREAM); self.s.settimeout(15) + self.s.connect((self.h,self.p)) + self._ws('/login','=name='+u,'=password='+p) + r=self._rr() + if any(isinstance(x,tuple) and x[0]=='!trap' for x in r): + for x in r: + if isinstance(x,dict) and 'ret' in x: ch=x['ret'] + if ch and len(ch)==32: + h=hashlib.md5(b'\x00'+p.encode()+bytes.fromhex(ch)).hexdigest() + self._ws('/login','=name='+u,'=response=00'+h) + self._rr() + return self + def _el(self,l): + if l<0x80: return bytes([l]) + elif l<0x4000: l|=0x8000; return bytes([(l>>8)&0xFF,l&0xFF]) + elif l<0x200000: l|=0xC00000; return bytes([(l>>16)&0xFF,(l>>8)&0xFF,l&0xFF]) + elif l<0x10000000: l|=0xE0000000; return bytes([(l>>24)&0xFF,(l>>16)&0xFF,(l>>8)&0xFF,l&0xFF]) + else: return bytes([0xF0,(l>>24)&0xFF,(l>>16)&0xFF,(l>>8)&0xFF,l&0xFF]) + def _ww(self,w): self.s.sendall(self._el(len(w))+w.encode('utf-8')) + def _ws(self,c,*ws): self._ww(c); [self._ww(w) for w in ws]; self.s.sendall(b'\x00') + def _rb(self): return self.s.recv(1) + def _rw(self): + f=self._rb() + if f==b'\x00': return None + lb=f[0] + if lb&0x80: + if (lb&0xC0)==0x80: l=((lb&0x3F)<<8)+self._rb()[0] + elif (lb&0xE0)==0xC0: l=((lb&0x1F)<<16)+(self._rb()[0]<<8)+self._rb()[0] + elif (lb&0xF0)==0xE0: l=((lb&0x0F)<<24)+(self._rb()[0]<<16)+(self._rb()[0]<<8)+self._rb()[0] + else: l=(self._rb()[0]<<24)+(self._rb()[0]<<16)+(self._rb()[0]<<8)+self._rb()[0] + else: l=lb + d=b'' + while len(d)=3 else '' + else: d[w]='' + rs.append(d) + elif r=='!trap': rs.append(('!trap',t)) + return rs + def cmd(self,c,**a): + ws=[c] + for k,v in a.items(): + if k.startswith('?'): ws.append(f'{k}={v}') + else: ws.append(f'={k}={v}') + self._ws(*ws) + return self._rr() + def close(self): + if self.s: self.s.close(); self.s=None + +api=R('192.168.100.2').c('mi4','m14') + +# Get current Family scheduler +print("=== Current Family scheduler ===") +family = None +for si in api.cmd('/system/scheduler/print'): + if si['name'] == 'Family': + family = si + print(f" Interval: {si.get('interval','')}") + print(f" On-event: {si.get('on-event','')[:100]}") + +if not family: + print(" NOT FOUND!") + api.close() + exit() + +oe = family.get('on-event', '') +# Replace chat ID: -765474293 -> -1002554941429 +new_oe = oe.replace('"-765474293"', '"-1002554941429"') + +if new_oe == oe: + print("\n Chat ID already updated?") + api.close() + exit() + +print(f"\n=== Updating Family scheduler ===") +# Update via set +api.cmd('/system/scheduler/set', numbers=family['.id'], **{'on-event': new_oe}) +print(" Updated!") + +# Verify +for si in api.cmd('/system/scheduler/print'): + if si['name'] == 'Family': + oe2 = si.get('on-event', '') + print(f" New on-event chat section: ", end='') + for line in oe2.split(';'): + if 'chat' in line.lower(): + print(line.strip()[:120]) + +api.close() diff --git a/update-scheduler-thread.py b/update-scheduler-thread.py new file mode 100644 index 0000000..6e23db9 --- /dev/null +++ b/update-scheduler-thread.py @@ -0,0 +1,126 @@ +import socket, hashlib + +class R: + def __init__(self,h,p=8728): self.h=h; self.p=p; self.s=None + def c(self,u,p): + self.s=socket.socket(socket.AF_INET,socket.SOCK_STREAM); self.s.settimeout(15) + self.s.connect((self.h,self.p)) + self._ws('/login','=name='+u,'=password='+p) + r=self._rr() + if any(isinstance(x,tuple) and x[0]=='!trap' for x in r): + for x in r: + if isinstance(x,dict) and 'ret' in x: ch=x['ret'] + if ch and len(ch)==32: + h=hashlib.md5(b'\x00'+p.encode()+bytes.fromhex(ch)).hexdigest() + self._ws('/login','=name='+u,'=response=00'+h) + self._rr() + return self + def _el(self,l): + if l<0x80: return bytes([l]) + elif l<0x4000: l|=0x8000; return bytes([(l>>8)&0xFF,l&0xFF]) + elif l<0x200000: l|=0xC00000; return bytes([(l>>16)&0xFF,(l>>8)&0xFF,l&0xFF]) + elif l<0x10000000: l|=0xE0000000; return bytes([(l>>24)&0xFF,(l>>16)&0xFF,(l>>8)&0xFF,l&0xFF]) + else: return bytes([0xF0,(l>>24)&0xFF,(l>>16)&0xFF,(l>>8)&0xFF,l&0xFF]) + def _ww(self,w): self.s.sendall(self._el(len(w))+w.encode('utf-8')) + def _ws(self,c,*ws): self._ww(c); [self._ww(w) for w in ws]; self.s.sendall(b'\x00') + def _rb(self): return self.s.recv(1) + def _rw(self): + f=self._rb() + if f==b'\x00': return None + lb=f[0] + if lb&0x80: + if (lb&0xC0)==0x80: l=((lb&0x3F)<<8)+self._rb()[0] + elif (lb&0xE0)==0xC0: l=((lb&0x1F)<<16)+(self._rb()[0]<<8)+self._rb()[0] + elif (lb&0xF0)==0xE0: l=((lb&0x0F)<<24)+(self._rb()[0]<<16)+(self._rb()[0]<<8)+self._rb()[0] + else: l=(self._rb()[0]<<24)+(self._rb()[0]<<16)+(self._rb()[0]<<8)+self._rb()[0] + else: l=lb + d=b'' + while len(d)=3 else '' + else: d[w]='' + rs.append(d) + elif r=='!trap': rs.append(('!trap',t)) + return rs + def cmd(self,c,**a): + ws=[c] + for k,v in a.items(): + if k.startswith('?'): ws.append(f'{k}={v}') + else: ws.append(f'={k}={v}') + self._ws(*ws) + return self._rr() + def close(self): + if self.s: self.s.close(); self.s=None + +api=R('192.168.100.2').c('mi4','m14') + +# Get current Family scheduler +print("=== Current Family scheduler ===") +for si in api.cmd('/system/scheduler/print'): + if si['name'] == 'Family': + oe = si.get('on-event', '') + for line in oe.split(';'): + l = line.strip() + if l: + print(f" {l[:130]}") + break + +# Update: add message_thread_id=42 to the fetch URL +for si in api.cmd('/system/scheduler/print'): + if si['name'] == 'Family': + oe = si.get('on-event', '') + # Replace the fetch URL to include message_thread_id + # Current: .../sendMessage?chat_id=...&text=... + # New: .../sendMessage?chat_id=...&message_thread_id=42&text=... + old = '/sendMessage?chat_id=' + new = '/sendMessage?message_thread_id=42&chat_id=' + new_oe = oe.replace(old, new) + + if new_oe == oe: + print("\n message_thread_id already in URL") + else: + print("\n Adding message_thread_id=42...") + api.cmd('/system/scheduler/set', numbers=si['.id'], **{'on-event': new_oe}) + print(" Updated!") + + # Verify + for sv in api.cmd('/system/scheduler/print'): + if sv['name'] == 'Family': + oe2 = sv.get('on-event', '') + for line in oe2.split(';'): + l = line.strip() + if 'fetch' in l or 'chat' in l: + print(f" {l[:150]}") + break + +# Send test to Notif Expire topic via tool fetch +print("\n=== Send test to Notif Expire topic ===") +api._ws('/tool/fetch', + '=url=https://api.telegram.org/bot5115036608:AAGeyXVq5eNvdAkaCQHU9NfH16uAAiSKiuk/sendMessage?message_thread_id=42&chat_id=-1002554941429&text=Test+notif+expire+via+scheduler', + '=http-method=get', '=keep-result=no') +r = api._rr() +print(f" Result: {r[:2]}") + +api.close() diff --git a/update-tg-and-check-key.py b/update-tg-and-check-key.py new file mode 100644 index 0000000..8b122c6 --- /dev/null +++ b/update-tg-and-check-key.py @@ -0,0 +1,123 @@ +import socket, hashlib + +class R: + def __init__(self,h,p=8728): self.h=h; self.p=p; self.s=None + def c(self,u,p): + self.s=socket.socket(socket.AF_INET,socket.SOCK_STREAM); self.s.settimeout(15) + self.s.connect((self.h,self.p)) + self._ws('/login','=name='+u,'=password='+p) + r=self._rr() + if any(isinstance(x,tuple) and x[0]=='!trap' for x in r): + for x in r: + if isinstance(x,dict) and 'ret' in x: ch=x['ret'] + if ch and len(ch)==32: + h=hashlib.md5(b'\x00'+p.encode()+bytes.fromhex(ch)).hexdigest() + self._ws('/login','=name='+u,'=response=00'+h) + self._rr() + return self + def _el(self,l): + if l<0x80: return bytes([l]) + elif l<0x4000: l|=0x8000; return bytes([(l>>8)&0xFF,l&0xFF]) + elif l<0x200000: l|=0xC00000; return bytes([(l>>16)&0xFF,(l>>8)&0xFF,l&0xFF]) + elif l<0x10000000: l|=0xE0000000; return bytes([(l>>24)&0xFF,(l>>16)&0xFF,(l>>8)&0xFF,l&0xFF]) + else: return bytes([0xF0,(l>>24)&0xFF,(l>>16)&0xFF,(l>>8)&0xFF,l&0xFF]) + def _ww(self,w): self.s.sendall(self._el(len(w))+w.encode('utf-8')) + def _ws(self,c,*ws): self._ww(c); [self._ww(w) for w in ws]; self.s.sendall(b'\x00') + def _rb(self): return self.s.recv(1) + def _rw(self): + f=self._rb() + if f==b'\x00': return None + lb=f[0] + if lb&0x80: + if (lb&0xC0)==0x80: l=((lb&0x3F)<<8)+self._rb()[0] + elif (lb&0xE0)==0xC0: l=((lb&0x1F)<<16)+(self._rb()[0]<<8)+self._rb()[0] + elif (lb&0xF0)==0xE0: l=((lb&0x0F)<<24)+(self._rb()[0]<<16)+(self._rb()[0]<<8)+self._rb()[0] + else: l=(self._rb()[0]<<24)+(self._rb()[0]<<16)+(self._rb()[0]<<8)+self._rb()[0] + else: l=lb + d=b'' + while len(d)=3 else '' + else: d[w]='' + rs.append(d) + elif r=='!trap': rs.append(('!trap',t)) + return rs + def cmd(self,c,**a): + ws=[c] + for k,v in a.items(): + if k.startswith('?'): ws.append(f'{k}={v}') + else: ws.append(f'={k}={v}') + self._ws(*ws) + return self._rr() + def close(self): + if self.s: self.s.close(); self.s=None + +api=R('192.168.100.2').c('mi4','m14') + +# 1. Cek script untuk key/token +print("=== Script dengan API key / token ===") +for sc in api.cmd('/system/script/print'): + src = sc.get('source', '') + name = sc['name'] + # Cari kata kunci: key, token, api, secret + keywords = ['key', 'token', 'api', 'secret', 'password', 'sk-', 'dahl', 'hc-'] + found = [] + for kw in keywords: + if kw.lower() in src.lower(): + found.append(kw) + if found: + print(f" {name}: keywords={found}") + # Tampilkan baris yg mengandung keyword + for line in src.split('\n'): + for kw in keywords: + if kw.lower() in line.lower(): + print(f" -> {line.strip()[:150]}") + break + +# 2. Cek scheduler on-event Family, dump bagian bot +print("\n=== Family scheduler - Telegram config ===") +for si in api.cmd('/system/scheduler/print'): + if si['name'] == 'Family': + oe = si.get('on-event', '') + # Extract bot/chat lines + for line in oe.split(';'): + if '5115036608' in line or 'bot' in line.lower() or 'chat' in line.lower(): + print(f" {line.strip()[:150]}") + break + +# 3. Cek file di router +print("\n=== Files on router ===") +for f in api.cmd('/file/print'): + name = f.get('name', '') + size = f.get('size', '0') + if int(size) > 0 and name.endswith('.txt') or name.endswith('.conf') or name.endswith('.rsc'): + print(f" {name} ({size} bytes)") + +# 4. Cek env vars +print("\n=== Env vars ===") +for env in api.cmd('/system/script/environment/print'): + print(f" {env.get('name')} = {env.get('value', '').strip()[:100]}") + +api.close() diff --git a/verif-tg.py b/verif-tg.py new file mode 100644 index 0000000..a9670b5 --- /dev/null +++ b/verif-tg.py @@ -0,0 +1,83 @@ +import socket, hashlib, urllib.parse +class R: + def __init__(s,h,p=8728): s.h=h; s.p=p; s.s=None + def c(s,u,p): + s.s=socket.socket(); s.s.settimeout(30) + s.s.connect((s.h,s.p)) + s._ws('/login','=name='+u,'=password='+p) + r=s._rr() + if any(isinstance(x,tuple) for x in r): + for x in r: + if isinstance(x,dict) and 'ret' in x: ch=x['ret'] + if ch and len(ch)==32: + hh=hashlib.md5(b'\x00'+p.encode()+bytes.fromhex(ch)).hexdigest() + s._ws('/login','=name='+u,'=response=00'+hh); s._rr() + return s + def _el(s,l): + if l<0x80: return bytes([l]) + elif l<0x4000: l|=0x8000; return bytes([(l>>8)&0xFF,l&0xFF]) + else: return bytes([0xC0|((l>>16)&0x0F),(l>>8)&0xFF,l&0xFF]) if l<0x200000 else bytes([0xE0|((l>>24)&0x07),(l>>16)&0xFF,(l>>8)&0xFF,l&0xFF]) + def _ww(s,w): s.s.sendall(s._el(len(w))+w.encode()) + def _ws(s,c,*ws): s._ww(c); [s._ww(w) for w in ws]; s.s.sendall(b'\x00') + def _rw(s): + f=s.s.recv(1) + if f==b'\x00': return None + lb=f[0]; l=lb + if lb&0x80: + if (lb&0xC0)==0x80: l=((lb&0x3F)<<8)+s.s.recv(1)[0] + elif (lb&0xE0)==0xC0: l=((lb&0x1F)<<16)+(s.s.recv(1)[0]<<8)+s.s.recv(1)[0] + else: l=((lb&0x0F)<<24)+(s.s.recv(1)[0]<<16)+(s.s.recv(1)[0]<<8)+s.s.recv(1)[0] + d=b'' + while len(d)=3 else '' + else: d[w]='' + rs.append(d) + elif r=='!trap': rs.append(('!trap',t)) + return rs + def cmd(s,c,**a): + ws=[c] + for k,v in a.items(): + ws.append(('?' if k.startswith('?') else '=')+k+'='+v) + s._ws(*ws) + return s._rr() + def close(s): + if s.s: s.s.close() + +api=R('remote.vpnmurahjogja.my.id',33206).c('mi4','m14') + +token='5115036608:AAGeyXVq5eNvdAkaCQHU9NfH16uAAiSKiuk' +chat='-1002554941429' +thread='42' + +text = 'VERIFIKASI final - notif router bekerja' +url = 'https://api.telegram.org/bot%s/sendMessage?chat_id=%s&message_thread_id=%s&text=%s' % (token, chat, thread, urllib.parse.quote_plus(text)) + +# jalankan script via /system/script? simpler: gunakan /tool fetch dengan output=user via :put +cmd = ':put [/tool fetch url="%s" mode=https http-method=get output=user]' % url +print('CMD:', cmd[:120]) +r = api.cmd('/system/script/run', **{'=script': ''}) # placeholder +# Actually use execute +r = api.cmd('/execute', **{'=command': cmd}) +print('EXEC:', r) + +api.close() diff --git a/verif-tg2.py b/verif-tg2.py new file mode 100644 index 0000000..0ff9e76 --- /dev/null +++ b/verif-tg2.py @@ -0,0 +1,81 @@ +import socket, hashlib, urllib.parse +class R: + def __init__(s,h,p=8728): s.h=h; s.p=p; s.s=None + def c(s,u,p): + s.s=socket.socket(); s.s.settimeout(30) + s.s.connect((s.h,s.p)) + s._ws('/login','=name='+u,'=password='+p) + r=s._rr() + if any(isinstance(x,tuple) for x in r): + for x in r: + if isinstance(x,dict) and 'ret' in x: ch=x['ret'] + if ch and len(ch)==32: + hh=hashlib.md5(b'\x00'+p.encode()+bytes.fromhex(ch)).hexdigest() + s._ws('/login','=name='+u,'=response=00'+hh); s._rr() + return s + def _el(s,l): + if l<0x80: return bytes([l]) + elif l<0x4000: l|=0x8000; return bytes([(l>>8)&0xFF,l&0xFF]) + else: return bytes([0xC0|((l>>16)&0x0F),(l>>8)&0xFF,l&0xFF]) if l<0x200000 else bytes([0xE0|((l>>24)&0x07),(l>>16)&0xFF,(l>>8)&0xFF,l&0xFF]) + def _ww(s,w): s.s.sendall(s._el(len(w))+w.encode()) + def _ws(s,c,*ws): s._ww(c); [s._ww(w) for w in ws]; s.s.sendall(b'\x00') + def _rw(s): + f=s.s.recv(1) + if f==b'\x00': return None + lb=f[0]; l=lb + if lb&0x80: + if (lb&0xC0)==0x80: l=((lb&0x3F)<<8)+s.s.recv(1)[0] + elif (lb&0xE0)==0xC0: l=((lb&0x1F)<<16)+(s.s.recv(1)[0]<<8)+s.s.recv(1)[0] + else: l=((lb&0x0F)<<24)+(s.s.recv(1)[0]<<16)+(s.s.recv(1)[0]<<8)+s.s.recv(1)[0] + d=b'' + while len(d)=3 else '' + else: d[w]='' + rs.append(d) + elif r=='!trap': rs.append(('!trap',t)) + return rs + def cmd(s,c,**a): + ws=[c] + for k,v in a.items(): + ws.append(('?' if k.startswith('?') else '=')+k+'='+v) + s._ws(*ws) + return s._rr() + def close(s): + if s.s: s.s.close() + +api=R('remote.vpnmurahjogja.my.id',33206).c('mi4','m14') + +r = api.cmd('/system/resource/print') +print('VERSION:', [x.get('version') for x in r if isinstance(x,dict)]) + +# cek param fetch yang valid: coba output=user +token='5115036608:AAGeyXVq5eNvdAkaCQHU9NfH16uAAiSKiuk' +chat='-1002554941429' +thread='42' +text = 'VERIFIKASI - pesan dari router' +url = 'https://api.telegram.org/bot%s/sendMessage?chat_id=%s&message_thread_id=%s&text=%s' % (token, chat, thread, urllib.parse.quote_plus(text)) + +r = api.cmd('/tool/fetch', url=url, mode='https', **{'http-method': 'get', 'keep-result': 'yes', 'output': 'user'}) +print('output=user:', r) + +api.close() diff --git a/verify-after-fix.py b/verify-after-fix.py new file mode 100644 index 0000000..2cdfef4 --- /dev/null +++ b/verify-after-fix.py @@ -0,0 +1,97 @@ +import socket, hashlib +class R: + def __init__(s,h,p=8728): s.h=h; s.p=p; s.s=None + def c(s,u,p): + s.s=socket.socket(); s.s.settimeout(15) + s.s.connect((s.h,s.p)) + s._ws('/login','=name='+u,'=password='+p) + r=s._rr() + if any(isinstance(x,tuple) for x in r): + for x in r: + if isinstance(x,dict) and 'ret' in x: ch=x['ret'] + if ch and len(ch)==32: + hh=hashlib.md5(b'\x00'+p.encode()+bytes.fromhex(ch)).hexdigest() + s._ws('/login','=name='+u,'=response=00'+hh); s._rr() + return s + def _el(s,l): + if l<0x80: return bytes([l]) + elif l<0x4000: l|=0x8000; return bytes([(l>>8)&0xFF,l&0xFF]) + else: return bytes([0xC0|((l>>16)&0x0F),(l>>8)&0xFF,l&0xFF]) if l<0x200000 else bytes([0xE0|((l>>24)&0x07),(l>>16)&0xFF,(l>>8)&0xFF,l&0xFF]) + def _ww(s,w): s.s.sendall(s._el(len(w))+w.encode()) + def _ws(s,c,*ws): s._ww(c); [s._ww(w) for w in ws]; s.s.sendall(b'\x00') + def _rw(s): + f=s.s.recv(1) + if f==b'\x00': return None + lb=f[0]; l=lb + if lb&0x80: + if (lb&0xC0)==0x80: l=((lb&0x3F)<<8)+s.s.recv(1)[0] + elif (lb&0xE0)==0xC0: l=((lb&0x1F)<<16)+(s.s.recv(1)[0]<<8)+s.s.recv(1)[0] + else: l=((lb&0x0F)<<24)+(s.s.recv(1)[0]<<16)+(s.s.recv(1)[0]<<8)+s.s.recv(1)[0] + d=b'' + while len(d)=3 else '' + else: d[w]='' + rs.append(d) + elif r=='!trap': rs.append(('!trap',t)) + return rs + def cmd(s,c,**a): + ws=[c] + for k,v in a.items(): + ws.append(('?' if k.startswith('?') else '=')+k+'='+v) + s._ws(*ws) + return s._rr() + def close(s): + if s.s: s.s.close() + +api=R('remote.vpnmurahjogja.my.id',33206).c('mi4','m14') + +print('=== Bulanan4 users after fix ===') +total = 0 +expired_set = 0 +still_active = 0 +for u in api.cmd('/ip/hotspot/user/print', **{'?profile': 'Bulanan4'}): + total += 1 + n = u.get('name','') + c = u.get('comment','') + lim = u.get('limit-uptime','') + dis = u.get('disabled','') + if lim == '1s': + expired_set += 1 + status = 'EXPIRED(1s)' + elif not c: + status = 'NOCOMMENT' + else: + still_active += 1 + status = 'ACTIVE' + print(' %s | %s | limit-uptime=%s | %s' % (n, c, lim, status)) + +print() +print('Total: %d | Expired(1s): %d | Still active: %d' % (total, expired_set, still_active)) + +# Clock verification +clk = api.cmd('/system/clock/print') +if clk: + print() + print('Clock: date=%s time=%s' % (clk[0].get('date',''), clk[0].get('time',''))) + +api.close() diff --git a/verify-expiry.py b/verify-expiry.py new file mode 100644 index 0000000..d92a64c --- /dev/null +++ b/verify-expiry.py @@ -0,0 +1,158 @@ +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() diff --git a/verify-fix.py b/verify-fix.py new file mode 100644 index 0000000..9607b27 --- /dev/null +++ b/verify-fix.py @@ -0,0 +1,125 @@ +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(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 + +import re + +api = RouterOSApi('192.168.100.2') +api.connect('mi4', 'm14') + +schedulers = ["Bulanan2", "Bulanan4", "vc-2K3J", "vc-5K10J", "Family", "DevB2"] + +print("=== VERIFICATION (detailed) ===") +for s in schedulers: + resp = api.cmd('/system/scheduler/print', **{'?name': s}) + if not resp: continue + + oe = resp[0]['on-event'] + + # Count real :pic typo (not inside :pick) + # Real typo: :pic followed by space, $, ], ;, etc. (NOT k) + real_pic = len(re.findall(r':pic(?!k)', oe)) + real_pick = oe.count(':pick') + pickk = oe.count(':pickk') + + print(f"{s}:") + print(f" Real :pic (typo): {real_pic}") + print(f" :pick (correct): {real_pick}") + print(f" :pickk (corrupt): {pickk}") + + # Show context around first 3 :pic occurrences if any + if real_pic > 0: + idx = 0 + found = 0 + while found < 3: + m = re.search(r':pic(?!k)', oe[idx:]) + if not m: break + start = idx + m.start() + ctx = oe[max(0,start-15):start+15] + print(f" Typo at {start}: ...{ctx}...") + idx = start + 4 + found += 1 + else: + print(f" STATUS: ALL CORRECT (no typo)") + +api.close() diff --git a/verify-laporkan.py b/verify-laporkan.py new file mode 100644 index 0000000..51a5d2a --- /dev/null +++ b/verify-laporkan.py @@ -0,0 +1,107 @@ +import socket, hashlib, base64 + +print("=== Decode password dari config.php ===") +enc = "pWJm" +# Coba base64 decode +try: + dec = base64.b64decode(enc).decode() + print(f" base64 decode '{enc}' -> '{dec}'") +except: + print(f" '{enc}' bukan base64 valid") + +# Coba reverse +print(f" reverse: '{enc[::-1]}'") + +print("\n=== Verifikasi Family scheduler ===") +class R: + def __init__(self,h,p=8728): self.h=h; self.p=p; self.s=None + def c(self,u,p): + self.s=socket.socket(socket.AF_INET,socket.SOCK_STREAM); self.s.settimeout(15) + self.s.connect((self.h,self.p)) + self._ws('/login','=name='+u,'=password='+p) + r=self._rr() + if any(isinstance(x,tuple) and x[0]=='!trap' for x in r): + for x in r: + if isinstance(x,dict) and 'ret' in x: ch=x['ret'] + if ch and len(ch)==32: + h=hashlib.md5(b'\x00'+p.encode()+bytes.fromhex(ch)).hexdigest() + self._ws('/login','=name='+u,'=response=00'+h) + self._rr() + return self + def _el(self,l): + if l<0x80: return bytes([l]) + elif l<0x4000: l|=0x8000; return bytes([(l>>8)&0xFF,l&0xFF]) + elif l<0x200000: l|=0xC00000; return bytes([(l>>16)&0xFF,(l>>8)&0xFF,l&0xFF]) + elif l<0x10000000: l|=0xE0000000; return bytes([(l>>24)&0xFF,(l>>16)&0xFF,(l>>8)&0xFF,l&0xFF]) + else: return bytes([0xF0,(l>>24)&0xFF,(l>>16)&0xFF,(l>>8)&0xFF,l&0xFF]) + def _ww(self,w): self.s.sendall(self._el(len(w))+w.encode('utf-8')) + def _ws(self,c,*ws): self._ww(c); [self._ww(w) for w in ws]; self.s.sendall(b'\x00') + def _rb(self): return self.s.recv(1) + def _rw(self): + f=self._rb() + if f==b'\x00': return None + lb=f[0] + if lb&0x80: + if (lb&0xC0)==0x80: l=((lb&0x3F)<<8)+self._rb()[0] + elif (lb&0xE0)==0xC0: l=((lb&0x1F)<<16)+(self._rb()[0]<<8)+self._rb()[0] + elif (lb&0xF0)==0xE0: l=((lb&0x0F)<<24)+(self._rb()[0]<<16)+(self._rb()[0]<<8)+self._rb()[0] + else: l=(self._rb()[0]<<24)+(self._rb()[0]<<16)+(self._rb()[0]<<8)+self._rb()[0] + else: l=lb + d=b'' + while len(d)=3 else '' + else: d[w]='' + rs.append(d) + elif r=='!trap': rs.append(('!trap',t)) + return rs + def cmd(self,c,**a): + ws=[c] + for k,v in a.items(): + if k.startswith('?'): ws.append(f'{k}={v}') + else: ws.append(f'={k}={v}') + self._ws(*ws) + return self._rr() + def close(self): + if self.s: self.s.close(); self.s=None + +api=R('192.168.100.2').c('mi4','m14') + +# Kirim test notif ke laporkan +print("\n=== Send test notification to laporkan ===") +tg = "/tool/fetch" +api._ws(tg, '=url=https://api.telegram.org/bot5115036608:AAGeyXVq5eNvdAkaCQHU9NfH16uAAiSKiuk/sendMessage?chat_id=-1002554941429&text=Test+notif+expire+dari+scheduler+Family', '=http-method=get', '=keep-result=no') +r = api._rr() +print(f" Result: {r[:2]}") + +# Check scheduler stats +print("\n=== Scheduler stats ===") +for si in api.cmd('/system/scheduler/print'): + name = si['name'] + run = si.get('run-count', '?') + intv = si.get('interval', '?') + print(f" {name}: r={run} intv={intv}") + +api.close() diff --git a/verify-markup.py b/verify-markup.py new file mode 100644 index 0000000..879b402 --- /dev/null +++ b/verify-markup.py @@ -0,0 +1,78 @@ +import socket, hashlib +class R: + def __init__(s,h,p=8728): s.h=h; s.p=p; s.s=None + def c(s,u,p): + s.s=socket.socket(); s.s.settimeout(60) + s.s.connect((s.h,s.p)) + s._ws('/login','=name='+u,'=password='+p) + r=s._rr() + if any(isinstance(x,tuple) for x in r): + for x in r: + if isinstance(x,dict) and 'ret' in x: ch=x['ret'] + if ch and len(ch)==32: + hh=hashlib.md5(b'\x00'+p.encode()+bytes.fromhex(ch)).hexdigest() + s._ws('/login','=name='+u,'=response=00'+hh); s._rr() + return s + def _el(s,l): + if l<0x80: return bytes([l]) + elif l<0x4000: l|=0x8000; return bytes([(l>>8)&0xFF,l&0xFF]) + else: return bytes([0xC0|((l>>16)&0x0F),(l>>8)&0xFF,l&0xFF]) if l<0x200000 else bytes([0xE0|((l>>24)&0x07),(l>>16)&0xFF,(l>>8)&0xFF,l&0xFF]) + def _ww(s,w): s.s.sendall(s._el(len(w))+w.encode()) + def _ws(s,c,*ws): s._ww(c); [s._ww(w) for w in ws]; s.s.sendall(b'\x00') + def _rw(s): + f=s.s.recv(1) + if f==b'\x00': return None + lb=f[0]; l=lb + if lb&0x80: + if (lb&0xC0)==0x80: l=((lb&0x3F)<<8)+s.s.recv(1)[0] + elif (lb&0xE0)==0xC0: l=((lb&0x1F)<<16)+(s.s.recv(1)[0]<<8)+s.s.recv(1)[0] + else: l=((lb&0x0F)<<24)+(s.s.recv(1)[0]<<16)+(s.s.recv(1)[0]<<8)+s.s.recv(1)[0] + d=b'' + while len(d)=3 else '' + else: d[w]='' + rs.append(d) + elif r=='!trap': rs.append(('!trap',t)) + return rs + def cmd(s,c,**a): + ws=[c] + for k,v in a.items(): + ws.append(('?' if k.startswith('?') else '=')+k+'='+v) + s._ws(*ws) + return s._rr() + def close(s): + if s.s: s.s.close() + +api=R('remote.vpnmurahjogja.my.id',33206).c('mi4','m14') +r = api.cmd('/system/scheduler/print', **{'?name': 'Family'}) +for x in r: + if isinstance(x,dict): + oe = x.get('on-event','') + i = oe.find('tool fetch') + print('FAMILY fetch portion:') + print(oe[i:i+600]) + print() + print('has reply_markup:', 'reply_markup' in oe) + print('has reset_:', 'reset_' in oe) + print('has .$name.:', '. $name .' in oe or '" . $name .' in oe) +api.close() diff --git a/verify-notif.py b/verify-notif.py new file mode 100644 index 0000000..9b7db53 --- /dev/null +++ b/verify-notif.py @@ -0,0 +1,82 @@ +import socket, hashlib +class R: + def __init__(s,h,p=8728): s.h=h; s.p=p; s.s=None + def c(s,u,p): + s.s=socket.socket(); s.s.settimeout(30) + s.s.connect((s.h,s.p)) + s._ws('/login','=name='+u,'=password='+p) + r=s._rr() + if any(isinstance(x,tuple) for x in r): + for x in r: + if isinstance(x,dict) and 'ret' in x: ch=x['ret'] + if ch and len(ch)==32: + hh=hashlib.md5(b'\x00'+p.encode()+bytes.fromhex(ch)).hexdigest() + s._ws('/login','=name='+u,'=response=00'+hh); s._rr() + return s + def _el(s,l): + if l<0x80: return bytes([l]) + elif l<0x4000: l|=0x8000; return bytes([(l>>8)&0xFF,l&0xFF]) + else: return bytes([0xC0|((l>>16)&0x0F),(l>>8)&0xFF,l&0xFF]) if l<0x200000 else bytes([0xE0|((l>>24)&0x07),(l>>16)&0xFF,(l>>8)&0xFF,l&0xFF]) + def _ww(s,w): s.s.sendall(s._el(len(w))+w.encode()) + def _ws(s,c,*ws): s._ww(c); [s._ww(w) for w in ws]; s.s.sendall(b'\x00') + def _rw(s): + f=s.s.recv(1) + if f==b'\x00': return None + lb=f[0]; l=lb + if lb&0x80: + if (lb&0xC0)==0x80: l=((lb&0x3F)<<8)+s.s.recv(1)[0] + elif (lb&0xE0)==0xC0: l=((lb&0x1F)<<16)+(s.s.recv(1)[0]<<8)+s.s.recv(1)[0] + else: l=((lb&0x0F)<<24)+(s.s.recv(1)[0]<<16)+(s.s.recv(1)[0]<<8)+s.s.recv(1)[0] + d=b'' + while len(d)=3 else '' + else: d[w]='' + rs.append(d) + elif r=='!trap': rs.append(('!trap',t)) + return rs + def cmd(s,c,**a): + ws=[c] + for k,v in a.items(): + ws.append(('?' if k.startswith('?') else '=')+k+'='+v) + s._ws(*ws) + return s._rr() + def close(s): + if s.s: s.s.close() + +api=R('remote.vpnmurahjogja.my.id',33206).c('mi4','m14') + +for name in ['Family', 'Bulanan2', 'Bulanan4']: + r = api.cmd('/system/scheduler/print', **{'?name': name}) + for x in r: + if isinstance(x,dict): + oe = x.get('on-event','') + tail = oe.split('limit-uptime ')[-1][:200] + print('=== %s (len %d) ===' % (name, len(oe))) + print(' contains telegram.org:', 'api.telegram.org' in oe) + print(' contains old 100.6:', '192.168.100.6' in oe) + print(' contains curlim dedup:', 'curlim' in oe) + print(' mode=https:', 'mode=https' in oe) + print(' tail:', tail) + print() + +api.close() diff --git a/verify-profiles.py b/verify-profiles.py new file mode 100644 index 0000000..8735ee9 --- /dev/null +++ b/verify-profiles.py @@ -0,0 +1,103 @@ +import socket, hashlib, re + +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') + +profiles = ["Bulanan2", "Bulanan4", "vc-2K3J", "vc-5K10J", "Family", "home", "DevB2"] + +print("=== ON-LOGIN VERIFICATION ===") +for p in profiles: + resp = api.cmd('/ip/hotspot/user/profile/print', **{'?name': p}) + if not resp: continue + ol = resp[0].get('on-login', '') + + real_pic = len(re.findall(r':pic(?!k)', ol)) + pick = ol.count(':pick') + pickk = ol.count(':pickk') + + status = 'ALL CORRECT' if real_pic == 0 and pickk == 0 else 'BROKEN' + print(f"{p}: {status} (pic_typo={real_pic} pick={pick} pickk={pickk})") + +api.close() diff --git a/verify-reset.py b/verify-reset.py new file mode 100644 index 0000000..64099b2 --- /dev/null +++ b/verify-reset.py @@ -0,0 +1,73 @@ +import socket, hashlib + +class R: + def __init__(self,h,p=8728): self.h=h; self.p=p; self.s=None + def c(self,u,p): + self.s=socket.socket(socket.AF_INET,socket.SOCK_STREAM); self.s.settimeout(15) + self.s.connect((self.h,self.p)) + self._ws('/login','=name='+u,'=password='+p) + r=self._rr() + if any(isinstance(x,tuple) for x in r): + for x in r: + if isinstance(x,dict) and 'ret' in x: ch=x['ret'] + if ch and len(ch)==32: + h=hashlib.md5(b'\x00'+p.encode()+bytes.fromhex(ch)).hexdigest() + self._ws('/login','=name='+u,'=response=00'+h) + self._rr() + return self + def _el(self,l): + if l<0x80: return bytes([l]) + elif l<0x4000: l|=0x8000; return bytes([(l>>8)&0xFF,l&0xFF]) + else: return bytes([0xC0|((l>>16)&0x0F),(l>>8)&0xFF,l&0xFF]) if l<0x200000 else bytes([0xE0|((l>>24)&0x07),(l>>16)&0xFF,(l>>8)&0xFF,l&0xFF]) + def _ww(self,w): self.s.sendall(self._el(len(w))+w.encode('utf-8')) + def _ws(self,c,*ws): self._ww(c); [self._ww(w) for w in ws]; self.s.sendall(b'\x00') + def _rw(self): + f=self._rb() + if f==b'\x00': return None + lb=f[0]; l=lb + if lb&0x80: + if (lb&0xC0)==0x80: l=((lb&0x3F)<<8)+self._rb()[0] + elif (lb&0xE0)==0xC0: l=((lb&0x1F)<<16)+(self._rb()[0]<<8)+self._rb()[0] + else: l=((lb&0x0F)<<24)+(self._rb()[0]<<16)+(self._rb()[0]<<8)+self._rb()[0] + d=b'' + while len(d)=3 else '' + else: d[w]='' + rs.append(d) + elif r=='!trap': rs.append(('!trap',t)) + return rs + def cmd(self,c,**a): + ws=[c] + for k,v in a.items(): ws.append(('?' if k.startswith('?') else '=')+k+'='+v) + self._ws(*ws) + return self._rr() + def close(self): + if self.s: self.s.close(); self.s=None + +api=R('192.168.100.2').c('mi4','m14') +for u in api.cmd('/ip/hotspot/user/print', **{'?name': 'test_reset_family'}): + print(" name=%s uptime=%s comment=%s" % (u.get('name',''), u.get('limit-uptime',''), u.get('comment','')[:30])) + if u.get('uptime'): + print(" uptime_usage=%s bytes_in=%s bytes_out=%s" % (u.get('uptime',''), u.get('bytes-in',''), u.get('bytes-out',''))) +api.close() diff --git a/verify-scheduler.py b/verify-scheduler.py new file mode 100644 index 0000000..ea8b781 --- /dev/null +++ b/verify-scheduler.py @@ -0,0 +1,98 @@ +import socket, hashlib +class R: + def __init__(self,h,p=8728): self.h=h; self.p=p; self.s=None + def c(self,u,p): + self.s=socket.socket(); self.s.settimeout(15) + self.s.connect((self.h,self.p)) + self._ws('/login','=name='+u,'=password='+p) + r=self._rr() + if any(isinstance(x,tuple) for x in r): + for x in r: + if isinstance(x,dict) and 'ret' in x: ch=x['ret'] + if ch and len(ch)==32: + h=hashlib.md5(b'\x00'+p.encode()+bytes.fromhex(ch)).hexdigest() + self._ws('/login','=name='+u,'=response=00'+h); self._rr() + return self + def _el(self,l): + if l<0x80: return bytes([l]) + elif l<0x4000: l|=0x8000; return bytes([(l>>8)&0xFF,l&0xFF]) + else: return bytes([0xC0|((l>>16)&0x0F),(l>>8)&0xFF,l&0xFF]) if l<0x200000 else bytes([0xE0|((l>>24)&0x07),(l>>16)&0xFF,(l>>8)&0xFF,l&0xFF]) + def _ww(self,w): self.s.sendall(self._el(len(w))+w.encode()) + def _ws(self,c,*ws): self._ww(c); [self._ww(w) for w in ws]; self.s.sendall(b'\x00') + def _rw(self): + f=self.s.recv(1) + if f==b'\x00': return None + lb=f[0]; l=lb + if lb&0x80: + if (lb&0xC0)==0x80: l=((lb&0x3F)<<8)+self.s.recv(1)[0] + elif (lb&0xE0)==0xC0: l=((lb&0x1F)<<16)+(self.s.recv(1)[0]<<8)+self.s.recv(1)[0] + else: l=((lb&0x0F)<<24)+(self.s.recv(1)[0]<<16)+(self.s.recv(1)[0]<<8)+self.s.recv(1)[0] + d=b'' + while len(d)=3 else '' + else: d[w]='' + rs.append(d) + elif r=='!trap': rs.append(('!trap',t)) + return rs + def cmd(self,c,**a): + ws=[c] + for k,v in a.items(): + ws.append(('?' if k.startswith('?') else '=')+k+'='+v) + self._ws(*ws) + return self._rr() + def close(self): + if self.s: self.s.close() + +api=R('192.168.100.2').c('mi4','m14') + +# FULL on-event Family +for si in api.cmd('/system/scheduler/print'): + if si['name'] == 'Family': + oe = si.get('on-event','') + print("=== Family on-event (FULL) ===") + print(oe) + print("=== END ===") + break + +# Check tg-cb-poll +for si in api.cmd('/system/scheduler/print'): + if 'tg-cb' in si['name'].lower(): + print("\ntg-cb-poll: %s" % si) + break +else: + print("\ntg-cb-poll NOT FOUND - creating...") + api.cmd('/system/scheduler/add', + name='tg-cb-poll', + interval='00:01:00', + **{'on-event': ':delay 10s;/tool fetch url="http://192.168.100.6/process/telegram-cb.php?key=msr-2026-reset" http-method=get keep-result=no', + 'start-time': 'startup'}) + print("Created!") + for si in api.cmd('/system/scheduler/print'): + if si['name'] == 'tg-cb-poll': + print(" %s" % si) + +# Also clean up test user +api.cmd('/ip/hotspot/user/remove', **{'numbers': [x['.id'] for x in api.cmd('/ip/hotspot/user/print', **{'?name': 'test_reset_family'})][0]}) +print("\nCleaned up test user") + +api.close() diff --git a/voucher-fix.py b/voucher-fix.py new file mode 100644 index 0000000..fc87fbe --- /dev/null +++ b/voucher-fix.py @@ -0,0 +1,93 @@ +import socket, hashlib + +class R: + def __init__(self,h,p=8728): self.h=h; self.p=p; self.s=None + def c(self,u,p): + self.s=socket.socket(socket.AF_INET,socket.SOCK_STREAM); self.s.settimeout(10) + self.s.connect((self.h,self.p)) + self._ws('/login','=name='+u,'=password='+p) + r=self._rr() + if any(isinstance(x,tuple) and x[0]=='!trap' for x in r): + for x in r: + if isinstance(x,dict) and 'ret' in x: ch=x['ret'] + if ch and len(ch)==32: + h=hashlib.md5(b'\x00'+p.encode()+bytes.fromhex(ch)).hexdigest() + self._ws('/login','=name='+u,'=response=00'+h) + self._rr() + return self + def _el(self,l): + if l<0x80: return bytes([l]) + elif l<0x4000: l|=0x8000; return bytes([(l>>8)&0xFF,l&0xFF]) + elif l<0x200000: l|=0xC00000; return bytes([(l>>16)&0xFF,(l>>8)&0xFF,l&0xFF]) + elif l<0x10000000: l|=0xE0000000; return bytes([(l>>24)&0xFF,(l>>16)&0xFF,(l>>8)&0xFF,l&0xFF]) + else: return bytes([0xF0,(l>>24)&0xFF,(l>>16)&0xFF,(l>>8)&0xFF,l&0xFF]) + def _ww(self,w): self.s.sendall(self._el(len(w))+w.encode('utf-8')) + def _ws(self,c,*ws): self._ww(c); [self._ww(w) for w in ws]; self.s.sendall(b'\x00') + def _rb(self): return self.s.recv(1) + def _rw(self): + f=self._rb() + if f==b'\x00': return None + lb=f[0] + if lb&0x80: + if (lb&0xC0)==0x80: l=((lb&0x3F)<<8)+self._rb()[0] + elif (lb&0xE0)==0xC0: l=((lb&0x1F)<<16)+(self._rb()[0]<<8)+self._rb()[0] + elif (lb&0xF0)==0xE0: l=((lb&0x0F)<<24)+(self._rb()[0]<<16)+(self._rb()[0]<<8)+self._rb()[0] + else: l=(self._rb()[0]<<24)+(self._rb()[0]<<16)+(self._rb()[0]<<8)+self._rb()[0] + else: l=lb + d=b'' + while len(d)=3 else '' + else: d[w]='' + rs.append(d) + elif r=='!trap': rs.append(('!trap',t)) + return rs + def cmd(self,c,**a): + ws=[c] + for k,v in a.items(): + if k.startswith('?'): ws.append(f'{k}={v}') + else: ws.append(f'={k}={v}') + self._ws(*ws) + return self._rr() + def close(self): + if self.s: self.s.close(); self.s=None + +api=R('192.168.100.2').c('mi4','m14') + +# Cari user labibah_tes +for u in api.cmd('/ip/hotspot/user/print', **{'?name': 'labibah_tes'}): + uid = u['.id'] + print(f"Ditemukan: {u['name']} pass={u.get('password')} comment={u.get('comment')}") + + # Ganti password = username (voucher style) + api.cmd('/ip/hotspot/user/set', **{'.id': uid, 'password': 'labibah_tes'}) + print("Password diubah -> labibah_tes") + break + +# Verify +for u in api.cmd('/ip/hotspot/user/print', **{'?name': 'labibah_tes'}): + print(f"Verify: user={u['name']} pass={u.get('password')} mac={u.get('mac-address')}") + +api.close()