效果
开关
// ==UserScript==
// @name Speedtest Hide IP
// @namespace https://www.speedtest.net/
// @version 1.1.0
// @description Hide the public IP address shown on Speedtest web pages, with a quick toggle.
// @author Codex
// @match https://www.speedtest.net/*
// @match https://speedtest.net/*
// @run-at document-start
// @grant GM_addStyle
// @grant GM_getValue
// @grant GM_setValue
// @grant GM_registerMenuCommand
// @grant GM_notification
// ==/UserScript==
(function () {
'use strict';
const MASK_TEXT = 'IP hidden';
const STORAGE_KEY = 'speedtest-hide-ip-enabled';
const ROOT_CLASS = 'speedtest-hide-ip-enabled';
const TOGGLE_SHORTCUT = 'Alt+Shift+I';
const originalText = new WeakMap();
let enabled = typeof GM_getValue === 'function' ? GM_getValue(STORAGE_KEY, true) : true;
let observer = null;
const ipPatterns = [
/\b(?:(?:25[0-5]|2[0-4]\d|1?\d?\d)\.){3}(?:25[0-5]|2[0-4]\d|1?\d?\d)\b/g,
/\b(?:[a-f0-9]{1,4}:){2,7}[a-f0-9]{1,4}\b/gi
];
const css = `
.${ROOT_CLASS} .js-data-ip,
.${ROOT_CLASS} [data-result-id="ip"],
.${ROOT_CLASS} [data-testid*="ip" i],
.${ROOT_CLASS} [class*="ip-address" i],
.${ROOT_CLASS} [class*="ipAddress" i],
.${ROOT_CLASS} [class*="isp-ip" i] {
color: transparent !important;
text-shadow: none !important;
position: relative !important;
user-select: none !important;
}
.${ROOT_CLASS} .js-data-ip::after,
.${ROOT_CLASS} [data-result-id="ip"]::after,
.${ROOT_CLASS} [data-testid*="ip" i]::after,
.${ROOT_CLASS} [class*="ip-address" i]::after,
.${ROOT_CLASS} [class*="ipAddress" i]::after,
.${ROOT_CLASS} [class*="isp-ip" i]::after {
content: "${MASK_TEXT}" !important;
color: #8f98b8 !important;
position: absolute !important;
inset-inline-start: 0 !important;
top: 0 !important;
}
`;
function addStyle() {
if (typeof GM_addStyle === 'function') {
GM_addStyle(css);
return;
}
const style = document.createElement('style');
style.textContent = css;
document.documentElement.appendChild(style);
}
function containsIp(text) {
return ipPatterns.some((pattern) => {
pattern.lastIndex = 0;
return pattern.test(text);
});
}
function maskTextNode(node) {
if (!enabled) return;
if (!node.nodeValue || !containsIp(node.nodeValue)) return;
if (!originalText.has(node)) {
originalText.set(node, node.nodeValue);
}
let nextText = node.nodeValue;
for (const pattern of ipPatterns) {
pattern.lastIndex = 0;
nextText = nextText.replace(pattern, MASK_TEXT);
}
node.nodeValue = nextText;
}
function shouldSkipElement(element) {
const tag = element.tagName;
return tag === 'SCRIPT' || tag === 'STYLE' || tag === 'NOSCRIPT' || tag === 'TEXTAREA' || tag === 'INPUT';
}
function walkTextNodes(root, callback) {
if (!root) return;
if (root.nodeType === Node.TEXT_NODE) {
callback(root);
return;
}
if (root.nodeType !== Node.ELEMENT_NODE || shouldSkipElement(root)) return;
const walker = document.createTreeWalker(root, NodeFilter.SHOW_TEXT, {
acceptNode(node) {
const parent = node.parentElement;
if (!parent || shouldSkipElement(parent)) return NodeFilter.FILTER_REJECT;
return NodeFilter.FILTER_ACCEPT;
}
});
while (walker.nextNode()) callback(walker.currentNode);
}
function maskIpText(root) {
if (!root) return;
walkTextNodes(root, (node) => {
if (containsIp(node.nodeValue || '')) {
maskTextNode(node);
}
});
}
function restoreIpText(root) {
walkTextNodes(root, (node) => {
if (originalText.has(node)) {
node.nodeValue = originalText.get(node);
}
});
}
function applyState() {
document.documentElement.classList.toggle(ROOT_CLASS, enabled);
if (!document.body) return;
if (enabled) {
maskIpText(document.body);
} else {
restoreIpText(document.body);
}
}
function notify(message) {
if (typeof GM_notification === 'function') {
GM_notification({ title: 'Speedtest Hide IP', text: message, timeout: 1500 });
return;
}
console.info(`[Speedtest Hide IP] ${message}`);
}
function toggleEnabled() {
enabled = !enabled;
if (typeof GM_setValue === 'function') GM_setValue(STORAGE_KEY, enabled);
applyState();
notify(`IP hiding ${enabled ? 'enabled' : 'disabled'} (${TOGGLE_SHORTCUT})`);
}
function registerControls() {
if (typeof GM_registerMenuCommand === 'function') {
GM_registerMenuCommand(`Toggle IP hiding (${TOGGLE_SHORTCUT})`, toggleEnabled);
}
document.addEventListener('keydown', (event) => {
if (event.altKey && event.shiftKey && event.code === 'KeyI') {
event.preventDefault();
toggleEnabled();
}
});
}
function startObserver() {
applyState();
observer = new MutationObserver((mutations) => {
if (!enabled) return;
for (const mutation of mutations) {
if (mutation.type === 'characterData') {
maskTextNode(mutation.target);
}
for (const node of mutation.addedNodes) {
maskIpText(node);
}
}
});
observer.observe(document.body, {
childList: true,
characterData: true,
subtree: true
});
}
addStyle();
registerControls();
document.documentElement.classList.toggle(ROOT_CLASS, enabled);
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', startObserver, { once: true });
} else {
startObserver();
}
})();