// ==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();
})();