您需要先安装一个扩展,例如 篡改猴、Greasemonkey 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 Userscripts ,之后才能安装此脚本。
您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。
您需要先安装用户脚本管理器扩展后才能安装此脚本。
Greasy Fork is available in English.
[v6.1.1 Author Update] Updated author name and optimized font sizes for better CDN name display.
// ==UserScript== // @name CDN & Server Info Displayer (UI Overhaul) // @name:en CDN & Server Info Displayer (UI Overhaul) // @namespace http://tampermonkey.net/ // @version 6.1.1 // @description [v6.1.1 Author Update] Updated author name and optimized font sizes for better CDN name display. // @description:en [v6.1.1 Author Update] Updated author name and optimized font sizes for better CDN name display. // @author Zhou Sulong // @license MIT // @match *://*/* // @grant GM_addStyle // @grant GM_getValue // @grant GM_setValue // @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: 'dark', // 'dark' or 'light' panelPosition: 'bottom-right', // 'top-left', 'top-right', 'bottom-left', 'bottom-right' showExtraInfo: true, excludedUrls: [], }, }; window.cdnScriptStatus = window.cdnScriptStatus || {}; // --- Core Info Parsing Functions --- function getCacheStatus(h) { // 1. Check server-timing first as it's often the most accurate const serverTiming = h.get('server-timing'); if (serverTiming) { if (serverTiming.includes('cdn-cache; desc=HIT')) return 'HIT'; if (serverTiming.includes('cdn-cache; desc=MISS')) return 'MISS'; } const headersToCheck = [ h.get('eo-cache-status'), // Prioritize specific headers 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'), ]; 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 const cdnProviders = { Akamai: { headers: ['x-akamai-transformed', 'x-akam-sw-version'], customCheck: (h) => { const cookieHeader = h.get('set-cookie') || ''; return cookieHeader.includes('ak_bmsc=') || cookieHeader.includes('akacd_'); }, priority: 10, getInfo: (h) => { let 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(); } } return { provider: 'Akamai', cache: getCacheStatus(h), pop: pop, extra: 'Detected via Akamai header/cookie', }; }, }, 'Tencent EdgeOne': { // NEW: Added 'eo-log-uuid' for detection serverHeaders: ['edgeone-pages'], headers: ['x-nws-log-uuid', 'eo-log-uuid'], priority: 10, getInfo: (h) => { let cache = 'N/A'; // NEW: Prioritize eo-cache-status for cache info 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); } // NEW: Check for either UUID const logUuid = h.get('eo-log-uuid') || h.get('x-nws-log-uuid') || 'N/A'; return { provider: 'Tencent EdgeOne', cache: cache, pop: 'N/A', extra: `Log-UUID: ${logUuid}`, }; }, }, 'ByteDance CDN': { serverHeaders: ['Byte-nginx'], headers: ['x-tt-trace-tag', 'x-bdcdn-cache-status'], priority: 11, getInfo: (h) => { let 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') { 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); } let pop = 'N/A'; const viaHeader = h.get('via'); if (viaHeader) { const viaParts = viaHeader.split(','); for (let i = viaParts.length - 1; i >= 0; i--) { const part = viaParts[i].trim(); const cityMatch = part.match(/\.([a-zA-Z]+)/); if (cityMatch && cityMatch[1]) { if (!/cn\d+/.test(cityMatch[1])) { pop = cityMatch[1].split('-')[0].toUpperCase(); break; } } const internalCodeMatch = part.match(/\b([a-z]*cn\d+)\b/i); if (internalCodeMatch && internalCodeMatch[1]) { pop = 'CN'; break; } } } return { provider: 'ByteDance CDN', cache, pop, extra: `Trace Tag: ${h.get('x-tt-trace-tag') || 'N/A'}`, }; }, }, 'Alibaba Cloud CDN': { serverHeaders: ['Tengine'], headers: ['eagleid'], priority: 10, getInfo: (h) => { let cache = 'N/A'; 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') { const xCache = h.get('x-cache'); if (xCache) cache = getCacheStatus(h); } if (cache === 'N/A') { cache = getCacheStatus(h); } return { provider: 'Alibaba Cloud CDN', cache, pop: h.get('X-Swift-Pop') || 'N/A', extra: `EagleID: ${h.get('eagleid') || 'N/A'}`, }; }, }, BunnyCDN: { serverHeaders: ['BunnyCDN'], priority: 9, getInfo: (h) => { let pop = 'N/A'; const serverHeader = h.get('server'); if (serverHeader) { const match = serverHeader.match(/BunnyCDN-([A-Z0-9]+)/); if (match && match[1]) { pop = match[1]; } } if (pop === 'N/A') { pop = h.get('cdn-requestcountrycode')?.toUpperCase() || 'N/A'; } return { provider: 'BunnyCDN', cache: h.get('cdn-cache')?.toUpperCase() || getCacheStatus(h), pop: pop, extra: `Pullzone: ${h.get('cdn-pullzone') || 'N/A'}`, }; }, }, 'JD Cloud CDN': { headers: ['x-jss-request-id'], customCheck: (h) => (h.get('via') || '').includes('(jcs'), priority: 10, getInfo: (h) => { let pop = 'N/A'; const viaHeader = h.get('via'); if (viaHeader) { const match = viaHeader.match(/\s([A-Z]{2,3})-[A-Z]{2,}/); if (match && match[1]) { pop = match[1]; } } return { provider: 'JD Cloud CDN', cache: getCacheStatus(h), pop: pop, extra: `Req ID: ${h.get('x-jss-request-id') || 'N/A'}`, }; }, }, 'QUIC.cloud': { headers: ['x-qc-pop', 'x-qc-cache'], priority: 9, getInfo: (h) => { let pop = 'N/A'; const popHeader = h.get('x-qc-pop'); if (popHeader) { const parts = popHeader.split('-'); if (parts.length >= 3) { pop = `${parts[1]}-${parts[2]}`.toUpperCase(); } else if (parts.length === 2) { pop = popHeader.toUpperCase(); } else { pop = popHeader; } } return { provider: 'QUIC.cloud', cache: h.get('x-qc-cache')?.toUpperCase() || getCacheStatus(h), pop: pop, extra: `POP Str: ${popHeader || 'N/A'}`, }; }, }, Cloudflare: { headers: ['cf-ray'], serverHeaders: ['cloudflare'], priority: 10, getInfo: (h) => ({ provider: 'Cloudflare', cache: h.get('cf-cache-status')?.toUpperCase() || 'N/A', pop: h.get('cf-ray')?.slice(-3).toUpperCase() || 'N/A', extra: `Ray ID: ${h.get('cf-ray') || 'N/A'}`, }), }, 'AWS CloudFront': { headers: ['x-amz-cf-pop', 'x-amz-cf-id'], priority: 9, getInfo: (h) => ({ provider: 'AWS CloudFront', cache: getCacheStatus(h), pop: (h.get('x-amz-cf-pop') || 'N/A').substring(0, 3), extra: `CF ID: ${h.get('x-amz-cf-id') || 'N/A'}`, }), }, Fastly: { headers: ['x-fastly-request-id', 'x-served-by'], priority: 9, getInfo: (h) => ({ provider: 'Fastly', cache: getCacheStatus(h), pop: h.get('x-served-by')?.split('-').pop() || 'N/A', extra: `ReqID: ${h.get('x-fastly-request-id') || 'N/A'}`, }), }, Vercel: { headers: ['x-vercel-id'], priority: 10, getInfo: (h) => { let pop = 'N/A'; const vercelId = h.get('x-vercel-id'); if (vercelId) { const regionPart = vercelId.split('::')[0]; const match = regionPart.match(/^[a-zA-Z]+/); if (match) pop = match[0].toUpperCase(); } return { provider: 'Vercel', cache: getCacheStatus(h), pop: pop, extra: `ID: ${h.get('x-vercel-id') || 'N/A'}`, }; }, }, Cloudflare: { headers: ['cf-ray'], serverHeaders: ['cloudflare'], priority: 10, getInfo: (h) => ({ provider: 'Cloudflare', cache: h.get('cf-cache-status')?.toUpperCase() || 'N/A', pop: h.get('cf-ray')?.slice(-3).toUpperCase() || 'N/A', extra: `Ray ID: ${h.get('cf-ray') || 'N/A'}`, }), }, 'AWS CloudFront': { headers: ['x-amz-cf-pop', 'x-amz-cf-id'], priority: 9, getInfo: (h) => ({ provider: 'AWS CloudFront', cache: getCacheStatus(h), pop: (h.get('x-amz-cf-pop') || 'N/A').substring(0, 3), extra: `CF ID: ${h.get('x-amz-cf-id') || 'N/A'}`, }), }, Fastly: { headers: ['x-fastly-request-id', 'x-served-by'], priority: 9, getInfo: (h) => ({ provider: 'Fastly', cache: getCacheStatus(h), pop: h.get('x-served-by')?.split('-').pop() || 'N/A', extra: `ReqID: ${h.get('x-fastly-request-id') || 'N/A'}`, }), }, Vercel: { headers: ['x-vercel-id'], priority: 10, getInfo: (h) => { let pop = 'N/A'; const vercelId = h.get('x-vercel-id'); if (vercelId) { const regionPart = vercelId.split('::')[0]; const match = regionPart.match(/^[a-zA-Z]+/); if (match) pop = match[0].toUpperCase(); } return { provider: 'Vercel', cache: getCacheStatus(h), pop: pop, extra: `ID: ${h.get('x-vercel-id') || 'N/A'}`, }; }, }, 'Wovn.io': { headers: ['x-wovn-cache', 'x-wovn-surrogate-key'], priority: 9, getInfo: (h) => ({ provider: 'Wovn.io', cache: h.get('x-wovn-cache')?.toUpperCase() || 'N/A', pop: 'N/A', extra: `Cache Hits: ${h.get('x-wovn-cache-hits') || 'N/A'}`, }), }, // New CDN providers KeyCDN: { serverHeaders: ['keycdn-engine'], headers: ['x-keycdn-cache'], priority: 8, getInfo: (h) => ({ provider: 'KeyCDN', cache: h.get('x-keycdn-cache')?.toUpperCase() || getCacheStatus(h), pop: 'N/A', extra: 'KeyCDN Engine', }), }, CDN77: { serverHeaders: ['CDN77'], headers: ['x-cdn-geo', 'x-cdn-pop'], priority: 8, getInfo: (h) => { const pop = h.get('x-cdn-pop') || h.get('x-cdn-geo') || 'N/A'; return { provider: 'CDN77', cache: getCacheStatus(h), pop: pop.toUpperCase(), extra: 'CDN77 Network', }; }, }, StackPath: { serverHeaders: ['stackpath'], headers: ['x-scp-served-by', 'x-scp-cache-status'], priority: 8, getInfo: (h) => { const cache = h.get('x-scp-cache-status')?.toUpperCase() || getCacheStatus(h); const pop = 'N/A'; // StackPath doesn't typically expose POP info in headers return { provider: 'StackPath', cache: cache, pop: pop, extra: 'StackPath CDN', }; }, }, ChinaCache: { serverHeaders: ['ChinaCache'], headers: ['x-source', 'via'], customCheck: (h) => { const viaHeader = h.get('via') || ''; return viaHeader.includes('ChinaCache') || viaHeader.includes('ChinaNetCenter'); }, priority: 7, getInfo: (h) => ({ provider: 'ChinaCache', cache: getCacheStatus(h), pop: 'N/A', extra: 'ChinaNetCenter', }), }, }; // --- Extended Information Functions --- function getServerInfo(h) { const server = h.get('server'); if (!server) return 'N/A'; // Clean up server string return server.split(';')[0].trim(); // Remove additional info after semicolon } 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 to include extended information function parseInfo(response) { const h = response.headers; const lowerCaseHeaders = new Map(); for (const [key, value] of h.entries()) { lowerCaseHeaders.set(key.toLowerCase(), value); } const detectedProviders = []; for (const [_, cdn] of Object.entries(cdnProviders)) { let isMatch = false; if (cdn.customCheck && cdn.customCheck(lowerCaseHeaders)) isMatch = true; if ( !isMatch && cdn.headers?.some((header) => lowerCaseHeaders.has(header.toLowerCase())) ) isMatch = true; if ( !isMatch && cdn.serverHeaders?.some((server) => (lowerCaseHeaders.get('server') || '') .toLowerCase() .includes(server.toLowerCase()) ) ) isMatch = true; if (isMatch) { // Avoid adding if a more specific rule from humble already exists if ( !detectedProviders.some( (p) => p.provider === cdn.getInfo(lowerCaseHeaders).provider ) ) { detectedProviders.push({ ...cdn.getInfo(lowerCaseHeaders), priority: cdn.priority || 5, }); } } } if (detectedProviders.length > 0) { detectedProviders.sort((a, b) => b.priority - a.priority); const result = detectedProviders[0]; // Add extended information result.server = getServerInfo(lowerCaseHeaders); result.connection = getConnectionInfo(response); result.additional = getAdditionalInfo(lowerCaseHeaders); return result; } const server = lowerCaseHeaders.get('server'); if (server) { const result = { provider: server, cache: getCacheStatus(lowerCaseHeaders), pop: 'N/A', extra: 'No CDN detected', server: getServerInfo(lowerCaseHeaders), connection: getConnectionInfo(response), additional: getAdditionalInfo(lowerCaseHeaders), }; return result; } return null; } // --- UI & Execution Functions --- function getPanelCSS() { const isDarkTheme = config.settings.theme === 'dark'; const bgColor = isDarkTheme ? 'rgba(25, 25, 25, 0.7)' : 'rgba(255, 255, 255, 0.7)'; const borderColor = isDarkTheme ? 'rgba(255, 255, 255, 0.15)' : 'rgba(0, 0, 0, 0.15)'; const textColor = isDarkTheme ? '#ffffff' : '#000000'; const labelColor = isDarkTheme ? 'rgba(255, 255, 255, 0.7)' : 'rgba(0, 0, 0, 0.7)'; const backdropFilter = 'blur(20px)'; // iOS-style blur effect const boxShadow = isDarkTheme ? '0 10px 30px rgba(0, 0, 0, 0.3), inset 0 0 0 1px rgba(255, 255, 255, 0.1)' : '0 10px 30px rgba(0, 0, 0, 0.1), inset 0 0 0 1px rgba(255, 255, 255, 0.5)'; return ` :host { all: initial; position: fixed; z-index: 2147483647; ${getPositionCSS()} font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif; } #cdn-info-panel-enhanced { position: relative; min-width: 200px; max-width: 300px; padding: 14px; border-radius: 20px; background-color: ${bgColor}; border: 1px solid ${borderColor}; box-shadow: ${boxShadow}; backdrop-filter: ${backdropFilter}; -webkit-backdrop-filter: ${backdropFilter}; cursor: move; user-select: none; transition: transform 0.2s ease, box-shadow 0.2s ease; } #cdn-info-panel-enhanced:hover { transform: translateY(-2px); box-shadow: 0 15px 35px rgba(0, 0, 0, 0.2); } .close-btn { position: absolute; top: 8px; right: 8px; width: 22px; height: 22px; border-radius: 50%; background: transparent; color: ${labelColor}; border: none; cursor: pointer; font-size: 16px; line-height: 22px; display: flex; align-items: center; justify-content: center; transition: all 0.2s; z-index: 2; } .close-btn:hover { background: ${isDarkTheme ? 'rgba(255,255,255,0.2)' : 'rgba(0,0,0,0.1)'}; color: ${textColor}; } .panel-header { font-size: 12px; font-weight: 600; color: ${labelColor}; text-align: center; margin-bottom: 12px; padding-bottom: 8px; border-bottom: 1px solid ${borderColor}; text-transform: uppercase; letter-spacing: 0.5px; } .info-line { display: flex; justify-content: space-between; align-items: center; margin-bottom: 10px; font-size: 13px; } .info-line:last-child { margin-bottom: 0; } .info-label { color: ${labelColor}; font-weight: 500; flex: 1; overflow: hidden; text-overflow: ellipsis; } .info-value { color: ${textColor}; font-weight: 600; text-align: right; flex: 1.5; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; font-family: -apple-system, BlinkMacSystemFont, 'SF Mono', 'Menlo', 'Consolas', 'Liberation Mono', 'Courier New', monospace; font-size: 12px; } .cache-HIT { color: #34C759 !important; } .cache-MISS { color: #FF2D55 !important; } .cache-BYPASS, .cache-DYNAMIC { color: #0A84FF !important; } /* Settings panel styles */ #settings-panel { position: fixed; top: 50%; left: 50%; transform: translate(-50%, -50%); width: 300px; padding: 18px; border-radius: 20px; background-color: ${bgColor}; border: 1px solid ${borderColor}; box-shadow: ${boxShadow}; backdrop-filter: ${backdropFilter}; -webkit-backdrop-filter: ${backdropFilter}; z-index: 2147483648; font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif; } #settings-panel h3 { margin-top: 0; color: ${textColor}; text-align: center; font-size: 16px; font-weight: 600; } .setting-item { margin-bottom: 16px; } .setting-item label { display: block; margin-bottom: 5px; color: ${labelColor}; font-weight: 500; font-size: 13px; } .setting-item select, .setting-item input { width: 100%; padding: 8px 10px; border-radius: 12px; border: 1px solid ${borderColor}; background-color: ${isDarkTheme ? 'rgba(0,0,0,0.2)' : 'rgba(255,255,255,0.5)'}; color: ${textColor}; font-size: 13px; box-sizing: border-box; } .setting-buttons { display: flex; justify-content: space-between; margin-top: 20px; } .setting-btn { padding: 8px 16px; border-radius: 12px; border: none; cursor: pointer; font-weight: 600; font-size: 14px; flex: 1; margin: 0 4px; } .save-btn { background-color: #0A84FF; color: white; } .cancel-btn { background-color: ${labelColor}; color: ${bgColor}; } `; } 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 = ` <h3>CDN Info Display Settings</h3> <div class="setting-item"> <label for="theme">Theme</label> <select id="theme"> <option value="dark" ${config.settings.theme === 'dark' ? 'selected' : ''}>Dark</option> <option value="light" ${config.settings.theme === 'light' ? 'selected' : ''}>Light</option> </select> </div> <div class="setting-item"> <label for="panelPosition">Panel Position</label> <select id="panelPosition"> <option value="top-left" ${config.settings.panelPosition === 'top-left' ? 'selected' : ''}>Top Left</option> <option value="top-right" ${config.settings.panelPosition === 'top-right' ? 'selected' : ''}>Top Right</option> <option value="bottom-left" ${config.settings.panelPosition === 'bottom-left' ? 'selected' : ''}>Bottom Left</option> <option value="bottom-right" ${config.settings.panelPosition === 'bottom-right' ? 'selected' : ''}>Bottom Right</option> </select> </div> <div class="setting-item"> <label for="showExtraInfo"> <input type="checkbox" id="showExtraInfo" ${config.settings.showExtraInfo ? 'checked' : ''}> Show Extra Information </label> </div> <div class="setting-buttons"> <button class="setting-btn cancel-btn">Cancel</button> <button class="setting-btn save-btn">Save</button> </div> `; shadowRoot.appendChild(settingsPanel); // Add event listeners shadowRoot.querySelector('.cancel-btn').addEventListener('click', () => { panel.remove(); }); shadowRoot.querySelector('.save-btn').addEventListener('click', () => { // Save settings config.settings.theme = shadowRoot.querySelector('#theme').value; config.settings.panelPosition = shadowRoot.querySelector('#panelPosition').value; config.settings.showExtraInfo = shadowRoot.querySelector('#showExtraInfo').checked; // Save to GM storage if available if (typeof GM_setValue !== 'undefined') { GM_setValue('cdnInfoSettings', JSON.stringify(config.settings)); } // Close panel panel.remove(); // Re-render info panel with new settings const infoPanel = document.getElementById('cdn-info-host-enhanced'); if (infoPanel) { infoPanel.remove(); // Re-run execution to show updated panel runExecution(config.max_retries); } }); } function createDisplayPanel(info) { if (!info || document.getElementById('cdn-info-host-enhanced')) return; const host = document.createElement('div'); host.id = 'cdn-info-host-enhanced'; document.body.appendChild(host); const shadowRoot = host.attachShadow({ mode: 'open' }); const styleEl = document.createElement('style'); styleEl.textContent = getPanelCSS(); shadowRoot.appendChild(styleEl); const panel = document.createElement('div'); panel.id = 'cdn-info-panel-enhanced'; const cacheStatus = info.cache.toUpperCase(); const cacheClass = 'cache-' + cacheStatus.split(' ')[0]; const providerLabel = info.provider.includes('CDN') || info.provider.includes('Cloud') || info.provider.includes('Edge') ? 'CDN' : 'Server'; // Truncate provider name if too long let displayProvider = info.provider; if (displayProvider.length > 20) { displayProvider = displayProvider.substring(0, 17) + '...'; } // Build panel content - keep it concise let panelContent = ` <button class="close-btn" title="Close">×</button> <div class="panel-header">CDN & Server Info</div> <div class="info-line"> <span class="info-label">${providerLabel}</span> <span class="info-value" title="${info.provider}">${displayProvider}</span> </div> <div class="info-line"> <span class="info-label">Cache</span> <span class="info-value ${cacheClass}">${cacheStatus}</span> </div> `; // Add POP location if available and not N/A if (info.pop && info.pop !== 'N/A') { let displayPop = info.pop; if (displayPop.length > 12) { displayPop = displayPop.substring(0, 9) + '...'; } panelContent += ` <div class="info-line"> <span class="info-label">POP</span> <span class="info-value" title="${info.pop}">${displayPop}</span> </div> `; } panel.innerHTML = panelContent; shadowRoot.appendChild(panel); shadowRoot.querySelector('.close-btn').addEventListener('click', (e) => { e.stopPropagation(); host.remove(); }); // Add settings button (right click on panel to open settings) panel.addEventListener('contextmenu', (e) => { e.preventDefault(); createSettingsPanel(); }); makeDraggable(host); } function makeDraggable(element) { let isDragging = false, startX = 0, startY = 0, elementX = 0, elementY = 0; const dragTarget = element.shadowRoot.querySelector('#cdn-info-panel-enhanced'); dragTarget.addEventListener('mousedown', (e) => { if (e.target.classList.contains('close-btn')) return; isDragging = true; startX = e.clientX; startY = e.clientY; const rect = element.getBoundingClientRect(); elementX = rect.left; elementY = rect.top; document.addEventListener('mousemove', drag); document.addEventListener('mouseup', dragEnd); }); function drag(e) { if (!isDragging) return; e.preventDefault(); const newX = elementX + e.clientX - startX; const newY = elementY + e.clientY - startY; const maxX = window.innerWidth - element.offsetWidth; const maxY = window.innerHeight - element.offsetHeight; element.style.left = `${Math.max(0, Math.min(newX, maxX))}px`; element.style.top = `${Math.max(0, Math.min(newY, maxY))}px`; element.style.right = 'auto'; element.style.bottom = 'auto'; } function dragEnd() { isDragging = false; document.removeEventListener('mousemove', drag); document.removeEventListener('mouseup', dragEnd); } } function shouldExcludePage() { const url = window.location.href.toLowerCase(); if (config.excludePatterns.some((pattern) => pattern.test(url))) { console.log('[CDN Detector] Excluded by URL pattern.'); return true; } return false; } async function runExecution(retriesLeft) { const currentHref = window.location.href; const status = window.cdnScriptStatus; if ( status[currentHref] === 'succeeded' || shouldExcludePage() || document.getElementById('cdn-info-host-enhanced') ) return; console.log(`[CDN Detector] Attempting to fetch headers... Retries left: ${retriesLeft}`); try { const response = await fetch(currentHref, { method: 'HEAD', cache: 'no-store', redirect: 'follow', headers: { 'User-Agent': navigator.userAgent, Accept: 'text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8', }, }); const info = parseInfo(response); if (info) { createDisplayPanel(info); status[currentHref] = 'succeeded'; console.log('[CDN Detector] Success:', info); } else { throw new Error('No server info found.'); } } catch (error) { console.warn( `[CDN Detector] Fetch failed: ${error.message}. This often indicates an active security challenge.` ); status[currentHref] = 'retrying'; if (retriesLeft > 0) { console.log(`[CDN Detector] Retrying in ${config.retry_delay / 1000} seconds...`); setTimeout(() => runExecution(retriesLeft - 1), config.retry_delay); } else { console.error('[CDN Detector] Max retries reached. Aborting for this page.'); status[currentHref] = 'failed'; } } } function loadUserSettings() { // Load settings from GM storage if available if (typeof GM_getValue !== 'undefined') { try { const savedSettings = GM_getValue('cdnInfoSettings'); if (savedSettings) { const parsed = JSON.parse(savedSettings); config.settings = { ...config.settings, ...parsed }; } } catch (e) { console.warn('[CDN Detector] Failed to load user settings:', e); } } } function main() { // Load user settings loadUserSettings(); setTimeout(() => runExecution(config.max_retries), config.initial_delay); let lastUrl = window.location.href; const observer = new MutationObserver(() => { if (window.location.href !== lastUrl) { console.log('[CDN Detector] URL changed (SPA), resetting...'); lastUrl = window.location.href; const oldPanel = document.getElementById('cdn-info-host-enhanced'); if (oldPanel) oldPanel.remove(); setTimeout(() => runExecution(config.max_retries), config.initial_delay); } }); if (document.body) { observer.observe(document.body, { childList: true, subtree: true }); } else { new MutationObserver((__, obs) => { if (document.body) { observer.observe(document.body, { childList: true, subtree: true }); obs.disconnect(); } }).observe(document.documentElement, { childList: true }); } } main(); })();