# ============================================================ # Cloud Hotspot Manager — RouterOS 6.x เท่านั้น # ถ้าเครื่องเป็น RouterOS 7 ให้กลับไปเลือกแท็บ RouterOS 7.x # ============================================================ /system identity set name="{{IDENTITY}}" # ลบเฉพาะรายการที่ระบบนี้สร้างไว้ ห้ามแตะ RADIUS ที่ผู้ดูแลตั้งเอง # ของเดิมสั่ง remove numbers=[/radius find] ซึ่งลบทิ้งทั้งหมด รวมของคนอื่นด้วย /radius remove numbers=[/radius find where comment="cloud-hotspot"] /radius add service=hotspot address={{SERVER}} secret={{SECRET}} timeout=3s comment="cloud-hotspot" # วางทับต้องลบตัวจับเวลาของเดิมทุกชื่อ ไม่ใช่แค่ชื่อตัวเอง # ถ้าเครื่องนี้เคยวางสคริปต์ v7 มาก่อน จะเหลือ cloudsync ค้างอยู่ # สองตัวยิงเข้าเซิร์ฟเวอร์พร้อมกันทุกนาที ตั้งค่าทับกันไปมาและ log เต็ม /system scheduler remove numbers=[/system scheduler find name="kickuser"] /system scheduler remove numbers=[/system scheduler find name="cloudsync"] /system schedule add name=kickuser on-event=":global nas [/sys id get name]\n\r\n:global nas [/sys id get name]\n\r\n:global version [/system resource get version]\n\r\n:global version [:pick \$version 0 [:find \$version \" \"]]\n\r\n:global model [/system resource get board-name]\n\r\n:global msp [:find \$model \" \"]\n\r\n:if ([:typeof \$msp] = \"num\") do={:set model [:pick \$model 0 \$msp]}\n\r\n:global cpu [/system resource get cpu-load]\n\r\n:global cputemp 0\n\r\n:do {:set cputemp [/system health get cpu-temperature]} on-error={}\n\r\n:global hardwaretemp 0\n\r\n:do {:set hardwaretemp [/system health get temperature]} on-error={}\n\r\n:global uptime [/system resource get uptime]\n\r\n:global useractive [/ip hotspot active print count-only]\n\r\n:delay 3\n\r\n/tool fetch url=\"https://{{HOST}}/fetch/index/{{TOKEN}}/\$model/\$version/\$cpu/\$uptime/\$useractive/\$cputemp/\$hardwaretemp\" mode=http dst-path=disconnect.rsc;\n\r\ndelay 3;\n\r\n/import disconnect.rsc;\n\r\ndelay 3;\n\r\n/file remove disconnect.rsc" interval=1m /ip hotspot profile set use-radius=yes radius-accounting=yes login-by=http-chap,http-pap,mac-cookie numbers=[/ip hotspot profile find name=hsprof1] # ต้องลบของเดิมก่อนเพิ่มเสมอ และต้องอยู่ติดกันแบบนี้ # ของเดิมเพิ่มอย่างเดียวไม่เคยลบ วางสคริปต์ซ้ำกี่ครั้งก็พอกขึ้นเท่านั้น # จนฮอตสปอตเพี้ยนและลูกค้าเข้าหน้าล็อกอินไม่ได้ # ข้ามรายการ dynamic ด้วย เพราะฮอตสปอตสร้างเองและลบไม่ได้ ถ้าสั่งลบจะ error ทั้งบรรทัด /ip hotspot walled-garden ip remove numbers=[/ip hotspot walled-garden ip find where comment="cloud-hotspot" and !dynamic] /ip hotspot walled-garden remove numbers=[/ip hotspot walled-garden find where comment="cloud-hotspot" and !dynamic] /ip hotspot walled-garden ip add action=accept disabled=no dst-address="{{SERVER}}" comment="cloud-hotspot" /ip hotspot walled-garden add action=allow disabled=no dst-host="{{HOST}}" comment="cloud-hotspot" # จำนวนเครื่องต่อหนึ่งคูปองให้เซิร์ฟเวอร์เป็นคนคุม เราเตอร์เปิดกว้างไว้ # ของเดิมตั้งเฉพาะ numbers=0 ซึ่งอาจไม่ใช่โปรไฟล์ที่ฮอตสปอตใช้จริง :foreach up in=[/ip hotspot user profile find] do={ /ip hotspot user profile set $up shared-users=10 } if ([/file print count-only where name="flash"]=1) do={/file set flash/hotspot/login.html contents="<html><head><title>...</title></head><body>\$(if chap-id)<noscript><center><b>JavaScript required. Enable JavaScript to continue.</b></center></noscript>\$(endif)<center>If you are not redirected in a few seconds, click \"continue\" below<br><form name=\"redirect\" action=\"https://{{HOST}}/auth/login.php\" method=\"post\"><input type=\"hidden\" name=\"mac\" value=\"\$(mac)\"><input type=\"hidden\" name=\"ip\" value=\"\$(ip)\"><input type=\"hidden\" name=\"username\" value=\"\$(username)\"><input type=\"hidden\" name=\"link-login\" value=\"\$(link-login)\"><input type=\"hidden\" name=\"link-logout\" value=\"\$(link-logout)\"><input type=\"hidden\" name=\"link-orig\" value=\"\$(link-orig)\"><input type=\"hidden\" name=\"error\" value=\"\$(error)\"><input type=\"hidden\" name=\"chap-id\" value=\"\$(chap-id)\"><input type=\"hidden\" name=\"chap-challenge\" value=\"\$(chap-challenge)\"><input type=\"hidden\" name=\"link-login-only\" value=\"\$(link-login-only)\"><input type=\"hidden\" name=\"link-orig-esc\" value=\"\$(link-orig-esc)\"><input type=\"hidden\" name=\"mac-esc\" value=\"\$(mac-esc)\"><input type=\"hidden\" name=\"identity\" value=\"\$(identity)\"><input type=\"submit\" value=\"continue\"></form><script language=\"JavaScript\">document.redirect.submit();</script></center></body></html>";} else {/file set hotspot/login.html contents="<html><head><title>...</title></head><body>\$(if chap-id)<noscript><center><b>JavaScript required. Enable JavaScript to continue.</b></center></noscript>\$(endif)<center>If you are not redirected in a few seconds, click \"continue\" below<br><form name=\"redirect\" action=\"https://{{HOST}}/auth/login.php\" method=\"post\"><input type=\"hidden\" name=\"mac\" value=\"\$(mac)\"><input type=\"hidden\" name=\"ip\" value=\"\$(ip)\"><input type=\"hidden\" name=\"username\" value=\"\$(username)\"><input type=\"hidden\" name=\"link-login\" value=\"\$(link-login)\"><input type=\"hidden\" name=\"link-logout\" value=\"\$(link-logout)\"><input type=\"hidden\" name=\"link-orig\" value=\"\$(link-orig)\"><input type=\"hidden\" name=\"error\" value=\"\$(error)\"><input type=\"hidden\" name=\"chap-id\" value=\"\$(chap-id)\"><input type=\"hidden\" name=\"chap-challenge\" value=\"\$(chap-challenge)\"><input type=\"hidden\" name=\"link-login-only\" value=\"\$(link-login-only)\"><input type=\"hidden\" name=\"link-orig-esc\" value=\"\$(link-orig-esc)\"><input type=\"hidden\" name=\"mac-esc\" value=\"\$(mac-esc)\"><input type=\"hidden\" name=\"identity\" value=\"\$(identity)\"><input type=\"submit\" value=\"continue\"></form><script language=\"JavaScript\">document.redirect.submit();</script></center></body></html>";} if ([/file print count-only where name="flash"]=1) do={/file set flash/hotspot/logout.html contents="<html><head><title></title><meta http-equiv=\"Content-Type\" content=text/html; charset=utf-8\"><meta http-equiv=\"refresh\" content=\"0; URL=\$(link-login)\"></body></html>";} else {/file set hotspot/logout.html contents="<html><head><title></title><meta http-equiv=\"Content-Type\" content=text/html; charset=utf-8\"><meta http-equiv=\"refresh\" content=\"0; URL=\$(link-login)\"></body></html>";}
# ============================================================ # Cloud Hotspot Manager - RESET # Removes ONLY what this system created, so the setup script # can be pasted again on a clean slate. # # SAFE: it does NOT touch # - the hotspot server itself or its address pool # - your IP addresses, bridges, DHCP, firewall rules # - RADIUS clients you added yourself # - walled-garden entries you added yourself # It removes only entries tagged comment="cloud-hotspot", # this system's schedulers, and the files it downloaded. # # NOTE: ASCII only on purpose. Some RouterOS terminals truncate # a paste containing non-ASCII text. # # Paste this, then paste the setup script again. # ============================================================ :put "=== Cloud Hotspot Manager: reset start ===" # ---------- stop the heartbeat first ---------- # do this before anything else, or the scheduler fires mid-reset # and puts the config back while we are still removing it :foreach n in=[/system scheduler find name="cloudsync"] do={/system scheduler remove $n} :foreach n in=[/system scheduler find name="kickuser"] do={/system scheduler remove $n} :put "schedulers removed" # ---------- files this system downloaded ---------- :foreach f in=[/file find name="cloudsync.rsc"] do={:do {/file remove $f} on-error={}} :foreach f in=[/file find name="disconnect.rsc"] do={:do {/file remove $f} on-error={}} :foreach f in=[/file find name="t.rsc"] do={:do {/file remove $f} on-error={}} :put "leftover files removed" # ---------- our RADIUS client ---------- :foreach n in=[/radius find where comment="cloud-hotspot"] do={/radius remove $n} :put "radius client removed" # ---------- our walled-garden entries ---------- # dynamic ones are created by the hotspot itself and cannot be removed; # they disappear on their own once the static entry that spawned them is gone. # # removed one at a time on purpose. the old one-liner filtered with # [find comment="..." and !dynamic] # wrapped in :do ... on-error={} , so if that query failed to parse on some # RouterOS build, NOTHING was removed and the script still printed "removed". # a silent lie is worse than an error. now each entry is checked by reading the # property, which parses the same on every build, and the count is printed so # you can see whether it actually did anything. :local wgN 0 :foreach w in=[/ip hotspot walled-garden ip find] do={ :local c "" :do {:set c [/ip hotspot walled-garden ip get $w comment]} on-error={:set c ""} :if ($c = "cloud-hotspot") do={ :do {/ip hotspot walled-garden ip remove $w; :set wgN ($wgN + 1)} on-error={} } } :foreach w in=[/ip hotspot walled-garden find] do={ :local c "" :do {:set c [/ip hotspot walled-garden get $w comment]} on-error={:set c ""} :if ($c = "cloud-hotspot") do={ :do {/ip hotspot walled-garden remove $w; :set wgN ($wgN + 1)} on-error={} } } :put ("walled-garden entries removed: " . $wgN) # ---------- our bypass devices ---------- :local bpN 0 :foreach b in=[/ip hotspot ip-binding find] do={ :local c "" :do {:set c [/ip hotspot ip-binding get $b comment]} on-error={:set c ""} :if ($c = "cloud-hotspot") do={ :do {/ip hotspot ip-binding remove $b; :set bpN ($bpN + 1)} on-error={} } } :put ("bypass devices removed: " . $bpN) # ---------- warn about OTHER cloud hotspot systems ---------- # this reset only removes what THIS system created. routers taken over from # another provider often still run that provider's scheduler, which rewrites # the radius servers and the hotspot profiles every few minutes and quietly # undoes this setup. the reset cannot delete those for you - they are not ours # to delete - but it must not stay silent about them either. # :find returns nothing (not -1) when the text is absent, so comparing it with # a number can abort the whole block. :typeof is the safe test on every build. :local foreign 0 :foreach r in=[/radius find] do={ :local c "" :do {:set c [/radius get $r comment]} on-error={:set c ""} :local svc "" :do {:set svc [:tostr [/radius get $r service]]} on-error={:set svc ""} :if (($c != "cloud-hotspot") and ([:typeof [:find $svc "hotspot"]] = "num")) do={ :set foreign ($foreign + 1) :local adr "" :do {:set adr [:tostr [/radius get $r address]]} on-error={:set adr "?"} :put ("WARNING: another radius server also serves hotspot: " . $adr) :put (" comment=" . $c) } } :if ($foreign > 0) do={ :put " hotspot logins may be sent to that server instead of ours." :put " remove it only if that provider is no longer used on this router." } # ---------- live state ---------- # cookies and host entries remember devices that logged in through this system. # leaving them behind makes a freshly pasted setup look broken: # devices neither get the login page nor get online. :do {/ip hotspot cookie remove [find]} on-error={} :do {/ip hotspot active remove [find]} on-error={} :do {/ip hotspot host remove [find]} on-error={} :put "sessions, cookies and host cache cleared" # ---------- hotspot profile back to stock ---------- # only the switches this system turns on. login-by keeps cookie/mac-cookie # so remembered devices on OTHER systems are not affected. :foreach hs in=[/ip hotspot find] do={ :local pf [/ip hotspot get $hs profile] :do { /ip hotspot profile set $pf use-radius=no radius-accounting=no :local cur [/ip hotspot profile get $pf login-by] :local keep [:toarray ""] :foreach x in=$cur do={:if ($x != "mac") do={:set keep ($keep,$x)}} :if ([:len $keep] = 0) do={:set keep [:toarray "http-chap,http-pap"]} /ip hotspot profile set $pf login-by=$keep :put ("hotspot profile reset: " . $pf) } on-error={:put ("could not reset profile " . $pf)} } # ---------- login page back to the MikroTik default ---------- # the setup script overwrites login.html / logout.html. # resetting them avoids a half-old half-new page after re-pasting. # this rewrites the hotspot html folder with MikroTik's stock files. :foreach hs in=[/ip hotspot find] do={ :do {/ip hotspot reset-html [/ip hotspot get $hs profile]} on-error={ :put "note: could not reset html automatically" :put " run this by hand if the login page looks wrong:" :put " /ip hotspot reset-html" } } :put "=== reset done - now paste the setup script again ===" :put "after pasting, check: /log print where message~\"cloudsync\""
# ============================================================ # Cloud Hotspot Manager - CLEAN SLATE # Wipes every hotspot automation on this router so the setup # script can be pasted onto a clean machine. # # WHY THIS EXISTS # -------------------------------------------------------- # Routers taken over from another provider keep running that # provider's scheduler. It re-downloads its config every minute # and rewrites the radius servers and the hotspot profiles, so # our setup silently stops working a few minutes after install. # Removing the scheduler alone is not enough: the scheduler # re-downloads and re-imports its own script when the script is # missing. Order matters - scheduler first, then script, then # files. # # SAFETY # -------------------------------------------------------- # Deny by default. It removes ONLY names listed in the KILL # lines below. Anything else on the router - your own monitoring # agents, your firewall, your WAN config, your netwatch - is not # touched and cannot be touched, even by accident. # Names in KEEP are refused outright, as a second safety net. # # HOW TO USE # -------------------------------------------------------- # 1. Paste as-is. DRYRUN is on: it only PRINTS what it would # remove and changes nothing. Read that list. # 2. If the list looks right, change DRYRUN to false (one word, # first line below) and paste again. Now it removes. # 3. Paste the setup script from the </> button. # # NOTE: ASCII only on purpose. Some RouterOS terminals drop or # truncate a paste containing non-ASCII text. # ============================================================ :local DRYRUN true # ---------- what to remove ---------- # comma-delimited on both ends. add a name between commas to extend. # "ours" = cloudsync, kickuser. "theirs" = vr-fetch-report. :local KILLSCHED ",cloudsync,kickuser,vr-fetch-report," :local KILLSCRIPT ",cloudsync,kickuser,vr-fetch-report," :local KILLFILE ",cloudsync.rsc,disconnect.rsc,t.rsc,vr-fb.rsc," # ---------- what must never be removed ---------- # checked before every removal, overrides the KILL lines above. :local KEEP ",monitor-push,si-agent," # radius clients: remove ours by comment, and any whose comment # contains this text (the other provider tags its own entry). :local KILLRADCMT ",cloud-hotspot," :local KILLRADTXT "VR" # walled-garden: remove ours by comment, and any pointing here. :local KILLWGTXT "vrservicenetwork" # ============================================================ :local TAG "DRY RUN - would remove" :if ($DRYRUN = false) do={:set TAG "removed"} :put "=== Cloud Hotspot Manager: clean slate ===" :if ($DRYRUN) do={ :put " DRY RUN. Nothing will be changed." :put " Set DRYRUN to false at the top and paste again to apply." } :put "" # ---------- 1. schedulers FIRST ---------- # before anything else: a scheduler that fires mid-wipe puts the # config back while we are still removing it. :put "-- schedulers --" :local nSch 0 :foreach s in=[/system scheduler find] do={ :local nm "" :do {:set nm [/system scheduler get $s name]} on-error={:set nm ""} :local kill false :if ([:typeof [:find $KILLSCHED ("," . $nm . ",")]] = "num") do={:set kill true} :if ([:typeof [:find $KEEP ("," . $nm . ",")]] = "num") do={:set kill false} :if ($kill) do={ :set nSch ($nSch + 1) :put (" " . $TAG . " scheduler: " . $nm) :if ($DRYRUN = false) do={:do {/system scheduler remove $s} on-error={:put (" FAILED: " . $nm)}} } } :if ($nSch = 0) do={:put " none"} # ---------- 2. scripts ---------- # only now, with nothing left to run them. :put "-- scripts --" :local nScr 0 :foreach s in=[/system script find] do={ :local nm "" :do {:set nm [/system script get $s name]} on-error={:set nm ""} :local kill false :if ([:typeof [:find $KILLSCRIPT ("," . $nm . ",")]] = "num") do={:set kill true} :if ([:typeof [:find $KEEP ("," . $nm . ",")]] = "num") do={:set kill false} :if ($kill) do={ :set nScr ($nScr + 1) :put (" " . $TAG . " script: " . $nm) :if ($DRYRUN = false) do={:do {/system script remove $s} on-error={:put (" FAILED: " . $nm)}} } } :if ($nScr = 0) do={:put " none"} # ---------- 3. downloaded files ---------- :put "-- leftover files --" :local nFil 0 :foreach f in=[/file find] do={ :local nm "" :do {:set nm [/file get $f name]} on-error={:set nm ""} :if ([:typeof [:find $KILLFILE ("," . $nm . ",")]] = "num") do={ :set nFil ($nFil + 1) :put (" " . $TAG . " file: " . $nm) :if ($DRYRUN = false) do={:do {/file remove $f} on-error={}} } } :if ($nFil = 0) do={:put " none"} # ---------- 4. radius clients ---------- # two radius servers on the hotspot service is the single most # common reason logins fail after a takeover: RouterOS tries them # in list order and the other provider rejects our users. :put "-- radius clients --" :local nRad 0 :foreach r in=[/radius find] do={ :local cmt "" :do {:set cmt [/radius get $r comment]} on-error={:set cmt ""} :local adr "" :do {:set adr [:tostr [/radius get $r address]]} on-error={:set adr "?"} :local kill false :if ([:typeof [:find $KILLRADCMT ("," . $cmt . ",")]] = "num") do={:set kill true} :if ([:typeof [:find $cmt $KILLRADTXT]] = "num") do={:set kill true} :if ($kill) do={ :set nRad ($nRad + 1) :put (" " . $TAG . " radius: " . $adr . " comment=" . $cmt) :if ($DRYRUN = false) do={:do {/radius remove $r} on-error={}} } } :if ($nRad = 0) do={:put " none"} # ---------- 5. walled-garden ---------- # dynamic entries belong to the hotspot itself and cannot be # removed; they disappear once the static entry that spawned them # is gone. removed one at a time so one failure cannot abort the # rest of the block. :put "-- walled-garden --" :local nWg 0 :foreach w in=[/ip hotspot walled-garden ip find] do={ :local cmt "" :do {:set cmt [/ip hotspot walled-garden ip get $w comment]} on-error={:set cmt ""} :if ($cmt = "cloud-hotspot") do={ :put (" " . $TAG . " walled-garden ip: comment=" . $cmt) :set nWg ($nWg + 1) :if ($DRYRUN = false) do={:do {/ip hotspot walled-garden ip remove $w} on-error={}} } } :foreach w in=[/ip hotspot walled-garden find] do={ :local cmt "" :do {:set cmt [/ip hotspot walled-garden get $w comment]} on-error={:set cmt ""} :local dh "" :do {:set dh [:tostr [/ip hotspot walled-garden get $w dst-host]]} on-error={:set dh ""} :local kill false :if ($cmt = "cloud-hotspot") do={:set kill true} :if ([:typeof [:find $dh $KILLWGTXT]] = "num") do={:set kill true} :if ($kill) do={ :put (" " . $TAG . " walled-garden: " . $dh . " comment=" . $cmt) :set nWg ($nWg + 1) :if ($DRYRUN = false) do={:do {/ip hotspot walled-garden remove $w} on-error={}} } } :if ($nWg = 0) do={:put " none"} # ---------- 6. our bypass devices ---------- :put "-- bypass devices (ours) --" :local nBp 0 :foreach b in=[/ip hotspot ip-binding find] do={ :local cmt "" :do {:set cmt [/ip hotspot ip-binding get $b comment]} on-error={:set cmt ""} :if ($cmt = "cloud-hotspot") do={ :set nBp ($nBp + 1) :put (" " . $TAG . " ip-binding: comment=" . $cmt) :if ($DRYRUN = false) do={:do {/ip hotspot ip-binding remove $b} on-error={}} } } :if ($nBp = 0) do={:put " none"} # ---------- 7. live state ---------- # cookies and host entries remember devices that logged in through # the old system. left behind, a freshly pasted setup looks broken: # devices neither get the login page nor get online. :put "-- sessions, cookies, host cache --" :if ($DRYRUN = false) do={ :do {/ip hotspot cookie remove [find]} on-error={} :do {/ip hotspot active remove [find]} on-error={} :do {/ip hotspot host remove [find]} on-error={} :put " cleared" } else={ :put " DRY RUN - would clear all hotspot cookies, active sessions and host cache" } # ---------- 8. hotspot profiles back to stock ---------- # only the switches a cloud system turns on. the setup script # turns the ones it needs back on by itself. :put "-- hotspot profiles --" :foreach hs in=[/ip hotspot find] do={ :local pf [/ip hotspot get $hs profile] :if ($DRYRUN = false) do={ :do { /ip hotspot profile set $pf use-radius=no radius-accounting=no :local cur [/ip hotspot profile get $pf login-by] :local keep [:toarray ""] :foreach x in=$cur do={:if ($x != "mac") do={:set keep ($keep,$x)}} :if ([:len $keep] = 0) do={:set keep [:toarray "http-chap,http-pap"]} /ip hotspot profile set $pf login-by=$keep :put (" reset: " . $pf) } on-error={:put (" FAILED to reset profile " . $pf)} } else={ :put (" DRY RUN - would reset profile " . $pf . " (use-radius=no, drop mac login)") } } # ---------- 9. login page back to the MikroTik default ---------- # the old system overwrote login.html / logout.html. resetting # avoids a half-old half-new page after pasting the setup script. :put "-- login page --" :if ($DRYRUN = false) do={ :foreach hs in=[/ip hotspot find] do={ :do {/ip hotspot reset-html [/ip hotspot get $hs profile]} on-error={ :put " could not reset html automatically - run /ip hotspot reset-html by hand" } } :put " reset to MikroTik default" } else={ :put " DRY RUN - would run /ip hotspot reset-html on every hotspot" } # ============================================================ # what is left that this script will NOT touch # these need a human decision, so they are reported, not removed # ============================================================ :put "" :put "=== review these yourself ===" # local hotspot users let someone log in WITHOUT our system. :local nU 0 :foreach u in=[/ip hotspot user find] do={:set nU ($nU + 1)} :if ($nU > 0) do={ :put ("local hotspot users still on this router: " . $nU) # printed up to 20 only. a takeover router can carry hundreds, and a wall of # names would push the rest of this report off the top of the terminal - # which is exactly how the scheduler list got lost the first time. :local shown 0 :foreach u in=[/ip hotspot user find] do={ :if ($shown < 20) do={ :do {:put (" user: " . [/ip hotspot user get $u name])} on-error={} :set shown ($shown + 1) } } :if ($nU > 20) do={:put (" ... and " . ($nU - 20) . " more - see /ip hotspot user print")} :put " these bypass the cloud system. remove by hand if they are not yours:" :put " /ip hotspot user remove [find name=\"THE-NAME\"]" } # bypass entries left by the old system let devices skip the portal. :local nB2 0 :foreach b in=[/ip hotspot ip-binding find] do={:set nB2 ($nB2 + 1)} :if ($nB2 > 0) do={ :put ("bypass entries (ip-binding) still on this router: " . $nB2) :put " check with: /ip hotspot ip-binding print detail" } # any other radius still serving hotspot would steal our logins. :foreach r in=[/radius find] do={ :local svc "" :do {:set svc [:tostr [/radius get $r service]]} on-error={:set svc ""} :if ([:typeof [:find $svc "hotspot"]] = "num") do={ :do {:put ("WARNING: radius still serving hotspot: " . [:tostr [/radius get $r address]])} on-error={} } } # leftover template files from the old portal. harmless - our login # page does not reference them - but listed so nothing is a surprise. :foreach hs in=[/ip hotspot find] do={ :local dir [/ip hotspot profile get [/ip hotspot get $hs profile] html-directory] :foreach x in={"api.json";"css";"img/user.svg";"img/password.svg"} do={ :local p ($dir . "/" . $x) :if ([:len [/file find name=$p]] > 0) do={ :put ("old portal leftover (harmless): " . $p) } } } :put "" :if ($DRYRUN) do={ :put "=== DRY RUN finished - nothing was changed ===" :put " set DRYRUN to false at the top and paste again to apply" } else={ :put "=== clean slate done ===" :put " now paste the setup script from the </> button" :put " then check: /log print where message~\"cloudsync\"" }
# ============================================================ # Cloud Hotspot Manager - RouterOS 7.x # Paste the whole block into Terminal once. Safe to re-run. # # A hotspot server must exist first. If not, run: # /ip hotspot setup # ...then paste this script. # # NOTE: ASCII only on purpose. Some RouterOS terminals truncate a # paste containing non-ASCII text, which leaves the scheduler # half-written and the heartbeat never runs. # ============================================================ :put "=== Cloud Hotspot Manager: start ===" # ---------- require a hotspot server ---------- :if ([:len [/ip hotspot find]] = 0) do={ :put "!! No hotspot server found. Run /ip hotspot setup first, then paste this again." :error "no hotspot server" } /system identity set name="{{IDENTITY}}" # ---------- RADIUS ---------- /radius remove [find comment="cloud-hotspot"] /radius add service=hotspot address={{SERVER}} secret="{{SECRET}}" timeout=3s comment="cloud-hotspot" /radius incoming set accept=yes port=3799 # ---------- hotspot profile actually in use ---------- # (not [find default=yes] - many boxes use a profile named hsprof1) :foreach hs in=[/ip hotspot find] do={ :local pf [/ip hotspot get $hs profile] /ip hotspot profile set $pf use-radius=yes radius-accounting=yes \ radius-interim-update=received nas-port-type=wireless-802.11 \ login-by=http-pap,mac-cookie :put ("hotspot profile configured: " . $pf) } # simultaneous devices are enforced by the server, keep the router open :foreach up in=[/ip hotspot user profile find] do={ /ip hotspot user profile set $up shared-users=10 keepalive-timeout=2m idle-timeout=5m } # ---------- walled garden ---------- /ip hotspot walled-garden ip remove [find comment="cloud-hotspot" and !dynamic] /ip hotspot walled-garden remove [find comment="cloud-hotspot" and !dynamic] /ip hotspot walled-garden ip add action=accept dst-address={{SERVER}} comment="cloud-hotspot" /ip hotspot walled-garden add action=allow dst-host="{{HOST}}" comment="cloud-hotspot" {{GARDEN}} # ---------- heartbeat every minute + pick up disconnect commands ---------- # remove leftovers from older scripts (kickuser errors every minute if left) /system scheduler remove [find name="kickuser"] :do {/file remove [find name="disconnect.rsc"]} on-error={} /system scheduler remove [find name="cloudsync"] /system scheduler add name=cloudsync interval=1m start-time=startup on-event=":local ver [/system resource get version]; :local sp [:find \$ver \" \"]; :if ([:typeof \$sp] = \"num\") do={:set ver [:pick \$ver 0 \$sp]}; :local mdl [/system resource get board-name]; :local cpu [/system resource get cpu-load]; :local upt [/system resource get uptime]; :local act 0; :do {:set act [/ip hotspot active print count-only]} on-error={}; :do {/file remove [find name=\"cloudsync.rsc\"]} on-error={}; :do {/tool fetch url=\"{{BASE}}/fetch/status/{{TOKEN}}\" http-method=post http-data=(\"model=\" . \$mdl . \"&version=\" . \$ver . \"&cpu=\" . \$cpu . \"&uptime=\" . \$upt . \"&active=\" . \$act) check-certificate=no dst-path=cloudsync.rsc; :delay 2s; :do {/import cloudsync.rsc} on-error={}; :do {/file remove [find name=\"cloudsync.rsc\"]} on-error={}; :log info \"cloudsync: sent ok\"} on-error={:log warning \"cloudsync: cannot reach server - check internet and walled-garden\"}" # ---------- point the hotspot login page at the cloud portal ---------- # write to EVERY hotspot on the box, not just the last one. # a router with two hotspots on different html-directories used to get the # portal page on only one of them; clients on the other stayed on the router's # own login page at its local ip and hit a certificate warning there. :local done [:toarray ""] :local wrote 0 :foreach hs in=[/ip hotspot find] do={ :local dir [/ip hotspot profile get [/ip hotspot get $hs profile] html-directory] :if ([:len $dir] = 0) do={:set dir "hotspot"} :if ([:typeof [:find $done $dir]] = "nil") do={ :set done ($done,$dir) :local lp ($dir . "/login.html") :local lo ($dir . "/logout.html") :if ([:len [/file find name=$lp]] = 0) do={ :put ("!! " . $lp . " not found. Run /ip hotspot reset-html then paste this script again.") } else={ /file set [find name=$lp] contents="<html><head><title>...</title><meta name=\"viewport\" content=\"width=device-width,initial-scale=1\"></head><body>\$(if chap-id)<noscript><center><b>JavaScript required. Enable JavaScript to continue.</b></center></noscript>\$(endif)<center>Redirecting...<br><form name=\"redirect\" action=\"{{BASE}}/auth/login.php\" method=\"post\"><input type=\"hidden\" name=\"mac\" value=\"\$(mac)\"><input type=\"hidden\" name=\"ip\" value=\"\$(ip)\"><input type=\"hidden\" name=\"username\" value=\"\$(username)\"><input type=\"hidden\" name=\"link-login\" value=\"\$(link-login)\"><input type=\"hidden\" name=\"link-login-only\" value=\"\$(link-login-only)\"><input type=\"hidden\" name=\"link-orig\" value=\"\$(link-orig)\"><input type=\"hidden\" name=\"link-orig-esc\" value=\"\$(link-orig-esc)\"><input type=\"hidden\" name=\"error\" value=\"\$(error)\"><input type=\"hidden\" name=\"chap-id\" value=\"\$(chap-id)\"><input type=\"hidden\" name=\"chap-challenge\" value=\"\$(chap-challenge)\"><input type=\"hidden\" name=\"mac-esc\" value=\"\$(mac-esc)\"><input type=\"hidden\" name=\"identity\" value=\"\$(identity)\"><input type=\"submit\" value=\"continue\"></form><script language=\"JavaScript\">document.redirect.submit();</script></center></body></html>" :set wrote ($wrote + 1) :put ("login page written: " . $lp) } :if ([:len [/file find name=$lo]] > 0) do={ /file set [find name=$lo] contents="<html><head><meta http-equiv=\"refresh\" content=\"0; url=\$(link-login)\"></head><body></body></html>" } } } :put ("hotspot dirs configured: " . $wrote) # ---------- check: the hotspot network must hand out a DNS server ---------- # without it the client gets an address and nothing else. no resolver means the # device never sends a DNS query, never sends an HTTP request, and so the # hotspot never gets anything to redirect: the login page simply never appears. # the device sits there looking connected, and nothing shows up in the log. # seen in the field on a router where the hotspot wizard was run with the DNS # field left blank, so this check makes it visible the moment the script runs. :foreach hs in=[/ip hotspot find] do={ :local pf [/ip hotspot get $hs profile] :local ha "" :do {:set ha [:tostr [/ip hotspot profile get $pf hotspot-address]]} on-error={:set ha ""} :if ($ha != "") do={ :foreach n in=[/ip dhcp-server network find] do={ :do { :local net [/ip dhcp-server network get $n address] :if ([:toip $ha] in $net) do={ :local ds "" :do {:set ds [:tostr [/ip dhcp-server network get $n dns-server]]} on-error={:set ds ""} :if ($ds = "") do={ :put "" :put ("!! PROBLEM: hotspot network " . [:tostr $net] . " hands out no DNS server.") :put "!! Clients get an address but the login page never appears." :put "!! Fix with this one line, then reconnect a client:" :put ("!! /ip dhcp-server network set [find address=" . [:tostr $net] . "] dns-server=" . $ha) :put "" } else={ :put ("hotspot network " . [:tostr $net] . " hands out DNS: " . $ds) } } } on-error={} } } } # ---------- send status now instead of waiting a minute ---------- :do {/system scheduler run cloudsync} on-error={} :delay 3s :put "=== done - open the Router page in the web UI and check status ===" # ---------- troubleshooting ---------- # /system scheduler print detail where name=cloudsync # /log print where message~"cloudsync" # /radius monitor 0 # /ip hotspot active print # manual test from the router: # /tool fetch url="{{BASE}}/fetch/status/{{TOKEN}}" http-method=post http-data="model=manual&version=7&cpu=1&uptime=1d00:00:00&active=0&cputemp=0&hwtemp=0" check-certificate=no dst-path=t.rsc # /log print where topics~"script"