CDN & Server Info Displayer Enhanced (增强版CDN及服务器信息显示)

[Shadow DOM] The definitive stable version. Using Shadow DOM for complete style isolation. 终极稳定版,使用Shadow DOM实现彻底的样式隔离。

当前为 2025-06-11 提交的版本,查看 最新版本

// ==UserScript==
// @name         CDN & Server Info Displayer Enhanced (增强版CDN及服务器信息显示)
// @namespace    http://tampermonkey.net/
// @version      2025.06.12.1
// @description  [Shadow DOM] The definitive stable version. Using Shadow DOM for complete style isolation. 终极稳定版,使用Shadow DOM实现彻底的样式隔离。
// @author       Claude (Enhanced by AI)
// @license      MIT
// @match        *://*/*
// @grant        GM_addStyle
// @run-at       document-end
// ==/UserScript==

(function() {
    'use strict';

    // --- 配置项 ---
    const config = {
        initialPosition: { bottom: '15px', right: '15px' },
        panelBgColor: 'rgba(44, 62, 80, 0.95)',
        panelTextColor: '#ffffff',
        borderColor: '#3498db',
        opacity: '0.9',
        animationDuration: '0.3s',
        minWindowSize: { width: 400, height: 300 },
        excludePatterns: [
            /(\/|&)pay(pal|ment)/i,
            /\/checkout|\/billing/i,
            /\/login|\/signin|\/auth/i,
            /(\/ads\/|ad_id=|advertisement)/i,
            /doubleclick\.net/i,
            /facebook\.com\/plugins/i,
            /twitter\.com\/widgets/i,
            /\/popup|\/modal/i
        ]
    };

    // --- CDN 服务商检测规则 (基于HTTP头) ---
    const cdnProviders = {
        'Cloudflare': { headers: ['cf-ray', 'cf-cache-status'], 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'}` }) },
        'Tencent EdgeOne': { serverHeaders: ['TencentEdgeOne'], headers: ['eo-cache-status'], priority: 10, getInfo: (h) => ({ provider: 'Tencent EdgeOne', cache: h.get('eo-cache-status')?.toUpperCase() || 'N/A', pop: 'N/A', extra: `Log-UUID: ${h.get('eo-log-uuid') || 'N/A'}` }) },
        'Fastly': { headers: ['x-fastly-request-id', 'x-served-by'], priority: 9, getInfo: (h) => ({ provider: 'Fastly', cache: h.get('x-cache')?.split(',')[0].toUpperCase() || 'N/A', pop: h.get('x-served-by')?.split('-')[0] || 'N/A', extra: `ReqID: ${h.get('x-fastly-request-id') || 'N/A'}` }) },
        'AWS CloudFront': { headers: ['x-amz-cf-pop', 'x-amz-cf-id'], priority: 9, getInfo: (h) => ({ provider: 'AWS CloudFront', cache: h.get('x-cache')?.split(' from ')[0].toUpperCase() || 'N/A', pop: h.get('x-amz-cf-pop') || 'N/A', extra: `CF ID: ${h.get('x-amz-cf-id') || 'N/A'}` }) },
        'Qiniu CDN': { headers: ['x-qiniu-zone', 'x-qnm-cache'], priority: 9, getInfo: (h) => ({ provider: 'Qiniu CDN', cache: h.get('x-qnm-cache')?.toUpperCase() || h.get('x-cache')?.split(' ')[0].toUpperCase() || 'N/A', pop: `Zone ${h.get('x-qiniu-zone') || 'N/A'}`, extra: `ReqID: ${h.get('x-reqid') || 'N/A'}` }) },
        'Alibaba Cloud CDN': { headers: ['eagleid', 'eagleeye-traceid'], serverHeaders: ['tengine', 'alicdn'], priority: 9, getInfo: (h) => { let cacheStatus = 'N/A'; const cacheHeader = (h.get('x-cache') || h.get('x-cache-remote') || '').toUpperCase(); if (cacheHeader.includes('HIT')) { cacheStatus = 'HIT'; } else if (cacheHeader.includes('MISS')) { cacheStatus = 'MISS'; } const traceId = h.get('eagleid') || h.get('eagleeye-traceid'); return { provider: 'Alibaba Cloud CDN', cache: cacheStatus, pop: 'N/A', extra: `TraceID: ${traceId || 'N/A'}` }; } },
        'Tencent Cloud CDN': { headers: ['x-cache-lookup', 'x-nws-log-uuid'], serverHeaders: ['nws'], priority: 9, getInfo: (h) => ({ provider: 'Tencent Cloud CDN', cache: h.get('x-cache-lookup')?.toUpperCase() || 'N/A', pop: h.get('x-nws-log-uuid') ? 'Available' : 'N/A', extra: null }) },
        'Akamai': { headers: ['x-akamai-transformed', 'x-check-cacheable'], priority: 9, getInfo: (h) => ({ provider: 'Akamai', cache: (h.get('x-check-cacheable') || h.get('x-cache'))?.toUpperCase() || 'N/A', pop: 'N/A', extra: h.get('x-akamai-transformed') ? 'Content transformed' : null }) },
        'QUIC.cloud': { headers: ['x-qc-cache', 'x-qc-pop'], serverHeaders: ['litespeed'], priority: 8, getInfo: (h) => ({ provider: 'QUIC.cloud', cache: h.get('x-qc-cache')?.toUpperCase() || 'N/A', pop: h.get('x-qc-pop')?.toUpperCase() || 'N/A', extra: null }) },
        'Vercel': { headers: ['x-vercel-id', 'x-vercel-cache'], priority: 8, getInfo: (h) => { let pop = 'N/A'; const vercelId = h.get('x-vercel-id'); if (vercelId) { const parts = vercelId.split('::'); if (parts.length > 1) pop = parts[1].split('-')[0].toUpperCase(); } return { provider: 'Vercel', cache: h.get('x-vercel-cache')?.toUpperCase() || 'N/A', pop: pop, extra: null }; } },
    };

    function parseInfo(headers) { const lowerCaseHeaders = new Map(); for (const [key, value] of headers.entries()) { lowerCaseHeaders.set(key.toLowerCase(), value); } let detectedProviders = []; for (const [cdnName, cdn] of Object.entries(cdnProviders)) { let isMatch = false; if (cdn.serverHeaders) { const serverHeader = lowerCaseHeaders.get('server') || ''; if (cdn.serverHeaders.some(server => serverHeader.toLowerCase().includes(server.toLowerCase()))) isMatch = true; } if (!isMatch && cdn.headers?.some(header => lowerCaseHeaders.has(header.toLowerCase()))) isMatch = true; if (isMatch) { detectedProviders.push({ ...cdn.getInfo(lowerCaseHeaders), priority: cdn.priority || 5 }); } } if (detectedProviders.length > 0) { detectedProviders.sort((a, b) => b.priority - a.priority); return detectedProviders[0]; } const server = lowerCaseHeaders.get('server'); if (server) { return { provider: server, cache: 'N/A', pop: 'N/A', extra: 'No CDN detected' }; } return { provider: 'Unknown', cache: 'N/A', pop: 'N/A', extra: 'No CDN or Server info' }; }
    
    function getPanelCSS() {
        // This function now returns the CSS text instead of injecting it.
        return `
            :host {
                all: initial;
                position: fixed;
                z-index: 99999;
                bottom: ${config.initialPosition.bottom};
                right: ${config.initialPosition.right};
            }
            #cdn-info-panel-enhanced { 
                padding: 12px 16px; 
                border-radius: 12px; 
                font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; 
                font-size: 13px; 
                color: ${config.panelTextColor}; 
                background: ${config.panelBgColor}; 
                backdrop-filter: blur(10px); 
                border: 1px solid ${config.borderColor}; 
                box-shadow: 0 8px 32px rgba(0,0,0,0.3), inset 0 1px 0 rgba(255,255,255,0.1); 
                cursor: move; 
                user-select: none; 
                transition: opacity ${config.animationDuration}, transform ${config.animationDuration}; 
                opacity: ${config.opacity}; 
                transform: scale(1); 
                min-width: 220px;
            } 
            #cdn-info-panel-enhanced:hover { 
                opacity: 1; 
                transform: scale(1.02); 
            } 
            .panel-header { 
                font-weight: 600; font-size: 14px; margin-bottom: 8px; color: ${config.borderColor}; 
                text-align: center; border-bottom: 1px solid rgba(52, 152, 219, 0.3); padding-bottom: 6px; 
            } 
            .info-line { display: flex; justify-content: space-between; align-items: center; margin: 6px 0; padding: 2px 0; } 
            .info-label { font-weight: 500; color: #a9cce3; font-size: 12px; text-transform: uppercase; letter-spacing: 0.5px; } 
            .info-value { font-weight: 600; max-width: 140px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; text-align: right; } 
            .extra-info { font-size: 11px; color: #95a5a6; font-style: italic; text-align: center; margin-top: 8px; 
                padding-top: 6px; border-top: 1px solid rgba(149, 165, 166, 0.2); word-break: break-all; 
            } 
            .cache-HIT { color: #2ecc71 !important; } 
            .cache-MISS { color: #e74c3c !important; } 
            .close-btn { 
                position: absolute; top: -8px; right: -8px; width: 20px; height: 20px; border-radius: 50%; 
                background: #e74c3c; color: white; border: none; cursor: pointer; font-size: 12px; display: none; 
                align-items: center; justify-content: center; transition: all 0.2s; 
            } 
            #cdn-info-panel-enhanced:hover .close-btn { display: flex; } 
            .close-btn:hover { background: #c0392b; transform: scale(1.1); }
        `;
    }

    function createDisplayPanel(info) {
        if (!info || document.getElementById('cdn-info-host-enhanced')) return;

        // 1. Create a host element that will live in the main DOM
        const host = document.createElement('div');
        host.id = 'cdn-info-host-enhanced';
        document.body.appendChild(host);

        // 2. Attach a Shadow DOM to the host
        const shadowRoot = host.attachShadow({ mode: 'open' });

        // 3. Create a <style> element and inject our CSS into the Shadow DOM
        const styleEl = document.createElement('style');
        styleEl.textContent = getPanelCSS();
        shadowRoot.appendChild(styleEl);

        // 4. Create the panel and append it to the Shadow DOM
        const panel = document.createElement('div');
        panel.id = 'cdn-info-panel-enhanced';
        
        const cacheStatus = info.cache.toUpperCase();
        const cacheClass = cacheStatus.includes('HIT') ? 'cache-HIT' : (cacheStatus.includes('MISS') ? 'cache-MISS' : '');
        const providerLabel = info.provider.includes('CDN') || info.provider.includes('Cloud') || info.provider.includes('Edge') || info.provider.includes('(inferred)') ? 'CDN' : 'Server';
        
        panel.innerHTML = `
            <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}">${info.provider}</span></div>
            <div class="info-line"><span class="info-label">Cache</span><span class="info-value ${cacheClass}">${info.cache}</span></div>
            <div class="info-line"><span class="info-label">POP</span><span class="info-value" title="${info.pop}">${info.pop}</span></div>
            ${info.extra ? `<div class="extra-info" title="${info.extra}">${info.extra}</div>` : ''}
        `;
        shadowRoot.appendChild(panel);
        
        // Add close functionality
        shadowRoot.querySelector('.close-btn').addEventListener('click', () => host.remove());

        // 5. Make the HOST element draggable, which moves the whole Shadow DOM
        makeDraggable(host);
    }
    
    function makeDraggable(element) { 
        let isDragging = false, startX = 0, startY = 0, elementX = 0, elementY = 0; 
        
        // Listen on the shadow root for the mousedown event
        element.shadowRoot.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; 
            // The panel itself can have transitions, but the host doesn't need them
            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 isElementVisible(el) { if (!el) return false; const rect = el.getBoundingClientRect(); return (rect.width > 1 && rect.height > 1 && getComputedStyle(el).visibility !== 'hidden' && getComputedStyle(el).display !== 'none'); }
    
    function shouldShowPanel() {
        if (window !== window.top && (window.innerWidth < config.minWindowSize.width || window.innerHeight < config.minWindowSize.height)) return false;
        const url = window.location.href.toLowerCase();
        if (config.excludePatterns.some(pattern => pattern.test(url))) {
            console.log(`[CDN Detector] URL (${url}) matches exclude pattern. Skipping.`);
            return false;
        }
        const captchaSelectors = {
            strict: [/challenge/i, /verify/i, /security check/i],
            lenient: ['[id*="captcha"]', '[class*="captcha"]', '[id*="recaptcha"]', '[class*="recaptcha"]', '[id*="hcaptcha"]', '[class*="hcaptcha"]', '[class*="cf-turnstile"]', '[id*="turnstile"]']
        };
        if (captchaSelectors.strict.some(pattern => pattern.test(document.title.toLowerCase()))) {
             console.log('[CDN Detector] Page title indicates a challenge. Skipping.');
            return false;
        }
        if (captchaSelectors.lenient.some(selector => {
            const el = document.querySelector(selector);
            if (el && isElementVisible(el)) {
                console.log(`[CDN Detector] Found a VISIBLE captcha element: ${selector}. Skipping.`);
                return true;
            }
            return false;
        })) {
            return false;
        }
        const excludeDomains = ['accounts.google.com', 'login.microsoftonline.com', 'challenges.cloudflare.com'];
        if (excludeDomains.some(domain => window.location.hostname.toLowerCase().includes(domain))) {
            console.log(`[CDN Detector] Excluded domain. Skipping.`);
            return false;
        }
        return true;
    }

    function detectFromDOM() { try { const html = document.documentElement.innerHTML.toLowerCase(); if (html.includes('/cdn-cgi/') || html.includes('cloudflare.com/privacypolicy')) { createDisplayPanel({ provider: 'Cloudflare (inferred)', cache: 'N/A', pop: 'N/A', extra: 'From page content' }); return true; } const vercelLink = Array.from(document.querySelectorAll('a[href="https://vercel.com"]')).find(a => a.innerText.toLowerCase().includes('powered by')); if (vercelLink) { createDisplayPanel({ provider: 'Vercel (inferred)', cache: 'N/A', pop: 'N/A', extra: 'From page footer' }); return true; } } catch (e) { console.error('[CDN Detector] DOM detection failed:', e); } return false; }
    
    async function main() {
        if (document.readyState !== "complete") {
            await new Promise(resolve => window.addEventListener("load", resolve, { once: true }));
        }
        if (!shouldShowPanel()) {
            console.log('[CDN Detector] Page does not meet display criteria. Script will not run.');
            return;
        }
        try {
            const response = await fetch(window.location.href, { method: 'HEAD', cache: 'no-cache', redirect: 'follow' });
            const info = parseInfo(response.headers);
            createDisplayPanel(info);
        } catch (error) {
            console.warn('[CDN Detector] HEAD request failed, trying DOM-based fallback.', error.message);
            if (!detectFromDOM()) {
                createDisplayPanel({
                    provider: 'Detection Limited',
                    cache: 'N/A', pop: 'N/A',
                    extra: 'CORS policy may have blocked analysis'
                });
            }
        }
    }

    main().catch(error => { console.error('[CDN Detector] Initialization failed:', error); });

})();

QingJ © 2025

镜像随时可能失效,请加Q群300939539或关注我们的公众号极客氢云获取最新地址