|
| 1 | +<!doctype html> |
| 2 | +<html lang="fa" dir="rtl"> |
| 3 | +<head> |
| 4 | +<meta charset="utf-8"> |
| 5 | +<meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover"> |
| 6 | +<title>اسکنر IP تمیز</title> |
| 7 | +<script src="https://telegram.org/js/telegram-web-app.js"></script> |
| 8 | +<link href="https://fonts.googleapis.com/css2?family=Vazirmatn:wght@400;500;600;700;800&display=swap" rel="stylesheet"> |
| 9 | +<style> |
| 10 | +:root{ |
| 11 | + --bg:#06100d; --bg2:#0a1814; --card:#0e201a; --card2:#12281f; --border:#1c3a30; |
| 12 | + --text:#e8fff6; --muted:#7fae9e; --dim:#4f7c6c; |
| 13 | + --teal:#00e5a0; --teal2:#10b981; --cyan:#22d3ee; --amber:#fbbf24; --red:#f87171; |
| 14 | + --grad:linear-gradient(135deg,#00e5a0,#06b6d4); |
| 15 | +} |
| 16 | +*{box-sizing:border-box} |
| 17 | +html,body{margin:0;min-height:100%} |
| 18 | +body{font-family:'Vazirmatn',system-ui,sans-serif;color:var(--text);font-size:14px; |
| 19 | + background:radial-gradient(900px 500px at 50% -10%, rgba(0,229,160,.12), transparent 60%), var(--bg); |
| 20 | + padding:14px 14px calc(120px + env(safe-area-inset-bottom));-webkit-font-smoothing:antialiased} |
| 21 | +.muted{color:var(--muted)}.dim{color:var(--dim)} |
| 22 | +button{font-family:inherit;cursor:pointer} |
| 23 | + |
| 24 | +.head{display:flex;align-items:center;gap:11px;margin-bottom:16px} |
| 25 | +.logo{width:44px;height:44px;border-radius:14px;background:var(--grad);display:grid;place-items:center; |
| 26 | + box-shadow:0 10px 26px -8px rgba(0,229,160,.7)} |
| 27 | +.logo svg{width:24px;height:24px;color:#04130d} |
| 28 | +.head h1{font-size:18px;margin:0;font-weight:800} |
| 29 | +.head p{margin:0;font-size:12px;color:var(--dim)} |
| 30 | + |
| 31 | +.steps{display:flex;gap:6px;margin-bottom:16px} |
| 32 | +.steps .s{flex:1;height:4px;border-radius:4px;background:var(--card2)} |
| 33 | +.steps .s.on{background:var(--grad)} |
| 34 | + |
| 35 | +.card{background:linear-gradient(180deg,var(--card),var(--bg2));border:1px solid var(--border); |
| 36 | + border-radius:18px;padding:16px;margin-bottom:14px} |
| 37 | +.card h3{margin:0 0 12px;font-size:15px;font-weight:800;display:flex;align-items:center;gap:8px} |
| 38 | +.row{display:flex;gap:10px} |
| 39 | +.field{flex:1} |
| 40 | +.field label{display:block;font-size:12px;color:var(--muted);font-weight:700;margin-bottom:6px} |
| 41 | +.field input,.field select{width:100%;background:var(--bg);border:1px solid var(--border);border-radius:12px; |
| 42 | + padding:12px 13px;color:var(--text);font-family:inherit;font-size:14px} |
| 43 | +.chips{display:flex;flex-wrap:wrap;gap:7px;margin-top:6px} |
| 44 | +.chip{background:var(--card2);border:1px solid var(--border);border-radius:10px;padding:7px 12px;font-size:12.5px;font-weight:700;color:var(--muted)} |
| 45 | +.chip.on{background:var(--grad);color:#04130d;border-color:transparent} |
| 46 | + |
| 47 | +.btn{border:0;border-radius:14px;padding:15px;font-weight:800;font-size:15px;width:100%;color:#04130d;background:var(--grad); |
| 48 | + box-shadow:0 14px 30px -12px rgba(0,229,160,.8);display:flex;align-items:center;justify-content:center;gap:9px} |
| 49 | +.btn.sec{background:var(--card2);color:var(--text);border:1px solid var(--border);box-shadow:none} |
| 50 | +.btn:disabled{opacity:.55} |
| 51 | +.btn.sm{padding:11px;font-size:13px;border-radius:12px} |
| 52 | + |
| 53 | +.progress{height:10px;border-radius:10px;background:var(--card2);overflow:hidden;margin:14px 0 8px} |
| 54 | +.progress>i{display:block;height:100%;background:var(--grad);width:0;transition:width .25s} |
| 55 | +.statline{display:flex;justify-content:space-between;font-size:12.5px;color:var(--muted);font-weight:700} |
| 56 | + |
| 57 | +.result{display:flex;align-items:center;gap:12px;padding:12px;border-radius:13px;background:var(--card2);border:1px solid var(--border);margin-bottom:9px} |
| 58 | +.result .rank{width:30px;height:30px;border-radius:9px;background:var(--bg);display:grid;place-items:center;font-weight:800;color:var(--teal);flex:none;font-size:13px} |
| 59 | +.result .ip{font-weight:700;font-variant-numeric:tabular-nums;letter-spacing:.2px} |
| 60 | +.result .ms{margin-inline-start:auto;font-weight:800;font-size:14px} |
| 61 | +.ms.good{color:var(--teal)}.ms.mid{color:var(--amber)}.ms.bad{color:var(--red)} |
| 62 | +.result .copybtn{width:34px;height:34px;border-radius:9px;background:var(--bg);border:1px solid var(--border);display:grid;place-items:center;color:var(--muted);flex:none} |
| 63 | + |
| 64 | +textarea{width:100%;min-height:120px;background:var(--bg);border:1px solid var(--border);border-radius:12px; |
| 65 | + padding:12px;color:var(--text);font-family:ui-monospace,Menlo,monospace;font-size:12px;resize:vertical;direction:ltr} |
| 66 | +.note{font-size:11.5px;color:var(--dim);margin-top:8px;line-height:1.7} |
| 67 | + |
| 68 | +.bottombar{position:fixed;inset-inline:0;bottom:0;padding:12px 14px calc(12px + env(safe-area-inset-bottom)); |
| 69 | + background:linear-gradient(0deg,var(--bg) 70%,transparent);display:flex;gap:10px} |
| 70 | +.hide{display:none!important} |
| 71 | +.toast{position:fixed;inset-inline:0;bottom:96px;margin:auto;width:max-content;max-width:90vw;background:var(--card2); |
| 72 | + border:1px solid var(--border);border-radius:12px;padding:11px 18px;font-weight:700;z-index:60;opacity:0;transform:translateY(8px);transition:.25s} |
| 73 | +.toast.show{opacity:1;transform:none} |
| 74 | +.empty{text-align:center;color:var(--dim);padding:30px 10px;font-weight:600} |
| 75 | +.spin{width:16px;height:16px;border:2px solid rgba(4,19,13,.3);border-top-color:#04130d;border-radius:50%;animation:sp .7s linear infinite} |
| 76 | +@keyframes sp{to{transform:rotate(360deg)}} |
| 77 | +@media(prefers-reduced-motion:reduce){.spin{animation:none}} |
| 78 | +</style> |
| 79 | +</head> |
| 80 | +<body> |
| 81 | + |
| 82 | +<div class="head"> |
| 83 | + <div class="logo"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M2 12h4l3 8 4-16 3 8h4"/></svg></div> |
| 84 | + <div><h1>اسکنر IP تمیز</h1><p>پیدا کردن بهترین IP کلودفلر برای کانفیگها</p></div> |
| 85 | +</div> |
| 86 | + |
| 87 | +<div class="steps"><div class="s on" id="st1"></div><div class="s" id="st2"></div><div class="s" id="st3"></div></div> |
| 88 | + |
| 89 | +<section id="step1"> |
| 90 | + <div class="card"> |
| 91 | + <h3>⚙️ تنظیمات اسکن</h3> |
| 92 | + <div class="row"> |
| 93 | + <div class="field"><label>پورت</label> |
| 94 | + <select id="port"><option>443</option><option>2053</option><option>2083</option><option>2087</option><option>2096</option><option>8443</option></select> |
| 95 | + </div> |
| 96 | + <div class="field"><label>تعداد IP برای تست</label> |
| 97 | + <select id="count"><option value="40">۴۰</option><option value="80" selected>۸۰</option><option value="150">۱۵۰</option><option value="300">۳۰۰</option></select> |
| 98 | + </div> |
| 99 | + </div> |
| 100 | + <div style="margin-top:12px"><label class="muted" style="font-size:12px;font-weight:700">حداکثر تأخیر قابلقبول</label> |
| 101 | + <div class="chips" id="maxms"> |
| 102 | + <span class="chip" data-v="200">۲۰۰ms</span> |
| 103 | + <span class="chip on" data-v="400">۴۰۰ms</span> |
| 104 | + <span class="chip" data-v="800">۸۰۰ms</span> |
| 105 | + <span class="chip" data-v="3000">همه</span> |
| 106 | + </div> |
| 107 | + </div> |
| 108 | + <p class="note">اسکن کاملاً داخل همین اپ و روی شبکهی شما انجام میشه — هیچ سروری در کار نیست. نتایج به اتصال فعلی شما بستگی داره.</p> |
| 109 | + </div> |
| 110 | + <div class="card"> |
| 111 | + <h3>📊 وضعیت</h3> |
| 112 | + <div class="progress"><i id="bar"></i></div> |
| 113 | + <div class="statline"><span id="scanState">آماده برای اسکن</span><span id="scanCount">۰ / ۰</span></div> |
| 114 | + <div class="statline" style="margin-top:6px"><span class="dim">سالم پیدا شده</span><span id="foundCount" style="color:var(--teal)">۰</span></div> |
| 115 | + </div> |
| 116 | +</section> |
| 117 | + |
| 118 | +<section id="step2" class="hide"> |
| 119 | + <div class="card"> |
| 120 | + <h3>✅ بهترین IPها <span id="resCount" class="muted" style="font-weight:600;font-size:12px"></span></h3> |
| 121 | + <div id="results"></div> |
| 122 | + </div> |
| 123 | +</section> |
| 124 | + |
| 125 | +<section id="step3" class="hide"> |
| 126 | + <div class="card"> |
| 127 | + <h3>🛠 ساخت کانفیگ با IP تمیز</h3> |
| 128 | + <label class="muted" style="font-size:12px;font-weight:700">کانفیگهات رو بذار (هر کدوم یک خط: vless/vmess/trojan/ss)</label> |
| 129 | + <textarea id="cfgIn" placeholder="vless://... vmess://... trojan://..."></textarea> |
| 130 | + <div class="row" style="margin-top:10px"> |
| 131 | + <button class="btn sm sec" id="useBest">استفاده از بهترین IP</button> |
| 132 | + <button class="btn sm" id="buildBtn">ساخت کانفیگها</button> |
| 133 | + </div> |
| 134 | + </div> |
| 135 | + <div class="card" id="outCard" style="display:none"> |
| 136 | + <h3>📤 خروجی</h3> |
| 137 | + <textarea id="cfgOut" readonly></textarea> |
| 138 | + <div class="row" style="margin-top:10px"> |
| 139 | + <button class="btn sm sec" id="copyClean">کپی بدون پینگ</button> |
| 140 | + <button class="btn sm" id="copyPing">کپی با پینگ</button> |
| 141 | + </div> |
| 142 | + </div> |
| 143 | +</section> |
| 144 | + |
| 145 | +<div class="bottombar" id="bar1"> |
| 146 | + <button class="btn" id="scanBtn"><svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="#04130d" stroke-width="2.4"><circle cx="11" cy="11" r="7"/><path d="M21 21l-4-4"/></svg> شروع اسکن</button> |
| 147 | +</div> |
| 148 | +<div class="bottombar hide" id="bar2"> |
| 149 | + <button class="btn sec" id="rescan">اسکن دوباره</button> |
| 150 | + <button class="btn" id="toOpt">ساخت کانفیگ ←</button> |
| 151 | +</div> |
| 152 | +<div class="bottombar hide" id="bar3"> |
| 153 | + <button class="btn sec" id="backRes">← نتایج</button> |
| 154 | +</div> |
| 155 | + |
| 156 | +<div class="toast" id="toast"></div> |
| 157 | + |
| 158 | +<script> |
| 159 | +const tg = window.Telegram && window.Telegram.WebApp; |
| 160 | +if(tg){ try{ tg.ready(); tg.expand(); }catch(e){} } |
| 161 | + |
| 162 | +const $=s=>document.querySelector(s), $$=s=>document.querySelectorAll(s); |
| 163 | +const fa=n=>String(n).replace(/[0-9]/g,d=>"۰۱۲۳۴۵۶۷۸۹"[d]); |
| 164 | +let toastT; function toast(m){const t=$("#toast");t.textContent=m;t.classList.add("show");clearTimeout(toastT);toastT=setTimeout(()=>t.classList.remove("show"),2000);if(tg&&tg.HapticFeedback)try{tg.HapticFeedback.impactOccurred('light')}catch(e){}} |
| 165 | + |
| 166 | +const CF_FALLBACK=["173.245.48.0/20","103.21.244.0/22","103.22.200.0/22","103.31.4.0/22","141.101.64.0/18","108.162.192.0/18","190.93.240.0/20","188.114.96.0/20","197.234.240.0/22","198.41.128.0/17","162.158.0.0/15","104.16.0.0/13","104.24.0.0/14","172.64.0.0/13","131.0.72.0/22"]; |
| 167 | +async function getRanges(){ |
| 168 | + try{ |
| 169 | + const r=await fetch("https://www.cloudflare.com/ips-v4",{cache:"no-store"}); |
| 170 | + const t=await r.text(); const list=t.split("\n").map(s=>s.trim()).filter(s=>/^\d+\.\d+\.\d+\.\d+\/\d+$/.test(s)); |
| 171 | + return list.length?list:CF_FALLBACK; |
| 172 | + }catch(e){ return CF_FALLBACK; } |
| 173 | +} |
| 174 | +const ipToInt=ip=>ip.split(".").reduce((a,o)=>(a<<8>>>0)+(+o),0)>>>0; |
| 175 | +const intToIp=n=>[24,16,8,0].map(s=>(n>>>s)&255).join("."); |
| 176 | +function randomIPs(ranges,n){ |
| 177 | + const out=new Set(); let guard=0; |
| 178 | + while(out.size<n && guard++<n*40){ |
| 179 | + const cidr=ranges[Math.floor(Math.random()*ranges.length)]; |
| 180 | + const [base,bits]=cidr.split("/"); const size=2**(32-(+bits)); |
| 181 | + const ip=intToIp((ipToInt(base)+Math.floor(Math.random()*size))>>>0); |
| 182 | + if(!ip.endsWith(".0")&&!ip.endsWith(".255")) out.add(ip); |
| 183 | + } |
| 184 | + return [...out]; |
| 185 | +} |
| 186 | + |
| 187 | +function pingIP(ip,port,timeout=2200){ |
| 188 | + return new Promise(resolve=>{ |
| 189 | + const ctrl=new AbortController(); const t0=performance.now(); |
| 190 | + const to=setTimeout(()=>ctrl.abort(),timeout); |
| 191 | + fetch(`https://${ip}:${port}/cdn-cgi/trace`,{mode:"no-cors",cache:"no-store",signal:ctrl.signal}) |
| 192 | + .then(()=>{clearTimeout(to);resolve(Math.round(performance.now()-t0));}) |
| 193 | + .catch(err=>{clearTimeout(to); if(err.name==="AbortError") resolve(null); else resolve(Math.round(performance.now()-t0));}); |
| 194 | + }); |
| 195 | +} |
| 196 | +async function pool(items,worker,concurrency,onTick){ |
| 197 | + let i=0,done=0; const res=[]; |
| 198 | + async function run(){ while(i<items.length){ const idx=i++; res[idx]=await worker(items[idx]); onTick(++done); } } |
| 199 | + await Promise.all(Array.from({length:Math.min(concurrency,items.length)},run)); |
| 200 | + return res; |
| 201 | +} |
| 202 | + |
| 203 | +let FOUND=[], MAXMS=400, scanning=false; |
| 204 | +$("#maxms").addEventListener("click",e=>{const c=e.target.closest(".chip");if(!c)return;$$("#maxms .chip").forEach(x=>x.classList.remove("on"));c.classList.add("on");MAXMS=+c.dataset.v;}); |
| 205 | + |
| 206 | +function setStep(n){ |
| 207 | + [1,2,3].forEach(i=>{$("#step"+i).classList.toggle("hide",i!==n);$("#bar"+i).classList.toggle("hide",i!==n);$("#st"+i).classList.toggle("on",i<=n);}); |
| 208 | + if(tg){ try{ if(n>1){tg.BackButton.show();}else{tg.BackButton.hide();} }catch(e){} } |
| 209 | + window.scrollTo(0,0); |
| 210 | +} |
| 211 | + |
| 212 | +async function startScan(){ |
| 213 | + if(scanning) return; scanning=true; FOUND=[]; |
| 214 | + const port=$("#port").value, count=+$("#count").value; |
| 215 | + $("#scanBtn").disabled=true; $("#scanBtn").innerHTML='<span class="spin"></span> در حال اسکن…'; |
| 216 | + $("#scanState").textContent="در حال آمادهسازی…"; $("#bar").style.width="0%"; |
| 217 | + const ranges=await getRanges(); |
| 218 | + const ips=randomIPs(ranges,count); |
| 219 | + $("#scanState").textContent="در حال تست IPها…"; |
| 220 | + await pool(ips,async ip=>{ |
| 221 | + const ms=await pingIP(ip,port,2200); |
| 222 | + if(ms!=null && ms<=MAXMS) FOUND.push({ip,ms}); |
| 223 | + return ms; |
| 224 | + },24,(done)=>{ |
| 225 | + $("#bar").style.width=(done/ips.length*100)+"%"; |
| 226 | + $("#scanCount").textContent=fa(done)+" / "+fa(ips.length); |
| 227 | + $("#foundCount").textContent=fa(FOUND.length); |
| 228 | + }); |
| 229 | + FOUND.sort((a,b)=>a.ms-b.ms); |
| 230 | + scanning=false; $("#scanBtn").disabled=false; |
| 231 | + $("#scanBtn").innerHTML='<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="#04130d" stroke-width="2.4"><circle cx="11" cy="11" r="7"/><path d="M21 21l-4-4"/></svg> شروع اسکن'; |
| 232 | + $("#scanState").textContent="اسکن کامل شد"; |
| 233 | + renderResults(); |
| 234 | + if(FOUND.length) setStep(2); else toast("IP سالمی پیدا نشد — حد تأخیر رو بیشتر کن یا دوباره بزن"); |
| 235 | +} |
| 236 | +function msClass(ms){return ms<=200?"good":ms<=500?"mid":"bad";} |
| 237 | +function renderResults(){ |
| 238 | + $("#resCount").textContent="("+fa(FOUND.length)+" مورد)"; |
| 239 | + const top=FOUND.slice(0,40); |
| 240 | + $("#results").innerHTML = top.length? top.map((r,i)=>` |
| 241 | + <div class="result"> |
| 242 | + <span class="rank">${fa(i+1)}</span> |
| 243 | + <span class="ip" dir="ltr">${r.ip}</span> |
| 244 | + <span class="ms ${msClass(r.ms)}">${fa(r.ms)}ms</span> |
| 245 | + <button class="copybtn" data-ip="${r.ip}"><svg width="15" height="15" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><rect x="9" y="9" width="11" height="11" rx="2"/><path d="M5 15V5a2 2 0 0 1 2-2h10"/></svg></button> |
| 246 | + </div>`).join("") : `<div class="empty">چیزی پیدا نشد.</div>`; |
| 247 | +} |
| 248 | +$("#results").addEventListener("click",e=>{const b=e.target.closest("[data-ip]");if(!b)return;copy(b.dataset.ip);toast("کپی شد: "+b.dataset.ip);}); |
| 249 | + |
| 250 | +function replaceHost(link,addr,port){ |
| 251 | + link=link.trim(); |
| 252 | + if(/^(vless|trojan):\/\//.test(link)){ |
| 253 | + return link.replace(/^((?:vless|trojan):\/\/[^@]+@)([^:\/?#]+)(?::(\d+))?/,(m,p,h,pt)=>`${p}${addr}:${port||pt||443}`); |
| 254 | + } |
| 255 | + if(link.startsWith("vmess://")){ |
| 256 | + try{const o=JSON.parse(atob(link.slice(8).replace(/-/g,'+').replace(/_/g,'/')+"=="));o.add=addr;if(port)o.port=String(port);return "vmess://"+btoa(JSON.stringify(o));}catch(e){return null;} |
| 257 | + } |
| 258 | + if(link.startsWith("ss://")){ |
| 259 | + const m=link.match(/^(ss:\/\/[^@]+@)([^:\/?#]+)(?::(\d+))?(.*)$/); |
| 260 | + if(m) return `${m[1]}${addr}:${port||m[3]||443}${m[4]}`; |
| 261 | + } |
| 262 | + return null; |
| 263 | +} |
| 264 | +function tagPing(link,ms){ |
| 265 | + if(link.startsWith("vmess://")){try{const o=JSON.parse(atob(link.slice(8)+"=="));o.ps=(o.ps||"node")+` (${ms}ms)`;return "vmess://"+btoa(JSON.stringify(o));}catch(e){return link;}} |
| 266 | + return link.includes("#")? link+encodeURIComponent(` (${ms}ms)`) : link+"#"+encodeURIComponent(`clean (${ms}ms)`); |
| 267 | +} |
| 268 | +let OUT_CLEAN=[], OUT_PING=[]; |
| 269 | +function build(){ |
| 270 | + const lines=$("#cfgIn").value.split("\n").map(s=>s.trim()).filter(Boolean); |
| 271 | + if(!lines.length){toast("اول کانفیگ بذار");return;} |
| 272 | + if(!FOUND.length){toast("اول اسکن کن");return;} |
| 273 | + const port=$("#port").value, ips=FOUND.slice(0,10); |
| 274 | + OUT_CLEAN=[];OUT_PING=[]; |
| 275 | + for(const ln of lines){ for(const r of ips){ const nl=replaceHost(ln,r.ip,port); if(nl){OUT_CLEAN.push(nl);OUT_PING.push(tagPing(nl,r.ms));} } } |
| 276 | + if(!OUT_CLEAN.length){toast("کانفیگ معتبری نبود");return;} |
| 277 | + $("#outCard").style.display="block"; |
| 278 | + $("#cfgOut").value=OUT_PING.join("\n"); |
| 279 | + toast(fa(OUT_CLEAN.length)+" کانفیگ ساخته شد"); |
| 280 | +} |
| 281 | +function copy(t){ if(navigator.clipboard) navigator.clipboard.writeText(t).catch(()=>{}); else {const a=document.createElement("textarea");a.value=t;document.body.appendChild(a);a.select();document.execCommand("copy");a.remove();} } |
| 282 | + |
| 283 | +$("#scanBtn").onclick=startScan; |
| 284 | +$("#rescan").onclick=()=>setStep(1); |
| 285 | +$("#toOpt").onclick=()=>setStep(3); |
| 286 | +$("#backRes").onclick=()=>setStep(2); |
| 287 | +$("#useBest").onclick=()=>{ if(FOUND[0]) toast("بهترین IP: "+FOUND[0].ip); }; |
| 288 | +$("#buildBtn").onclick=build; |
| 289 | +$("#copyClean").onclick=()=>{copy(OUT_CLEAN.join("\n"));toast("کپی شد (بدون پینگ)");}; |
| 290 | +$("#copyPing").onclick=()=>{copy(OUT_PING.join("\n"));toast("کپی شد (با پینگ)");}; |
| 291 | +if(tg){ try{ tg.BackButton.onClick(()=>{ const cur=$("#step3").classList.contains("hide")?($("#step2").classList.contains("hide")?1:2):3; setStep(Math.max(1,cur-1)); }); }catch(e){} } |
| 292 | +setStep(1); |
| 293 | +</script> |
| 294 | +</body> |
| 295 | +</html> |
0 commit comments