// ==UserScript== // @name CDN & Server Info Displayer (UI Overhaul) // @name:en CDN & Server Info Displayer (UI Overhaul) // @namespace http://tampermonkey.net/ // @version 7.48.0 // @description [v7.48.0] Enhanced CDN detection: improved Akamai cache/POP detection, added Azure fingerprints, fixed DNS display and icon rendering. // @description:en [v7.48.0] Enhanced CDN detection: improved Akamai cache/POP detection, added Azure fingerprints, fixed DNS display and icon rendering. // @author Zhou Sulong // @license MIT // @match *://*/* // @downloadURL https://raw.githubusercontent.com/zhousulong/cdn-server-info-userscript/main/cdn-server-info.user.js // @updateURL https://raw.githubusercontent.com/zhousulong/cdn-server-info-userscript/main/cdn-server-info.user.js // @grant GM_addStyle // @grant GM_getValue // @grant GM_setValue // @grant GM_getResourceText // @resource cdn_rules https://raw.githubusercontent.com/zhousulong/cdn-server-info-userscript/main/cdn_rules.json?v=7.47.13 // @connect dns.alidns.com // @connect dns.google // @grant GM_xmlhttpRequest // @run-at document-idle // @noframes // ==/UserScript== (function () { 'use strict'; // --- Configuration --- const config = { initialPosition: { bottom: '20px', right: '20px' }, initial_delay: 2500, retry_delay: 7000, max_retries: 4, excludePatterns: [ /\/wp-admin/i, /\/wp-login\.php/i, /(\/|&)pay(pal|ment)/i, /\/checkout|\/billing/i, /\/login|\/signin|\/auth/i, /\/phpmyadmin/i, /(\/ads\/|ad_id=|advertisement)/i, /doubleclick\.net/i, ], // Default settings settings: { theme: 'auto', // 'auto', 'dark' or 'light' panelPosition: 'bottom-right', // 'top-left', 'top-right', 'bottom-left', 'bottom-right' showExtraInfo: true, excludedUrls: [], }, // Initial position for custom placement initialPosition: { bottom: '20px', right: '20px' } }; window.cdnScriptStatus = window.cdnScriptStatus || {}; // --- Core Info Parsing Functions --- function getCacheStatus(h) { const headersToCheck = [ h.get('mpulse_cdn_cache'), // Akamai mPulse (reliable with fetch) h.get('eo-cache-status'), // Prioritize specific headers h.get('hascache'), // Kestrel-based CDN h.get('x-cache'), h.get('x-bdcdn-cache-status'), h.get('x-response-cache'), h.get('x-qc-cache'), h.get('x-cache-lookup'), h.get('cache-status'), h.get('x-cache-status'), h.get('x-edge-cache-status'), h.get('x-sucuri-cache'), h.get('x-vercel-cache'), h.get('cf-cache-status'), h.get('cdn-cache'), h.get('bunny-cache-state'), h.get('x-site-cache-status'), h.get('x-litespeed-cache'), h.get('x-lsadc-cache'), ]; for (const value of headersToCheck) { if (!value) continue; const firstValue = value.split(',')[0].trim(); const upperVal = firstValue.toUpperCase(); if (upperVal.includes('HIT')) return 'HIT'; if (upperVal.includes('MISS')) return 'MISS'; if (upperVal.includes('BYPASS')) return 'BYPASS'; if (upperVal.includes('DYNAMIC')) return 'DYNAMIC'; } if (parseInt(h.get('age'), 10) > 0) return 'HIT (inferred)'; return 'N/A'; } // CDN Providers Configuration // --- Rule Loading & Custom Handlers --- let cdnRules = {}; // Custom handlers for complex extraction logic that can't be easily JSON-ified const customHandlers = { 'Akamai': { getInfo: (h, rule) => { let cache = 'N/A'; // Priority 1: Check server-timing cdn-cache (modern Akamai) const serverTiming = h.get('server-timing'); if (serverTiming) { const cdnCacheMatch = serverTiming.match(/cdn-cache;\s*desc=([^,;]+)/i); if (cdnCacheMatch && cdnCacheMatch[1]) { cache = cdnCacheMatch[1].trim().toUpperCase(); } } // Priority 2: Check x-ak-cache (Akamai specific) if (cache === 'N/A') { const xAkCache = h.get('x-ak-cache'); if (xAkCache) { const status = xAkCache.toUpperCase(); if (status.includes('HIT')) { cache = 'HIT'; } else if (status.includes('MISS')) { cache = 'MISS'; } else if (status.includes('ERROR')) { cache = 'ERROR'; } } } // Priority 3: Check x-tzla-edge-cache-hit (Tesla specific) if (cache === 'N/A') { const tzlaHit = h.get('x-tzla-edge-cache-hit'); if (tzlaHit) { cache = tzlaHit.toUpperCase().includes('HIT') ? 'HIT' : 'MISS'; } } // Priority 4: Check x-age header if (cache === 'N/A') { const xAge = h.get('x-age'); if (xAge !== null) { const age = parseInt(xAge); if (age === 0) { cache = 'MISS'; } else if (age > 0) { cache = 'HIT'; } } } // Fallback to generic cache status if (cache === 'N/A') { cache = getCacheStatus(h); } let pop = 'N/A'; // Try multiple POP extraction strategies // Strategy 1: x-tzla-edge-server (Tesla's Akamai) const tzlaServer = h.get('x-tzla-edge-server'); if (tzlaServer) { // Extract from "sjc38p1tegvr67.teslamotors.com" -> "SJC" const match = tzlaServer.match(/^([a-z]{3})\d+/i); if (match && match[1]) { pop = match[1].toUpperCase(); } } // Strategy 2: x-served-by (standard Akamai) if (pop === 'N/A') { const servedBy = h.get('x-served-by'); if (servedBy) { const match = servedBy.match(/cache-([a-z0-9]+)-/i); if (match && match[1]) pop = match[1].toUpperCase(); } } // Strategy 3: EdgeScape geo headers (fallback for location info) if (pop === 'N/A') { const country = h.get('x-visitor-country'); const continent = h.get('x-visitor-continent'); if (country) { // Use country code as POP indicator (e.g., "JP", "US", "CN") pop = country.toUpperCase(); // Add continent info if available for better context if (continent) { pop = `${continent.toUpperCase()}-${pop}`; } } } // Extract request ID if available let requestId = h.get('x-request-id') || h.get('x-akamai-request-id') || h.get('x-cache-uuid') || h.get('x-reference-error'); // Fallback: extract from server-timing (Akamai's ak_p) if (!requestId) { const serverTiming = h.get('server-timing'); if (serverTiming) { const akMatch = serverTiming.match(/ak_p;\s*desc="?([^;"]+)"?/i); if (akMatch) requestId = akMatch[1]; } } const extra = requestId ? `Req-ID: ${requestId}` : 'Detected via Akamai header/cookie'; return { provider: 'Akamai', cache: cache, pop: pop, extra: extra, }; } }, 'Tencent Cloud': { // Updated name getInfo: (h, rule) => { let cache = 'N/A'; const eoCache = h.get('eo-cache-status'); const nwsLookup = h.get('x-cache-lookup'); if (eoCache) { cache = eoCache.toUpperCase(); } else if (nwsLookup) { const firstPart = nwsLookup.split(',')[0].trim(); cache = firstPart.replace('Cache ', '').toUpperCase(); } else { cache = getCacheStatus(h); } const logUuid = h.get('eo-log-uuid') || h.get('x-nws-log-uuid') || 'N/A'; return { provider: 'Tencent Cloud', cache: cache, pop: 'N/A', extra: `Log-UUID: ${logUuid}`, }; } }, 'Kingsoft Cloud CDN': { getInfo: (h, rule) => { let cache = 'N/A'; const cacheStatus = h.get('x-cache-status'); if (cacheStatus) { const match = cacheStatus.match(/^([A-Z]+)\s+from/i); if (match) cache = match[1].toUpperCase(); } if (cache === 'N/A') cache = getCacheStatus(h); let pop = 'N/A'; if (cacheStatus) { // Extract from "KS-CLOUD-YANC-MP-16-05" -> "YANC" const match = cacheStatus.match(/KS-CLOUD-([A-Z]{2,6})-\w+/i); if (match) { pop = match[1].toUpperCase(); } } if (pop === 'N/A') { const via = h.get('x-link-via'); if (via) { // Extract from "ntct13:443;yancmp16:80;" -> "NTCT13" const match = via.match(/([^:;]+):/); if (match) pop = match[1].toUpperCase(); } } const requestId = h.get('x-cdn-request-id') || h.get('x-kss-request-id') || 'N/A'; return { provider: 'Kingsoft Cloud CDN', cache: cache, pop: pop, extra: `Req-ID: ${requestId}`, }; } }, 'Gcore': { getInfo: (h, rule) => { let cache = h.get('cache') || 'N/A'; if (cache === 'N/A') cache = getCacheStatus(h); else cache = cache.toUpperCase(); const traceparent = h.get('traceparent') || h.get('x-id') || 'N/A'; return { provider: 'Gcore', cache: cache, pop: 'N/A', extra: `ID: ${traceparent.split('-')[1] || traceparent}`, }; } }, 'BytePlus CDN': { getInfo: (h, rule) => { let cache = 'N/A'; // Parse from server-timing: cdn-cache;desc=miss const serverTiming = h.get('server-timing'); if (serverTiming) { const match = serverTiming.match(/cdn-cache;desc=([^,]+)/); if (match) cache = match[1].toUpperCase(); } if (cache === 'N/A') cache = getCacheStatus(h); // BytePlus CDN doesn't have standard airport codes, skip POP let pop = 'N/A'; const requestId = h.get('x-cdn-request-id') || h.get('x-tt-trace-id') || 'N/A'; return { provider: 'BytePlus CDN', cache: cache, pop: pop, extra: `Req-ID: ${requestId}`, }; } }, 'CDNetworks': { getInfo: (h, rule) => { let cache = getCacheStatus(h); let pop = 'N/A'; // Prioritize x-via as it contains more detailed POP info const via = h.get('x-via') || h.get('via'); console.log('[CDNetworks] via:', via); console.log('[CDNetworks] x-via:', h.get('x-via')); if (via) { // Try to extract alphabetic codes first (e.g., PS-FOC, PS-NTG) // Avoid pure numeric codes like CS-000 const regex = /(PS|CS)-([A-Z0-9]{3})-/g; let match; while ((match = regex.exec(via)) !== null) { const code = match[2]; console.log('[CDNetworks] Found:', code, 'HasLetter:', /[A-Z]/.test(code)); // Only accept codes that contain at least one letter if (/[A-Z]/.test(code)) { pop = code.toUpperCase(); console.log('[CDNetworks] Selected POP:', pop); break; } } } // If not found in via, try x-px header or x-ws-request-id if (pop === 'N/A') { const altHeaders = [h.get('x-px'), h.get('x-ws-request-id')]; for (const val of altHeaders) { if (val) { const regex = /(PS|CS)-([A-Z0-9]{3})-/g; let match; while ((match = regex.exec(val)) !== null) { const code = match[2]; if (/[A-Z]/.test(code)) { pop = code.toUpperCase(); break; } } if (pop !== 'N/A') break; } } } const requestId = h.get('x-ws-request-id') || 'N/A'; return { provider: 'CDNetworks', cache: cache, pop: pop, extra: `Req-ID: ${requestId}`, }; } }, 'State Cloud CDN': { getInfo: (h, rule) => { let cache = 'N/A'; const ctlStatus = h.get('ctl-cache-status'); if (ctlStatus) { const match = ctlStatus.match(/^(HIT|MISS|EXPIRED|UPDATING)/i); if (match) cache = match[0].toUpperCase(); } if (cache === 'N/A') cache = getCacheStatus(h); let pop = 'N/A'; if (ctlStatus) { // Extract from "HIT from zj-wenzhou8-ca08" -> "ZJ-WENZHOU" const match = ctlStatus.match(/from ([a-z0-9-]+)/i); if (match) { const parts = match[1].split('-'); pop = (parts[0] + (parts[1] ? '-' + parts[1].replace(/\d+$/, '') : '')).toUpperCase(); } } const requestId = h.get('x-ct-request-id') || h.get('request-id') || 'N/A'; return { provider: 'State Cloud CDN', cache: cache, pop: pop, extra: `Req-ID: ${requestId}`, }; } }, 'Adobe Experience Manager': { getInfo: (h, rule) => { let cache = getCacheStatus(h); let pop = 'N/A'; const dispatcher = h.get('x-dispatcher'); if (dispatcher) { // Extract region like "euwest1" from "dispatcher2euwest1-b80" const match = dispatcher.match(/dispatcher\d+([a-z0-9]+)-/i); if (match) pop = match[1].toUpperCase(); } const vhost = h.get('x-vhost') || 'N/A'; return { provider: 'Adobe Experience Manager', cache: cache, pop: pop, extra: `Vhost: ${vhost}`, }; } }, 'QRATOR': { getInfo: (h, rule) => { // Check for x-nextjs-cache first, then fallback to generic cache status let cache = h.get('x-nextjs-cache') || getCacheStatus(h); if (cache) cache = cache.toUpperCase(); const instance = h.get('x-app-instance-ing') || 'N/A'; return { provider: 'QRATOR', cache: cache, pop: 'N/A', extra: `Instance: ${instance}`, }; } }, 'Huawei Cloud CDN': { getInfo: (h, rule) => { let cache = 'N/A'; const xHCache = h.get('x-h-cache-status'); const xCacheStatus = h.get('x-cache-status'); if (xHCache) { cache = xHCache.toUpperCase(); } else if (xCacheStatus) { cache = xCacheStatus.toUpperCase(); } else if (h.get('nginx-hit') === '1') { cache = 'HIT'; } else { cache = getCacheStatus(h); } let pop = 'N/A'; const via = h.get('via'); if (via) { const match = via.match(/(?:HCDN|CHN)-([a-zA-Z0-9]+)/); if (match) { const loc = match[1]; const compound = loc.match(/^([A-Z]{2})([a-z]+)/); if (compound) { pop = (compound[1] + '-' + compound[2]).toUpperCase(); } else { pop = loc.toUpperCase(); } } } // Extract x-ccdn-req-id (format: x-ccdn-req-id-46b1) let requestId = 'N/A'; for (const [key, value] of h.entries()) { if (key.startsWith('x-ccdn-req-id')) { requestId = value; break; } } if (requestId === 'N/A') { requestId = h.get('x-obs-request-id') || h.get('x-hw-request-id') || 'N/A'; } return { provider: 'Huawei Cloud CDN', cache: cache, pop: pop, extra: `Req-ID: ${requestId}`, }; } }, 'Baidu Cloud CDN': { getInfo: (h, rule) => { let cache = 'N/A'; const ohcCacheHit = h.get('ohc-cache-hit'); const xCacheStatus = h.get('x-cache-status'); if (ohcCacheHit) { cache = 'HIT'; } else if (xCacheStatus) { cache = xCacheStatus.toUpperCase(); } else { cache = getCacheStatus(h); } let pop = 'N/A'; if (ohcCacheHit) { const popMatch = ohcCacheHit.match(/([a-z]+\d+)/i); if (popMatch && popMatch[1]) { pop = popMatch[1].toUpperCase(); } } const requestId = h.get('x-bce-request-id') || h.get('x-life-unique-id') || 'N/A'; return { provider: 'Baidu Cloud CDN', cache: cache, pop: pop, extra: `Req-ID: ${requestId}`, }; } }, 'ByteDance CDN': { getInfo: (h, rule) => { let cache = 'N/A'; // Priority 1: x-bdcdn-cache-status const bdcdnCache = h.get('x-bdcdn-cache-status'); if (bdcdnCache) { cache = bdcdnCache.replace('TCP_', '').toUpperCase(); } else { // Priority 2: x-response-cache const responseCache = h.get('x-response-cache'); if (responseCache) { cache = responseCache.replace('edge_', '').toUpperCase(); } else { // Priority 3: server-timing const serverTiming = h.get('server-timing'); if (serverTiming) { const match = serverTiming.match(/cdn-cache;desc=([^,]+)/); if (match) cache = match[1].toUpperCase(); } } } // Fallback to x-tt-trace-tag or generic if (cache === 'N/A') { const ttTrace = h.get('x-tt-trace-tag'); if (ttTrace) { const match = ttTrace.match(/cdn-cache=([^;]+)/); if (match) cache = match[1].toUpperCase(); } } if (cache === 'N/A') cache = getCacheStatus(h); let pop = 'N/A'; const viaHeader = h.get('via'); if (viaHeader) { // Try to extract from "cache17.jswuxi-ct32" -> "JSWUXI" let match = viaHeader.match(/cache\d+\.([a-z]+)/i); if (match && match[1]) { pop = match[1].toUpperCase(); } else { // Fallback: Extract from "live4.cn7594[899,0]" or "ens-live7.cn8685" -> "CN" match = viaHeader.match(/(?:ens-)?live\d+\.(cn\d+)/i); if (match && match[1]) { pop = 'CN'; } } } const traceId = h.get('x-tt-trace-id') || h.get('x-tt-logid') || h.get('x-tos-request-id') || 'N/A'; return { provider: 'ByteDance CDN', cache: cache, pop: pop, extra: `Trace-ID: ${traceId}`, }; } }, 'Netlify': { getInfo: (h, rule) => { let pop = 'N/A'; const serverTiming = h.get('server-timing'); if (serverTiming) { const match = serverTiming.match(/dc;desc="?([^",]+)"?/); if (match && match[1]) { pop = match[1].toUpperCase(); } } return { provider: 'Netlify', cache: getCacheStatus(h), pop: pop, extra: `Req-ID: ${h.get('x-nf-request-id') || 'N/A'}`, }; } }, 'BunnyCDN': { getInfo: (h, rule) => { let cache = 'N/A'; const cdnCache = h.get('cdn-cache'); if (cdnCache) { cache = cdnCache.toUpperCase(); } else { cache = getCacheStatus(h); } let pop = 'N/A'; const server = h.get('server'); if (server) { // Extract POP from server header like "BunnyCDN-LA1-912" -> "LA1" const match = server.match(/BunnyCDN-([A-Z0-9]+)-/i); if (match && match[1]) { pop = match[1].toUpperCase(); } } const requestId = h.get('cdn-requestid') || 'N/A'; return { provider: 'BunnyCDN', cache: cache, pop: pop, extra: `Req-ID: ${requestId}`, }; } }, 'Medianova': { getInfo: (h, rule) => { let cache = 'N/A'; const cacheStatus = h.get('x-cache-status'); if (cacheStatus) { // Parse format like "Edge : HIT," or "Edge : MISS," const match = cacheStatus.match(/Edge\s*:\s*([A-Z]+)/i); if (match && match[1]) { cache = match[1].toUpperCase(); } } else { cache = getCacheStatus(h); } let pop = 'N/A'; const edgeLocation = h.get('x-edge-location'); if (edgeLocation) { // Extract POP from format like "SG-378" -> "SG" const match = edgeLocation.match(/^([A-Z]{2,3})-/i); if (match && match[1]) { pop = match[1].toUpperCase(); } } const requestId = h.get('x-mnrequest-id') || 'N/A'; return { provider: 'Medianova', cache: cache, pop: pop, extra: `Req-ID: ${requestId}`, }; } }, 'CacheFly': { getInfo: (h, rule) => { let cache = 'N/A'; // x-cf3: H = HIT, M = MISS const cf3 = h.get('x-cf3'); if (cf3) { if (cf3.toUpperCase() === 'H') { cache = 'HIT'; } else if (cf3.toUpperCase() === 'M') { cache = 'MISS'; } } else { cache = getCacheStatus(h); } let pop = 'N/A'; const cf1 = h.get('x-cf1'); if (cf1) { // Extract POP from format like "28787:fP.tko2:co:1765918716:cacheN.tko2-01:M" // Looking for pattern like "tko2" or "cache location" const match = cf1.match(/\.([a-z]{3,4}\d*)[:\-\.]/i); if (match && match[1]) { pop = match[1].toUpperCase(); } } const requestId = h.get('x-cf-reqid') || 'N/A'; const age = h.get('cf4age') || 'N/A'; return { provider: 'CacheFly', cache: cache, pop: pop, extra: `Req-ID: ${requestId}, Age: ${age}s`, }; } }, 'SwiftServe CDN': { getInfo: (h, rule) => { let cache = 'N/A'; let pop = 'N/A'; // Parse x-cache header: "HIT from da010.vn17.swiftserve.com:443" const xCache = h.get('x-cache'); if (xCache) { // Extract cache status if (xCache.toUpperCase().includes('HIT')) { cache = 'HIT'; } else if (xCache.toUpperCase().includes('MISS')) { cache = 'MISS'; } // Extract POP from domain: da010.vn17.swiftserve.com -> VN17 const match = xCache.match(/from\s+([a-z0-9]+)\.([a-z0-9]+)\.swiftserve\.com/i); if (match && match[2]) { pop = match[2].toUpperCase(); } } // Fallback to generic cache status if not found if (cache === 'N/A') { cache = getCacheStatus(h); } return { provider: 'SwiftServe CDN', cache: cache, pop: pop, extra: 'Detected via x-cache header', }; } }, 'SiteGround': { getInfo: (h, rule) => { let cache = 'N/A'; // Extract cache status from x-proxy-cache header const proxyCache = h.get('x-proxy-cache'); if (proxyCache) { cache = proxyCache.toUpperCase(); } else { cache = getCacheStatus(h); } let pop = 'N/A'; // Extract POP from x-ce header: "asia-northeast1-zp5x" -> "ASIA-NORTHEAST1" const xCe = h.get('x-ce'); if (xCe) { // Remove the trailing hash part (e.g., "-zp5x") const match = xCe.match(/^([a-z]+-[a-z]+\d+)/i); if (match && match[1]) { pop = match[1].toUpperCase(); } } const requestId = h.get('x-proxy-cache-info') || 'N/A'; return { provider: 'SiteGround', cache: cache, pop: pop, extra: `Cache-Info: ${requestId}`, }; } }, 'StackPath': { getInfo: (h, rule) => { let cache = 'N/A'; // Priority 1: x-cdn-cache-status (StackCDN) const cdnCache = h.get('x-cdn-cache-status'); if (cdnCache) { cache = cdnCache.toUpperCase(); } else { // Priority 2: x-scp-cache-status (newer StackPath) const scpCache = h.get('x-scp-cache-status'); if (scpCache) { cache = scpCache.toUpperCase(); } else { // Priority 3: x-proxy-cache (older configs) const proxyCache = h.get('x-proxy-cache'); if (proxyCache) { cache = proxyCache.toUpperCase(); } else { cache = getCacheStatus(h); } } } let pop = 'N/A'; // Priority 1: x-via (StackCDN) - e.g., "NRT1" const xVia = h.get('x-via'); if (xVia) { pop = xVia.toUpperCase(); } else { // Priority 2: x-scp-served-by (newer StackPath) const servedBy = h.get('x-scp-served-by'); if (servedBy) { const match = servedBy.match(/([A-Z]{3,4})/i); if (match && match[1]) { pop = match[1].toUpperCase(); } } } // Extra info: show origin cache status if available const originCache = h.get('x-origin-cache-status'); const cacheInfo = h.get('x-proxy-cache-info'); let extra = 'Detected via StackCDN/StackPath headers'; if (originCache) { extra = `Origin: ${originCache}`; } else if (cacheInfo && cacheInfo !== 'N/A') { extra = `Cache-Info: ${cacheInfo}`; } return { provider: 'StackPath', cache: cache, pop: pop, extra: extra, }; } } }; function loadRules() { try { const rulesText = GM_getResourceText('cdn_rules'); if (rulesText) { cdnRules = JSON.parse(rulesText); console.log('[CDN Info] Loaded rules from resource'); } else { console.warn('[CDN Info] No cdn_rules resource found'); } } catch (e) { console.error('[CDN Info] Failed to load rules:', e); } } // Generic Info Extractor function genericGetInfo(h, rule, providerName) { let pop = 'N/A'; if (rule.pop_header) { const val = h.get(rule.pop_header); if (val) { if (rule.pop_regex) { const match = val.match(new RegExp(rule.pop_regex, 'i')); if (match && match[1]) pop = match[1].toUpperCase(); } else { // Default heuristic - extract airport code from value // First try to match letters at the start const letterMatch = val.trim().match(/^([A-Z]+)/i); if (letterMatch && letterMatch[1].length >= 3) { // If we have 3+ letters at start, use first 3-4 pop = letterMatch[1].substring(0, Math.min(4, letterMatch[1].length)).toUpperCase(); } else { // For hyphenated formats like "AS-JP-HND-HYBRID", find the 3-4 letter part const parts = val.trim().split(/[-_]/); for (const part of parts) { const partMatch = part.match(/^([A-Z]+)$/i); if (partMatch && partMatch[1].length >= 3 && partMatch[1].length <= 4) { pop = partMatch[1].toUpperCase(); break; } } // If still not found, use first part if (pop === 'N/A' && parts.length > 0) { pop = parts[0].toUpperCase(); } } } } } let extra = 'N/A'; if (rule.id_header) { extra = `${rule.id_header}: ${h.get(rule.id_header) || 'N/A'}`; } return { provider: providerName, cache: getCacheStatus(h), pop: pop, extra: extra }; } // --- Extended Information Functions --- function getServerInfo(h) { const server = h.get('server'); if (!server) return 'N/A'; // Clean up server string: handle cases like "AkamaiGHost; opt=..." or "nginx/1.2.3" return server.split(';')[0].trim(); } function getConnectionInfo(response) { // Get TLS version from response if available // Note: This is not directly available in fetch API, but we can infer from other headers const protocol = response.url.startsWith('https') ? 'HTTPS' : 'HTTP'; return protocol; } function getAdditionalInfo(h) { // Get content type const contentType = h.get('content-type'); if (!contentType) return ''; // Extract just the MIME type const mimeType = contentType.split(';')[0].trim(); return `Type: ${mimeType}`; } // Enhanced parseInfo function with Scoring System function parseInfo(response) { if (Object.keys(cdnRules).length === 0) loadRules(); const h = response.headers; const lowerCaseHeaders = new Map(); for (const [key, value] of h.entries()) { lowerCaseHeaders.set(key.toLowerCase(), value); } const candidates = []; for (const [name, rule] of Object.entries(cdnRules)) { let score = 0; // 1. Header Checks // - Exact Key Match: +20 // - Regex Value Match: +30 if (rule.headers) { for (const [header, val] of Object.entries(rule.headers)) { if (lowerCaseHeaders.has(header)) { if (val === null) { score += 20; } else { if (new RegExp(val, 'i').test(lowerCaseHeaders.get(header))) { score += 30; } } } } } // 2. ID Header Check (Strong Signal) -> +50 if (rule.id_header && lowerCaseHeaders.has(rule.id_header)) { score += 50; } // 3. Server Header Check -> +10 (or +50 for unique signatures) if (rule.server) { const server = lowerCaseHeaders.get('server'); if (server && new RegExp(rule.server, 'i').test(server)) { // Unique server signatures get higher weight (+50) // These are exclusive to specific CDNs and should almost guarantee a win against generic headers const uniqueServers = [ 'TLB', 'Byte-nginx', 'AkamaiGHost', 'cloudflare', 'Lego Server', 'edgeone', 'CloudWAF', 'SLT-MID', // Tencent 'ESA', // Alibaba 'yunjiasu', 'JSP3', // Baidu 'EdgeNext', 'ChinaCache', 'MNCDN', 'CFS', // CacheFly 'ECS', 'ECAcc', // Edgio 'PWS', 'ChinaNetCenter', // Wangsu 'BunnyCDN', 'keycdn', 'stackpath', 'HuaweiCloud', 'CDN77', 'Netlify', 'HiNetCDN', 'QRATOR' ]; const isUnique = uniqueServers.some(sig => new RegExp(sig, 'i').test(rule.server)); score += isUnique ? 50 : 10; } } // 4. Via Header Check -> +10 if (rule.via) { const via = lowerCaseHeaders.get('via'); if (via && new RegExp(rule.via, 'i').test(via)) { score += 10; } } // 5. Cookie Check -> +20 if (rule.cookies) { const cookie = lowerCaseHeaders.get('set-cookie') || ''; for (const [cName, cVal] of Object.entries(rule.cookies)) { if (cookie.includes(cName)) { if (cVal === null || cookie.includes(cVal)) { score += 20; } } } } // 6. Custom Logic Match -> +20 if (rule.custom_check_logic === 'check_aws_compat') { if (lowerCaseHeaders.has('x-amz-cf-id')) { const via = lowerCaseHeaders.get('via') || ''; if (!via.includes('cloudfront.net')) { score += 20; } } } if (score > 0) { // Add base priority from rules score += (rule.priority || 0); const handler = customHandlers[name] ? customHandlers[name].getInfo : genericGetInfo; candidates.push({ ...handler(lowerCaseHeaders, rule, name), score: score, }); } } if (candidates.length > 0) { // Sort by score descending candidates.sort((a, b) => b.score - a.score); const winner = candidates[0]; // Console log for debugging the scoring decision if (candidates.length > 1) { console.log(`[CDN Scoring] Winner: ${winner.provider} (${winner.score}) vs Runner-up: ${candidates[1].provider} (${candidates[1].score})`); } // Add extended information winner.server = getServerInfo(lowerCaseHeaders); if (winner.server === 'N/A' && winner.provider) { winner.server = winner.provider; // Intelligent fallback check } winner.connection = getConnectionInfo(response); winner.additional = getAdditionalInfo(lowerCaseHeaders); return winner; } // Fallback: No CDN detected, check if server header exists const server = lowerCaseHeaders.get('server'); const result = { provider: server || 'Unknown', cache: getCacheStatus(lowerCaseHeaders), pop: 'N/A', extra: server ? 'No CDN detected' : 'No server header found', server: getServerInfo(lowerCaseHeaders), connection: getConnectionInfo(response), additional: getAdditionalInfo(lowerCaseHeaders), }; return result; } // --- Icons & Assets --- const cdnIcons = { 'CDNetworks': ``, 'Kingsoft Cloud CDN': ``, 'State Cloud CDN': ``, 'Adobe Experience Manager': ``, 'Huawei Cloud CDN': ``, 'Gcore': ``, 'Cloudflare': ``, 'Vercel': ``, 'CloudFront': ``, 'Netlify': ``, 'Nginx': ``, 'Akamai': ``, 'Fastly': ``, 'Tencent': ``, 'Alibaba': ``, 'ByteDance': ``, 'Microsoft Azure CDN': ``, 'BytePlus': ``, 'Google': ``, 'QUIC': ``, 'Bunny': ``, 'KeyCDN': ``, 'Apache': ``, 'LiteSpeed': ``, 'OpenResty': ``, 'EdgeNext': ``, 'Medianova': ``, 'Angie': ``, 'QRATOR': ``, 'CacheFly': ``, 'Baidu Cloud CDN': ``, 'HiNet CDN': ``, }; // --- DNS Detection Logic --- const dnsCache = new Map(); // Cache results to avoid redundant requests async function checkDNS(domain) { if (!domain) { console.log('[CDN DNS] Skipped: no domain'); return null; } if (dnsCache.has(domain)) { console.log('[CDN DNS] Using cached result for', domain); return dnsCache.get(domain); } console.log('[CDN DNS] Starting DNS lookup for', domain); // Try Alibaba DNS first (China friendly), then Google DNS const dohProviders = [ `https://dns.alidns.com/resolve?name=${domain}&type=CNAME`, `https://dns.google/resolve?name=${domain}&type=CNAME` ]; for (const url of dohProviders) { try { const result = await new Promise((resolve, reject) => { GM_xmlhttpRequest({ method: "GET", url: url, onload: function (response) { if (response.status === 200) { try { resolve(JSON.parse(response.responseText)); } catch (e) { reject(e); } } else { reject(new Error(response.statusText)); } }, onerror: reject, ontimeout: reject }); }); if (result && result.Answer) { const candidates = []; const foundCnames = []; for (const record of result.Answer) { const cname = record.data; if (!cname) continue; // Check against rules // Remove trailing dot if present const cleanCname = cname.endsWith('.') ? cname.slice(0, -1) : cname; foundCnames.push(cleanCname); for (const [providerName, rule] of Object.entries(cdnRules)) { if (rule.cnames && Array.isArray(rule.cnames)) { for (const pattern of rule.cnames) { if (cleanCname.includes(pattern)) { candidates.push({ provider: providerName, cname: cleanCname, priority: rule.priority || 0 }); } } } } } // If multiple matches, choose the one with highest priority if (candidates.length > 0) { candidates.sort((a, b) => b.priority - a.priority); const winner = candidates[0]; console.log(`[CDN DNS] Confirmed ${winner.provider} via CNAME: ${winner.cname} (Priority: ${winner.priority})`); if (candidates.length > 1) { console.log(`[CDN DNS] Runner-up: ${candidates[1].provider} (Priority: ${candidates[1].priority})`); } const match = { provider: winner.provider, cname: winner.cname }; dnsCache.set(domain, match); return match; } else if (foundCnames.length > 0) { // Found CNAME but no matching CDN - still return the CNAME for display console.log(`[CDN DNS] Found CNAME(s) but no matching CDN: ${foundCnames.join(', ')}`); const match = { provider: null, cname: foundCnames[0] }; dnsCache.set(domain, match); return match; } } } catch (e) { console.warn(`[CDN DNS] Failed to query ${url}:`, e); // Continue to next provider } } console.log('[CDN DNS] No CNAME found for', domain); dnsCache.set(domain, null); return null; } // Update the panel if DNS detection finds a better result function updatePanelWithDNS(dnsResult, currentInfo) { if (!dnsResult) return; const panel = document.getElementById('cdn-info-host-enhanced'); if (!panel || !panel.shadowRoot) return; // Find the provider value element (first info-line's info-value) const firstInfoLine = panel.shadowRoot.querySelector('.info-line'); if (!firstInfoLine) return; const providerValue = firstInfoLine.querySelector('.info-value'); if (!providerValue) return; if (dnsResult.provider && dnsResult.provider !== currentInfo.provider) { // DNS result differs from header detection - SILENTLY OVERRIDE console.log(`[CDN DNS] ⚠️ Correcting provider from ${currentInfo.provider} to ${dnsResult.provider}`); // Update provider text providerValue.textContent = dnsResult.provider; providerValue.title = `Detected via DNS: ${dnsResult.cname}`; // Update watermark to match new provider let watermark = panel.shadowRoot.querySelector('.cdn-watermark'); // Find the icon for the new provider let iconKey = Object.keys(cdnIcons).find(key => key === dnsResult.provider); if (!iconKey) { iconKey = Object.keys(cdnIcons).find(key => { const providerLower = dnsResult.provider.toLowerCase(); const keyLower = key.toLowerCase(); return providerLower.includes(keyLower) || keyLower.includes(providerLower); }); } if (iconKey) { const iconSvg = cdnIcons[iconKey]; if (watermark) { // Update existing watermark watermark.innerHTML = iconSvg; console.log('[CDN DNS] Updated existing watermark icon'); } else { // Create new watermark element watermark = document.createElement('div'); watermark.className = 'cdn-watermark'; watermark.innerHTML = iconSvg; const panelElement = panel.shadowRoot.querySelector('#cdn-info-panel-enhanced'); if (panelElement) { panelElement.insertBefore(watermark, panelElement.firstChild); console.log('[CDN DNS] Created new watermark icon'); } } } else { // No matching icon found if (watermark) { watermark.innerHTML = ''; } console.log('[CDN DNS] No icon found for:', dnsResult.provider); } // Add DNS status at bottom addDNSStatus(panel, `DNS: ${dnsResult.cname}`); } else { // No provider match or provider matches - just show CNAME if (dnsResult.provider) { console.log(`[CDN DNS] ✓ Confirmed ${dnsResult.provider} via CNAME: ${dnsResult.cname}`); } else { console.log(`[CDN DNS] Found CNAME but no CDN match: ${dnsResult.cname}`); } addDNSStatus(panel, `DNS: ${dnsResult.cname}`); } } // Helper function to add DNS status line at bottom function addDNSStatus(panel, statusText) { if (!panel || !panel.shadowRoot) return; // Check if status already exists let statusLine = panel.shadowRoot.querySelector('.dns-status'); if (!statusLine) { statusLine = document.createElement('div'); statusLine.className = 'dns-status'; panel.shadowRoot.querySelector('#cdn-info-panel-enhanced').appendChild(statusLine); } const prefix = "DNS: "; let value = statusText; if (statusText.startsWith(prefix)) { value = statusText.substring(prefix.length); } statusLine.innerHTML = `DNS:${value}`; } // --- UI & Execution Functions --- // Detect if the current page is using dark or light theme function detectPageTheme() { try { // Method 1: Check data-theme or data-bs-theme attributes (common in modern sites) const htmlTheme = document.documentElement.getAttribute('data-theme') || document.documentElement.getAttribute('data-bs-theme') || document.documentElement.getAttribute('data-color-mode'); if (htmlTheme) { if (htmlTheme.toLowerCase().includes('dark')) return 'dark'; if (htmlTheme.toLowerCase().includes('light')) return 'light'; } const bodyTheme = document.body?.getAttribute('data-theme') || document.body?.getAttribute('data-bs-theme') || document.body?.getAttribute('data-color-mode'); if (bodyTheme) { if (bodyTheme.toLowerCase().includes('dark')) return 'dark'; if (bodyTheme.toLowerCase().includes('light')) return 'light'; } // Method 2: Check class names for dark/light keywords const htmlClass = document.documentElement.className || ''; const bodyClass = document.body?.className || ''; const combinedClasses = (htmlClass + ' ' + bodyClass).toLowerCase(); if (combinedClasses.includes('dark-mode') || combinedClasses.includes('dark-theme') || combinedClasses.includes(' dark ') || combinedClasses.startsWith('dark ') || combinedClasses.endsWith(' dark')) { return 'dark'; } if (combinedClasses.includes('light-mode') || combinedClasses.includes('light-theme') || combinedClasses.includes(' light ') || combinedClasses.startsWith('light ') || combinedClasses.endsWith(' light')) { return 'light'; } // Method 3: Check color-scheme CSS property const colorScheme = getComputedStyle(document.documentElement).colorScheme; if (colorScheme && colorScheme.includes('dark')) return 'dark'; if (colorScheme && colorScheme.includes('light')) return 'light'; // Method 4: Analyze background color brightness const bgColor = getComputedStyle(document.body).backgroundColor; if (bgColor && bgColor !== 'transparent' && bgColor !== 'rgba(0, 0, 0, 0)') { const brightness = calculateBrightness(bgColor); // Lower threshold for better dark detection return brightness < 100 ? 'dark' : 'light'; } // Fallback to html element const htmlBg = getComputedStyle(document.documentElement).backgroundColor; if (htmlBg && htmlBg !== 'transparent' && htmlBg !== 'rgba(0, 0, 0, 0)') { const brightness = calculateBrightness(htmlBg); return brightness < 100 ? 'dark' : 'light'; } return null; // Cannot determine } catch (e) { console.warn('[CDN Detector] Error detecting page theme:', e); return null; } } // Calculate brightness from RGB color string function calculateBrightness(color) { const rgb = color.match(/\d+/g); if (!rgb || rgb.length < 3) return 255; // Default to light const [r, g, b] = rgb.map(Number); // Standard brightness formula return (r * 299 + g * 587 + b * 114) / 1000; } function getPanelCSS() { // Simple light/dark theme (no auto mode) const isDarkTheme = config.settings.theme === 'dark'; console.log('[CDN Detector] Panel theme:', config.settings.theme); // Ultra-premium aesthetic: deeper blacks, cleaner whites const materialBase = isDarkTheme ? 'rgba(15, 15, 15, 0.65)' // Dark mode: darker, slightly less transparent for legibility : 'rgba(255, 255, 255, 0.65)'; // Light mode: milky white const surfaceGradient = isDarkTheme ? 'linear-gradient(180deg, rgba(255, 255, 255, 0.08) 0%, rgba(255, 255, 255, 0) 100%)' : 'linear-gradient(180deg, rgba(255, 255, 255, 0.4) 0%, rgba(255, 255, 255, 0) 100%)'; const borderColor = isDarkTheme ? 'rgba(255, 255, 255, 0.12)' // Crisp border in dark : 'rgba(0, 0, 0, 0.08)'; const textColor = isDarkTheme ? '#FFFFFF' : '#000000'; const labelColor = isDarkTheme ? 'rgba(255, 255, 255, 0.5)' : 'rgba(0, 0, 0, 0.5)'; // Specific font stacks const uiFont = '"SF Pro Text", -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif'; const monoFont = '"SF Mono", "Menlo", "Monaco", "Consolas", "Liberation Mono", "Courier New", monospace'; // Colors & Shadows const backdropFilter = 'blur(24px) saturate(180%)'; // Balanced blur const boxShadow = isDarkTheme ? '0 12px 32px rgba(0, 0, 0, 0.4), 0 4px 8px rgba(0, 0, 0, 0.2)' : '0 12px 32px rgba(0, 0, 0, 0.1), 0 4px 8px rgba(0, 0, 0, 0.05)'; const greenColor = isDarkTheme ? '#4ADE80' : '#16A34A'; // Slightly muted green const redColor = '#EF4444'; const blueColor = '#3B82F6'; return ` /* Safe CSS Reset for Shadow DOM */ :host { all: initial; position: fixed; z-index: 2147483647; ${getPositionCSS()} font-family: ${uiFont}; -webkit-font-smoothing: antialiased; -moz-osx-font-smoothing: grayscale; /* Prevent font scaling issues */ text-size-adjust: 100%; } /* Reset inherited properties specifically for our container */ #cdn-info-panel-enhanced { all: unset; /* Clear inherited styles on container */ position: relative; box-sizing: border-box; width: 252px; /* Optimal width for compact display */ padding: 14px 16px; border-radius: 14px; background-color: ${materialBase}; backdrop-filter: ${backdropFilter}; -webkit-backdrop-filter: ${backdropFilter}; border: 1px solid ${borderColor}; box-shadow: ${boxShadow}; cursor: move; user-select: none; transition: all 0.3s ease; color: ${textColor}; display: flex; flex-direction: column; gap: 10px; overflow: hidden; /* Explicitly define inherited properties to stop leakage */ line-height: 1.5; font-size: 14px; font-style: normal; font-weight: normal; text-align: left; text-decoration: none; text-transform: none; } /* Ensure all children use border-box */ #cdn-info-panel-enhanced * { box-sizing: border-box; } /* Subtle top highlight */ #cdn-info-panel-enhanced::after { content: ''; position: absolute; top: 0; left: 0; right: 0; bottom: 0; border-radius: 14px; background: ${surfaceGradient}; pointer-events: none; z-index: 1; } #cdn-info-panel-enhanced > *:not(.cdn-watermark) { position: relative; z-index: 2; } /* --- Buttons (Hidden by default) --- */ button.icon-btn { position: absolute !important; top: 14px !important; width: 18px !important; height: 18px !important; border-radius: 50% !important; background: transparent !important; color: ${textColor} !important; border: none !important; outline: none !important; cursor: pointer !important; display: flex !important; align-items: center !important; justify-content: center !important; opacity: 0 !important; transition: opacity 0.2s ease, transform 0.2s ease !important; z-index: 100 !important; padding: 0 !important; margin: 0 !important; -webkit-appearance: none !important; appearance: none !important; line-height: 1 !important; } button.close-btn { right: 12px !important; font-size: 16px !important; font-weight: 300 !important; line-height: 18px !important; } button.theme-btn { right: 36px !important; font-size: 12px !important; line-height: 18px !important; } #cdn-info-panel-enhanced:hover button.icon-btn { opacity: 0.5 !important; } button.icon-btn:hover { opacity: 1 !important; transform: scale(1.1); } /* --- Content Typography --- */ .panel-header { display: block; font-family: ${uiFont}; font-size: 10px; font-weight: 700; color: ${isDarkTheme ? 'rgba(255, 255, 255, 0.4)' : 'rgba(0, 0, 0, 0.4)'}; text-transform: uppercase; letter-spacing: 1.5px; margin: 0 0 2px 0; padding-left: 2px; line-height: 1.4; } .info-lines-container { display: flex; flex-direction: column; gap: 6px; margin: 0; padding: 0; } .info-line { display: flex; justify-content: space-between; align-items: baseline; margin: 0; padding: 0; border: none; line-height: normal; } .info-label { display: inline-block; font-family: ${uiFont}; font-size: 11px; font-weight: 500; color: ${isDarkTheme ? 'rgba(255, 255, 255, 0.5)' : 'rgba(0, 0, 0, 0.5)'}; letter-spacing: 0px; flex-shrink: 0; /* Protect label from squeezing */ margin-right: 8px; width: 42px; /* Fixed width for labels */ } .info-value { display: inline-block; font-family: ${monoFont}; /* Mono for data */ font-size: 11px; font-weight: 500; color: ${textColor}; text-align: right; opacity: 0.95; flex: 1; /* Occupy all remaining space */ min-width: 0; /* Enable truncation in flex item */ white-space: nowrap; overflow: hidden; text-overflow: ellipsis; letter-spacing: -0.2px; } .cache-HIT { color: ${greenColor} !important; } .cache-MISS { color: ${redColor} !important; } .cache-BYPASS, .cache-DYNAMIC { color: ${blueColor} !important; } /* Watermark Styles */ .cdn-watermark { position: absolute; top: 8px; left: 75%; bottom: 8px; max-width: 45%; opacity: 1; pointer-events: none; z-index: 0; display: flex; align-items: flex-end; justify-content: center; transform: translateX(-50%); color: ${isDarkTheme ? 'rgba(255, 255, 255, 0.04)' : 'rgba(0, 0, 0, 0.04)'}; } .cdn-watermark svg { height: 100%; width: auto; fill: currentColor; display: block; } /* DNS Status Line */ /* DNS Status Line */ .dns-status { display: flex; align-items: center; justify-content: center; font-family: ${monoFont}; font-size: 9px; color: ${isDarkTheme ? 'rgba(255, 255, 255, 0.3)' : 'rgba(0, 0, 0, 0.3)'}; margin-top: 1px; padding-top: 1px; border-top: 1px solid ${isDarkTheme ? 'rgba(255, 255, 255, 0.05)' : 'rgba(0, 0, 0, 0.05)'}; letter-spacing: 0.3px; opacity: 0.8; line-height: normal; width: 100%; } .dns-label { white-space: nowrap; flex-shrink: 0; margin-right: 4px; } .dns-value { white-space: nowrap; overflow: hidden; text-overflow: ellipsis; direction: rtl; text-align: right; flex: 1; min-width: 0; } /* Settings panel styles */ #settings-panel { position: fixed; top: 50%; left: 50%; transform: translate(-50%, -50%); width: 320px; padding: 24px; border-radius: 24px; background-color: ${materialBase}; backdrop-filter: ${backdropFilter}; -webkit-backdrop-filter: ${backdropFilter}; box-shadow: ${boxShadow}; z-index: 2147483648; font-family: -apple-system, BlinkMacSystemFont, 'SF Pro Display', sans-serif; color: ${textColor}; overflow: hidden; /* Simple sleek border for settings panel too */ border: 1px solid ${borderColor}; } #settings-panel::after { content: ''; position: absolute; top: 0; left: 0; right: 0; bottom: 0; border-radius: 24px; background: ${surfaceGradient}; pointer-events: none; z-index: 1; } #settings-panel > * { position: relative; z-index: 2; } #settings-panel h3 { margin-top: 0; color: ${textColor}; text-align: center; font-size: 14px; font-weight: 600; } .setting-item { margin-bottom: 12px; } .setting-item label { display: block; margin-bottom: 4px; color: ${labelColor}; font-weight: 500; font-size: 12px; } .setting-item select, .setting-item input { width: 100%; padding: 8px 12px; border-radius: 12px; border: 1px solid ${isDarkTheme ? 'rgba(255, 255, 255, 0.15)' : 'rgba(0, 0, 0, 0.1)'}; background: ${isDarkTheme ? 'rgba(0, 0, 0, 0.3)' : 'rgba(255, 255, 255, 0.6)'}; color: ${textColor}; font-size: 13px; box-sizing: border-box; transition: all 0.2s; backdrop-filter: blur(10px); -webkit-backdrop-filter: blur(10px); } .setting-item select:focus, .setting-item input:focus { outline: none; border-color: ${blueColor}; background: ${isDarkTheme ? 'rgba(0, 0, 0, 0.4)' : 'rgba(255, 255, 255, 0.8)'}; } .setting-buttons { display: flex; justify-content: space-between; margin-top: 16px; } .setting-btn { padding: 10px 16px; border-radius: 12px; border: none; cursor: pointer; font-weight: 600; font-size: 13px; flex: 1; margin: 0 6px; transition: all 0.2s cubic-bezier(0.4, 0, 0.2, 1); backdrop-filter: blur(10px); -webkit-backdrop-filter: blur(10px); } .setting-btn:hover { transform: translateY(-1px); box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15); } .setting-btn:active { transform: translateY(0); } .save-btn { background: ${blueColor}; color: white; } .cancel-btn { background: ${isDarkTheme ? 'rgba(120, 120, 128, 0.3)' : 'rgba(120, 120, 128, 0.2)'}; color: ${textColor}; } `; } function getPositionCSS() { switch (config.settings.panelPosition) { case 'top-left': return 'top: 20px; left: 20px;'; case 'top-right': return 'top: 20px; right: 20px;'; case 'bottom-left': return 'bottom: 20px; left: 20px;'; case 'bottom-right': default: return `bottom: ${config.initialPosition.bottom}; right: ${config.initialPosition.right};`; } } function createSettingsPanel() { // Remove existing settings panel if present const existingPanel = document.getElementById('cdn-settings-panel'); if (existingPanel) existingPanel.remove(); const panel = document.createElement('div'); panel.id = 'cdn-settings-panel'; document.body.appendChild(panel); const shadowRoot = panel.attachShadow({ mode: 'open' }); const styleEl = document.createElement('style'); styleEl.textContent = getPanelCSS(); shadowRoot.appendChild(styleEl); const settingsPanel = document.createElement('div'); settingsPanel.id = 'settings-panel'; settingsPanel.innerHTML = `