[
  {
    "path": "index.html",
    "content": "<!doctype html>\n<html lang=\"zh-CN\">\n\n<head>\n    <meta charset=\"utf-8\">\n    <title>在线 Clash 检测</title>\n    <link rel=\"manifest\" href=\"manifest.json\">\n\n    <link rel=\"stylesheet\" href=\"styles.css\">\n    <meta name=\"viewport\" content=\"width=device-width, initial-scale=1\">\n</head>\n\n<body>\n    <div class=\"wrapper\">\n        <header>\n            <h1 id=\"title\">您是否在使用 Clash？</h1>\n            <p>任何网站都能检测您是否正在使用 Clash。</p>\n            <ul>\n                <li style=\"width:120px\"><a href=\"javascript:startScan()\">Start Scan <strong>开始检测</strong></a></li>\n                <li><a href=\"https://github.com/MikeWang000000/ClashScan\">View On <strong>GitHub</strong></a></li>\n            </ul>\n        </header>\n        <div class=\"progress\"><div class=\"progress-done\"></div></div>\n        <section>\n            <h2>使用说明</h2>\n            <ul>\n                <li>推荐使用 Chrome ⩾ 103 版本（2022）；</li>\n                <li>点击 “开始检测”，您将同意本网页进行端口扫描；</li>\n                <li>您本次的检测结果不会被上传至云端；</li>\n                <li>检测结果仅供参考。</li>\n            </ul>\n\n            <hr />\n\n            <h2>检测结果</h2>\n            <div id=\"div_version\">\n                <h3>Clash 版本：</h3>\n                <p id=\"txt_version\">\n                    暂无数据\n                </p>\n            </div>\n\n            <div id=\"div_hosts\" style=\"display: none\">\n                <h3>您正在浏览：</h3>\n                <blockquote>\n                    <p id=\"txt_hosts\">\n                        bilibili.com<br />\n                        bilibili.com<br />\n                        bilibili.com<br />\n                    </p>\n                </blockquote>\n            </div>\n\n            <div id=\"div_servers\" style=\"display: none\">\n                <h3>您的服务器：</h3>\n                <pre class=\"\"><code id=\"txt_servers\"></code></pre>\n            </div>\n\n            <hr />\n        </section>\n    </div>\n    <footer>\n        <p>Project maintained by <a href=\"https://github.com/MikeWang000000\">MikeWang000000</a></p>\n        <p>Hosted on GitHub Pages &mdash; Theme by <a href=\"https://github.com/orderedlist\">orderedlist</a></p>\n    </footer>\n    <script src=\"script.js\"></script>\n</body>\n\n</html>\n"
  },
  {
    "path": "manifest.json",
    "content": "{\n    \"name\": \"Clash Scan\",\n    \"short_name\": \"ClashScan\",\n    \"start_url\": \"ClashScan/index.html\",\n    \"display\": \"standalone\",\n    \"background_color\": \"#ffffff\",\n    \"theme_color\": \"#000000\",\n    \"icons\": [\n      {\n        \"src\": \"/ClashScan/icons/icon-192x192.png\",\n        \"sizes\": \"192x192\",\n        \"type\": \"image/png\"\n      },\n      {\n        \"src\": \"/ClashScan/icons/icon-512x512.png\",\n        \"sizes\": \"512x512\",\n        \"type\": \"image/png\"\n      }\n    ]\n  }"
  },
  {
    "path": "scannerWorker.js",
    "content": "// scannerWorker.js\nself.onmessage = async function (e) {\n    const { port, timeout } = e.data;\n    let foundPort = 0;\n\n    const controller = new AbortController();\n    const signal = controller.signal;\n\n    async function checkIsClash(port) {\n        try {\n            const response = await fetch(\n                \"http://127.0.0.1:\" + port,\n                { method: \"GET\", signal: signal }\n            );\n            const dat = await response.json();\n            if (Object.keys(dat).length === 1 && (dat.message === \"Unauthorized\" || dat.hello)) {\n                return true;\n            }\n            return false;\n        } catch (error) {\n            if (error.name === 'AbortError') {\n                console.log('Fetch aborted');\n            }\n            return false;\n        }\n    }\n\n    const timeoutId = setTimeout(() => controller.abort(), timeout);\n\n    if (await checkIsClash(port)) {\n        foundPort = port;\n    }\n\n    clearTimeout(timeoutId);\n    self.postMessage({ foundPort, port });\n};\n\nself.onterminate = function () {\n    controller.abort();\n};"
  },
  {
    "path": "script.js",
    "content": "if ('serviceWorker' in navigator) {\n    window.addEventListener('load', () => {\n      navigator.serviceWorker.register('service-worker.js')\n        .then(registration => {\n          console.log('ServiceWorker registration successful with scope: ', registration.scope);\n        }, error => {\n          console.log('ServiceWorker registration failed: ', error);\n        });\n    });\n  }\n\n\nif (!AbortSignal.timeout) {\n    AbortSignal.timeout = function (ms) {\n        const controller = new AbortController();\n        setTimeout(() => controller.abort(new DOMException(\"TimeoutError\")), ms);\n        return controller.signal;\n    };\n}\n\nfunction avg (arr) {\n    let sum = 0;\n    arr.forEach((k) => { sum += k; })\n    return sum / arr.length;\n}\n\nfunction shuffle(array) {\n    for (let i = array.length - 1; i >= 0; i--) {\n        const j = Math.floor(Math.random() * (i + 1));\n        [array[i], array[j]] = [array[j], array[i]];\n    }\n    return array;\n}\n\nfunction randArr(start, end, length) {\n    return shuffle(range(start, end)).slice(0, length);\n}\n\nfunction range(start, end) {\n    return Array.from({ length: end - start }, (v, i) => i + start);\n}\n\nfunction sleep(ms) {\n    return new Promise((r) => setTimeout(r, ms));\n}\n\nasync function portTime(port) {\n    const st = performance.now();\n    try {\n        await fetch(\n            \"http://127.0.0.1:\" + port,\n            { signal: AbortSignal.timeout(3000) }\n        );\n    } catch (error) {}\n    const et = performance.now();\n    return et - st;\n}\n\nasync function guessOpenProxyPort() {\n    const promList = [];\n    const randTime = [];\n    const msThresh = 300;\n\n    randArr(41000, 49000, 8).forEach((port) => {\n        promList.push(portTime(port));\n    });\n    const prom7890 = portTime(7890);\n    const prom7897 = portTime(7897);\n\n    for (const prom of promList) {\n        randTime.push(await prom);\n    }\n    closedAvg = avg(randTime);\n\n    if (Math.abs(await prom7890 - closedAvg) > msThresh) {\n        return 7890;\n    }\n    if (Math.abs(await prom7897 - closedAvg) > msThresh) {\n        return 7897;\n    }\n    return 0;\n}\n\nasync function guessClashVersion(port) {\n    let hasError = false;\n    let version = \"Clash Core Unknown\";\n    async function pathExists(path) {\n        try {\n            const response = await fetch(\n                \"http://127.0.0.1:\" + port + path,\n                { method: \"GET\", signal: AbortSignal.timeout(500) }\n            );\n            if (response.ok || response.status === 401) {\n                return true;\n            }\n            return false;\n        } catch (error) {\n            hasError = true;\n            return false;\n        }\n    }\n    if (await pathExists(\"/memory\")) {\n        version = \"Clash Meta Core v1.14.4 ~ Latest (guessed)\";\n    } else if (await pathExists(\"/restart\")) {\n        version = \"Clash Meta Core v1.14.3 (guessed)\";\n    } else if (await pathExists(\"/dns\")) {\n        version = \"Clash Meta Core v1.14.2 (guessed)\";\n    } else if (await pathExists(\"/group\")) {\n        version = \"Clash Meta Core v1.12.0 ~ v1.14.1 (guessed)\";\n    } else if (await pathExists(\"/cache\")) {\n        version = \"Clash Meta Core v1.10.0 ~ v1.11.8 (guessed)\";\n    } else if (await pathExists(\"/script\")) {\n        version = \"Clash Meta Core v1.9.1 (guessed)\";\n    } else if (await pathExists(\"/providers/rules\")) {\n        version = \"Clash Meta Core v1.8.0 ~ v1.9.0 (guessed)\";\n    } else if (await pathExists(\"/providers/proxies\")) {\n        version = \"Clash Core v0.17.0 ~ Latest (guessed)\";\n    } else if (await pathExists(\"/version\")) {\n        version = \"Clash Core v0.16.0 (guessed)\";\n    } else {\n        version = \"Clash Core Unknown\";\n    }\n    if (hasError) {\n        version = \"Clash Core Unknown\";\n    }\n    return version;\n}\n\nasync function getClashVersion(port) {\n    try {\n        const response = await fetch(\n            \"http://127.0.0.1:\" + port + \"/version\",\n            { method: \"GET\", signal: AbortSignal.timeout(500) }\n        );\n        const dat = await response.json();\n        if (response.ok) {\n            let brand = \"Clash Core\";\n            if (dat.meta) {\n                brand = \"Clash Meta Core\";\n            } else if (dat.premium) {\n                brand = \"Clash Core Premium\";\n            }\n            return brand + \" \" + dat.version;\n        }\n        return await guessClashVersion(port);\n    } catch (error) {\n        return \"Clash Core Unknown\";\n    }\n}\n\nasync function getClashTraffic(port) {\n    try {\n        const hostlist = [];\n        const response = await fetch(\n            \"http://127.0.0.1:\" + port + \"/connections\",\n            { method: \"GET\", signal: AbortSignal.timeout(500) }\n        );\n        const dat = await response.json();\n        if (Array.isArray(dat.connections)) {\n            dat.connections.forEach((conn) => {\n                let addr = conn.metadata.host ? conn.metadata.host : conn.metadata.destinationIP;\n                let port = parseInt(conn.metadata.destinationPort, 10);\n                if (port !== 80 && port !== 443) {\n                    addr += (\":\" + port);\n                }\n                hostlist.push(addr);\n            });\n            return hostlist;\n        }\n        return null;\n    } catch (error) {\n        return null;\n    }\n}\n\nasync function getClashProxies(port) {\n    try {\n        const response = await fetch(\n            \"http://127.0.0.1:\" + port + \"/proxies\",\n            { method: \"GET\", signal: AbortSignal.timeout(500) }\n        );\n        if (!response.ok) {\n            return null;\n        }\n        return await response.json();\n    } catch (error) {\n        return null;\n    }\n}\n\nasync function scanLocalhost(workerNum) {\n    window.scanning = true;\n    let proxyPort = 0;\n    let workerDone = 0;\n    let totalScannedPorts = 0;\n    let foundPort = 0;\n\n    const ports = [9090]\n        .concat(range(9091, 10000))\n        .concat(range(2000, 9090).reverse())\n        .concat(range(10000, 65536))\n        .concat(range(1, 2000).reverse());\n\n    const workers = [];\n    const commonPortLength = 3200;\n    let percentage = 0;\n    let currentPortIndex = 0;\n\n    function updateProgress() {\n        let preInfo = \"\";\n        if (proxyPort === 7890) {\n            preInfo = \"TCP/7890 开放 (Clash?) | \";\n        } else if (proxyPort === 7897) {\n            preInfo = \"TCP/7897 开放 (Clash Verge?) | \";\n        }\n        if (totalScannedPorts < commonPortLength) {\n            percentage = Math.round(100 * totalScannedPorts / commonPortLength);\n            document.querySelector(\"#title\").innerText =\n                preInfo + \"扫描中... \" + percentage + \"%\";\n            document.querySelector(\".progress-done\").style.width = percentage + \"%\";\n        } else {\n            percentage = Math.round(\n                100 * (totalScannedPorts - commonPortLength) / ports.length\n            );\n            document.querySelector(\"#title\").innerText =\n                preInfo + \"扩展扫描中... \" + percentage + \"%\";\n            document.querySelector(\".progress-done\").style.width = percentage + \"%\";\n        }\n    }\n\n    async function updateInfoWorker() {\n        let preInfo = \"\";\n        if (proxyPort === 7890) {\n            preInfo = \"TCP/7890 开放 (Clash?) | \";\n        } else if (proxyPort === 7897) {\n            preInfo = \"TCP/7897 开放 (Clash Verge?) | \";\n        }\n\n        document.querySelector(\"#txt_version\").innerText = \"暂无数据\";\n        document.querySelector(\"#txt_hosts\").innerHTML = \"\";\n        document.querySelector(\"#div_hosts\").style.display = \"none\";\n        document.querySelector(\"#txt_servers\").innerHTML = \"\";\n        document.querySelector(\"#div_servers\").style.display = \"none\";        \n\n        while (!foundPort && workerDone !== workerNum) {\n            updateProgress();\n            await sleep(500);\n        }\n        window.scanning = false;\n        document.querySelector(\".progress-done\").style.width = \"100%\";\n\n        if (foundPort) {\n            document.querySelector(\"#title\").innerText =\n                \"您在使用 Clash：TCP/\" + foundPort;\n            document.querySelector(\"#txt_version\").innerText =\n                await getClashVersion(foundPort);\n            const hostlist = await getClashTraffic(foundPort);\n            if (hostlist && Array.isArray(hostlist)) {\n                document.querySelector(\"#txt_hosts\").innerHTML = hostlist.join(\"<br/>\");\n                document.querySelector(\"#div_hosts\").style.display = \"inherit\";\n            }\n            const proxies = await getClashProxies(foundPort);\n            if (proxies) {\n                document.querySelector(\"#txt_servers\").innerHTML =\n                    JSON.stringify(proxies, null, 4);\n                document.querySelector(\"#div_servers\").style.display = \"inherit\";\n            }\n        } else if (proxyPort) {\n            document.querySelector(\"#title\").innerText = preInfo + \"扫描完毕\";\n        }\n        else {\n            document.querySelector(\"#title\").innerText = \"未发现 Clash\";\n        }\n    }\n\n\n\n    function terminateAllWorkers() {\n        workers.forEach(worker => worker.terminate());\n    }\n\n    updateInfoWorker();\n    proxyPort = await guessOpenProxyPort();\n\n    const portChunks = Array.from({ length: workerNum }, () => []);\n    \n    for (let i = 0; i < ports.length; i++) {\n        portChunks[i % workerNum].push(ports[i]);\n    }\n    \n    for (let wn = 0; wn < workerNum; wn++) {\n        const worker = new Worker('scannerWorker.js');\n        worker.onmessage = function (e) {\n            const { foundPort: port } = e.data;\n            if (port) {\n                foundPort = port;\n                terminateAllWorkers();\n            }\n            totalScannedPorts++;\n            if (portChunks[wn].length > 0) {\n                worker.postMessage({ port: portChunks[wn].shift(), timeout: 120 });\n            } else {\n                workerDone++;\n            }\n        };\n        workers.push(worker);\n        if (portChunks[wn].length > 0) {\n            worker.postMessage({ port: portChunks[wn].shift(), timeout: 120 });\n        }\n    }\n\n    // 确保在窗口关闭或刷新时终止所有 Web Worker\n    window.addEventListener('beforeunload', terminateAllWorkers);\n}\n\nfunction startScan() {\n    if (!AbortSignal || !AbortSignal.timeout) {\n        alert(\"您的浏览器版本较低，检测结果可能不准确。\");\n    }\n    if (window.scanning) {\n        alert(\"正在扫描中。\");\n    } else {\n        scanLocalhost(128);\n    }\n}"
  },
  {
    "path": "service-worker.js",
    "content": "const CACHE_NAME = 'clash-scan-cache-v1';\nconst urlsToCache = [\n  '/ClashScan/',\n  '/ClashScan/index.html',\n  '/ClashScan/styles.css',\n  '/ClashScan/script.js',\n  '/ClashScan/scannerWorker.js',\n  '/ClashScan/icons/icon-192x192.png',\n  '/ClashScan/icons/icon-512x512.png'\n];\n\nself.addEventListener('install', event => {\n  event.waitUntil(\n    caches.open(CACHE_NAME)\n      .then(cache => {\n        return Promise.all(\n          urlsToCache.map(url => {\n            return cache.add(url).catch(error => {\n              console.error(`Failed to cache ${url}:`, error);\n            });\n          })\n        );\n      })\n      .catch(error => {\n        console.error('Failed to open cache:', error);\n      })\n  );\n});\n\nself.addEventListener('fetch', event => {\n  const url = new URL(event.request.url);\n\n  // 检查是否为 127.0.0.1 且端口不是 80 或 443\n  if (url.hostname === '127.0.0.1' && url.port !== '80' && url.port !== '443') {\n    return; // 不缓存这些请求\n  }\n\n  event.respondWith(\n    caches.match(event.request)\n      .then(response => {\n        if (response) {\n          return response;\n        }\n        return fetch(event.request);\n      })\n  );\n});\n\nself.addEventListener('activate', event => {\n  const cacheWhitelist = [CACHE_NAME];\n  event.waitUntil(\n    caches.keys().then(cacheNames => {\n      return Promise.all(\n        cacheNames.map(cacheName => {\n          if (cacheWhitelist.indexOf(cacheName) === -1) {\n            return caches.delete(cacheName);\n          }\n        })\n      );\n    })\n  );\n});"
  },
  {
    "path": "styles.css",
    "content": "@import url(\"https://fonts.googleapis.com/css?family=Lato:300italic,700italic,300,700\");\n\nhtml {\n    background: #6C7989;\n    background: #6C7989 linear-gradient(#6C7989, #434B55) fixed;\n    height: 100%\n}\n\nbody {\n    padding: 50px 0;\n    margin: 0;\n    font: 14px/1.5 Lato, \"Helvetica Neue\", \"PingFang SC\", \"Noto Sans SC\", Helvetica, Arial, sans-serif;\n    color: #555;\n    font-weight: 300;\n    min-height: calc(100% - 100px)\n}\n\n.wrapper {\n    width: 800px;\n    margin: 0 auto;\n    background: #DEDEDE;\n    border-radius: 8px;\n    box-shadow: rgba(0, 0, 0, 0.2) 0 0 0 1px, rgba(0, 0, 0, 0.45) 0 3px 10px\n}\n\nheader,\nsection,\nfooter {\n    display: block\n}\n\na {\n    color: #069;\n    text-decoration: none\n}\n\np {\n    margin: 0 0 20px;\n    padding: 0\n}\n\nstrong {\n    color: #222;\n    font-weight: 700\n}\n\nheader {\n    border-radius: 8px 8px 0 0;\n    background: #C6EAFA;\n    background: linear-gradient(#DDFBFC, #C6EAFA);\n    position: relative;\n    padding: 15px 20px;\n    border-bottom: 1px solid #B2D2E1\n}\n\nheader h1 {\n    margin: 0;\n    padding: 0;\n    font-size: 24px;\n    line-height: 1.2;\n    color: #069;\n    text-shadow: rgba(255, 255, 255, 0.9) 0 1px 0\n}\n\nheader.without-description h1 {\n    margin: 10px 0\n}\n\nheader p {\n    margin: 0;\n    color: #61778B;\n    width: 300px;\n    font-size: 13px\n}\n\nheader p.view {\n    display: none;\n    font-weight: 700;\n    text-shadow: rgba(255, 255, 255, 0.9) 0 1px 0;\n    -webkit-font-smoothing: antialiased\n}\n\nheader p.view a {\n    color: #06c\n}\n\nheader p.view small {\n    font-weight: 400\n}\n\nheader ul {\n    margin: 0;\n    padding: 0;\n    list-style: none;\n    position: absolute;\n    z-index: 1;\n    right: 20px;\n    top: 20px;\n    height: 38px;\n    padding: 1px 0;\n    background: #5198DF;\n    background: linear-gradient(#77B9FB, #3782CD);\n    border-radius: 5px;\n    box-shadow: inset rgba(255, 255, 255, 0.45) 0 1px 0, inset rgba(0, 0, 0, 0.2) 0 -1px 0;\n    width: auto\n}\n\nheader ul:before {\n    content: '';\n    position: absolute;\n    z-index: -1;\n    left: -5px;\n    top: -4px;\n    right: -5px;\n    bottom: -6px;\n    background: rgba(0, 0, 0, 0.1);\n    border-radius: 8px;\n    box-shadow: rgba(0, 0, 0, 0.2) 0 -1px 0, inset rgba(255, 255, 255, 0.7) 0 -1px 0\n}\n\nheader ul li {\n    width: 79px;\n    float: left;\n    border-right: 1px solid #3A7CBE;\n    height: 38px\n}\n\nheader ul li.single {\n    border: none\n}\n\nheader ul li+li {\n    width: 78px;\n    border-left: 1px solid #8BBEF3\n}\n\nheader ul li+li+li {\n    border-right: none;\n    width: 79px\n}\n\nheader ul a {\n    line-height: 1;\n    font-size: 11px;\n    color: #fff;\n    color: rgba(255, 255, 255, 0.8);\n    display: block;\n    text-align: center;\n    font-weight: 400;\n    padding-top: 6px;\n    height: 40px;\n    text-shadow: rgba(0, 0, 0, 0.4) 0 -1px 0\n}\n\nheader ul a strong {\n    font-size: 14px;\n    display: block;\n    color: #fff;\n    -webkit-font-smoothing: antialiased\n}\n\nsection {\n    padding: 15px 20px;\n    font-size: 15px;\n    border-top: 1px solid #fff;\n    background: linear-gradient(#fafafa, #DEDEDE 700px);\n    border-radius: 0 0 8px 8px;\n    position: relative\n}\n\nh1,\nh2,\nh3,\nh4,\nh5,\nh6 {\n    color: #222;\n    padding: 0;\n    margin: 0 0 20px;\n    line-height: 1.2\n}\n\np,\nul,\nol,\ntable,\npre,\ndl {\n    margin: 0 0 20px\n}\n\nh1,\nh2,\nh3 {\n    line-height: 1.1\n}\n\nh1 {\n    font-size: 28px\n}\n\nh2 {\n    color: #393939\n}\n\nh3,\nh4,\nh5,\nh6 {\n    color: #494949\n}\n\nblockquote {\n    margin: 0 -20px 20px;\n    padding: 15px 20px 1px 40px;\n    font-style: italic;\n    background: #ccc;\n    background: rgba(0, 0, 0, 0.06);\n    color: #222\n}\n\nimg {\n    max-width: 100%\n}\n\ncode,\npre {\n    font-family: Monaco, Bitstream Vera Sans Mono, Lucida Console, Terminal;\n    color: #333;\n    font-size: 12px;\n    overflow-x: auto\n}\n\npre {\n    padding: 20px;\n    background: #3A3C42;\n    color: #f8f8f2;\n    margin: 0 -20px 20px\n}\n\npre code {\n    color: #f8f8f2\n}\n\nli pre {\n    margin-left: -60px;\n    padding-left: 60px\n}\n\ntable {\n    width: 100%;\n    border-collapse: collapse\n}\n\nth,\ntd {\n    text-align: left;\n    padding: 5px 10px;\n    border-bottom: 1px solid #aaa\n}\n\ndt {\n    color: #222;\n    font-weight: 700\n}\n\nth {\n    color: #222\n}\n\nsmall {\n    font-size: 11px\n}\n\nhr {\n    border: 0;\n    background: #aaa;\n    height: 1px;\n    margin: 0 0 20px\n}\n\nkbd {\n    background-color: #fafbfc;\n    border: 1px solid #c6cbd1;\n    border-bottom-color: #959da5;\n    border-radius: 3px;\n    box-shadow: inset 0 -1px 0 #959da5;\n    color: #444d56;\n    display: inline-block;\n    font-size: 11px;\n    line-height: 10px;\n    padding: 3px 5px;\n    vertical-align: middle\n}\n\nfooter {\n    width: 640px;\n    margin: 0 auto;\n    padding: 20px 0 0;\n    color: #ccc;\n    overflow: hidden\n}\n\nfooter a {\n    color: #fff;\n    font-weight: bold\n}\n\nfooter p {\n    float: left\n}\n\nfooter p+p {\n    float: right\n}\n\n@media print,\nscreen and (max-width: 800px) {\n    body {\n        padding: 0\n    }\n\n    .wrapper {\n        border-radius: 0;\n        box-shadow: none;\n        width: 100%\n    }\n\n    footer {\n        border-radius: 0;\n        padding: 20px;\n        width: auto\n    }\n\n    footer p {\n        float: none;\n        margin: 0\n    }\n\n    footer p+p {\n        float: none\n    }\n}\n\n@media print,\nscreen and (max-width: 580px) {\n    header ul {\n        display: none\n    }\n\n    header p.view {\n        display: block\n    }\n\n    header p {\n        width: 100%\n    }\n}\n\n.progress {\n    display: block;\n    height: 6px;\n    background: #e6e6e6;\n    width: 100%\n}\n\n.progress-done {\n    display: block;\n    height: 100%;\n    background: #0000ff;\n    width: 0%;\n}\n"
  }
]