Auto-PROXY-SF

Advanced privacy proxy redirector with intelligent instance selection and I2P support

目前為 2025-10-06 提交的版本,檢視 最新版本

您需要先安裝使用者腳本管理器擴展,如 TampermonkeyGreasemonkeyViolentmonkey 之後才能安裝該腳本。

You will need to install an extension such as Tampermonkey to install this script.

您需要先安裝使用者腳本管理器擴充功能,如 TampermonkeyViolentmonkey 後才能安裝該腳本。

您需要先安裝使用者腳本管理器擴充功能,如 TampermonkeyUserscripts 後才能安裝該腳本。

你需要先安裝一款使用者腳本管理器擴展,比如 Tampermonkey,才能安裝此腳本

您需要先安裝使用者腳本管理器擴充功能後才能安裝該腳本。

(我已經安裝了使用者腳本管理器,讓我安裝!)

你需要先安裝一款使用者樣式管理器擴展,比如 Stylus,才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展,比如 Stylus,才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展,比如 Stylus,才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展後才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展後才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展後才能安裝此樣式

(我已經安裝了使用者樣式管理器,讓我安裝!)

// ==UserScript==
// @name         Auto-PROXY-SF
// @description  Advanced privacy proxy redirector with intelligent instance selection and I2P support
// @namespace    https://anonymousik.is-a.dev/userscripts
// @version      1.0.1
// @author       Anonymousik
// @homepageURL  https://anonymousik.is-a.dev
// @supportURL   https://anonymousik.is-a.dev
// @license      AGPL-3.0-only
// @match        *://*/*
// @grant        GM.getValue
// @grant        GM.setValue
// @grant        GM.deleteValue
// @grant        GM.xmlHttpRequest
// @grant        GM.registerMenuCommand
// @run-at       document-start
// @connect      nadeko.net
// @connect      puffyan.us
// @connect      yewtu.be
// @connect      tux.pizza
// @connect      nitter.net
// @connect      xcancel.com
// @connect      privacydev.net
// @connect      spike.codes
// @connect      privacy.com.de
// @connect      searx.be
// @connect      mdosch.de
// @connect      pabloferreiro.es
// @icon         data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCA1MTIgNTEyIj48dGV4dCB5PSI0MDAiIGZvbnQtc2l6ZT0iNDAwIj7wn5S3PC90ZXh0Pjwvc3ZnPg==
// ==/UserScript==

(function() {
    'use strict';

    // Configuration constants
    const CONFIG = {
        VERSION: '1.0.1',
        HEALTH_CHECK_INTERVAL: 300000, // 5 minutes
        INSTANCE_TIMEOUT: 4000, // 4 seconds
        PARALLEL_CHECKS: 4,
        MAX_RETRY_ATTEMPTS: 2
    };

    // Static instance lists - no external fetching
    const I2P_INSTANCES = {
        invidious: [
            'http://inv.vern.i2p',
            'http://inv.cn.i2p',
            'http://ytmous.i2p',
            'http://tube.i2p'
        ],
        nitter: [
            'http://tm4rwkeysv3zz3q5yacyr4rlmca2c4etkdobfvuqzt6vsfsu4weq.b32.i2p'
        ],
        libreddit: [
            'http://woo5ugmoomzbtaq6z46q4wgei5mqmc6jkafqfi5c37zni7xc4ymq.b32.i2p'
        ],
        searx: [
            'http://ransack.i2p',
            'http://mqamk4cfykdvhw5kjez2gnvse56gmnqxn7vkvvbuor4k4j2lbbnq.b32.i2p'
        ],
        proxitok: [
            'http://qr.vern.i2p'
        ]
    };

    const CLEARNET_INSTANCES = {
        invidious: [
            'https://inv.nadeko.net',
            'https://vid.puffyan.us',
            'https://yewtu.be',
            'https://inv.tux.pizza'
        ],
        nitter: [
            'https://nitter.net',
            'https://xcancel.com',
            'https://nitter.privacydev.net'
        ],
        libreddit: [
            'https://libreddit.spike.codes',
            'https://libreddit.privacy.com.de'
        ],
        searx: [
            'https://searx.be',
            'https://search.mdosch.de'
        ],
        proxitok: [
            'https://proxitok.pabloferreiro.es'
        ]
    };

    // Service detection patterns
    const SERVICE_PATTERNS = {
        invidious: {
            regex: /^(?:www\.)?(?:youtube\.com|youtu\.be)$/,
            pathBuilder: function(url) {
                const videoId = url.searchParams.get('v');
                return videoId ? '/watch?v=' + videoId : url.pathname + url.search;
            }
        },
        nitter: {
            regex: /^(?:www\.)?(?:twitter\.com|x\.com)$/,
            pathBuilder: function(url) {
                return url.pathname + url.search;
            }
        },
        libreddit: {
            regex: /^(?:www\.)?(?:old\.)?reddit\.com$/,
            pathBuilder: function(url) {
                return url.pathname + url.search;
            }
        },
        searx: {
            regex: /^(?:www\.)?google\.com$/,
            pathCheck: /^\/search/,
            pathBuilder: function(url) {
                const query = url.searchParams.get('q');
                return query ? '/search?q=' + encodeURIComponent(query) : '/';
            }
        },
        proxitok: {
            regex: /^(?:www\.)?tiktok\.com$/,
            pathBuilder: function(url) {
                return url.pathname;
            }
        }
    };

    // Loading page class - expanded CSS for readability
    class LoadingPage {
        static show(targetUrl, instanceUrl) {
            const hostname = new URL(instanceUrl).hostname;
            
            // Readable, unminified HTML
            const htmlContent = `<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Auto-PROXY-SF - Redirecting</title>
    <style>
        * {
            margin: 0;
            padding: 0;
            box-sizing: border-box;
        }

        body {
            font-family: 'Segoe UI', system-ui, -apple-system, sans-serif;
            background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
            min-height: 100vh;
            display: flex;
            justify-content: center;
            align-items: center;
            color: #ffffff;
            overflow: hidden;
        }

        .container {
            text-align: center;
            padding: 2rem;
            max-width: 600px;
            animation: fadeIn 0.5s ease-in;
        }

        @keyframes fadeIn {
            from {
                opacity: 0;
                transform: translateY(-20px);
            }
            to {
                opacity: 1;
                transform: translateY(0);
            }
        }

        .logo {
            font-size: 4rem;
            margin-bottom: 1rem;
            animation: pulse 2s infinite;
        }

        @keyframes pulse {
            0%, 100% {
                transform: scale(1);
            }
            50% {
                transform: scale(1.1);
            }
        }

        h1 {
            font-size: 2.5rem;
            margin-bottom: 1rem;
            font-weight: 700;
            text-shadow: 2px 2px 4px rgba(0, 0, 0, 0.3);
        }

        .subtitle {
            font-size: 1.2rem;
            margin-bottom: 2rem;
            opacity: 0.9;
        }

        .loader {
            width: 60px;
            height: 60px;
            border: 5px solid rgba(255, 255, 255, 0.3);
            border-top: 5px solid #ffffff;
            border-radius: 50%;
            margin: 2rem auto;
            animation: spin 1s linear infinite;
        }

        @keyframes spin {
            0% {
                transform: rotate(0deg);
            }
            100% {
                transform: rotate(360deg);
            }
        }

        .status {
            font-size: 1rem;
            margin-top: 2rem;
            padding: 1rem;
            background: rgba(255, 255, 255, 0.1);
            border-radius: 10px;
            backdrop-filter: blur(10px);
        }

        .instance-info {
            margin-top: 1rem;
            font-size: 0.9rem;
            opacity: 0.8;
            word-break: break-all;
        }

        .progress-bar {
            width: 100%;
            height: 4px;
            background: rgba(255, 255, 255, 0.2);
            border-radius: 2px;
            margin-top: 2rem;
            overflow: hidden;
        }

        .progress-fill {
            height: 100%;
            background: #ffffff;
            width: 0%;
            animation: progress 3s ease-in-out;
        }

        @keyframes progress {
            0% {
                width: 0%;
            }
            100% {
                width: 100%;
            }
        }

        .footer {
            margin-top: 3rem;
            font-size: 0.85rem;
            opacity: 0.7;
        }

        .footer a {
            color: #ffffff;
            text-decoration: none;
            border-bottom: 1px solid rgba(255, 255, 255, 0.3);
            transition: border-color 0.3s;
        }

        .footer a:hover {
            border-bottom-color: #ffffff;
        }

        .particles {
            position: fixed;
            top: 0;
            left: 0;
            width: 100%;
            height: 100%;
            pointer-events: none;
            z-index: -1;
        }

        .particle {
            position: absolute;
            width: 4px;
            height: 4px;
            background: rgba(255, 255, 255, 0.5);
            border-radius: 50%;
            animation: float 10s infinite;
        }

        @keyframes float {
            0%, 100% {
                transform: translateY(0) translateX(0);
                opacity: 0;
            }
            10% {
                opacity: 1;
            }
            90% {
                opacity: 1;
            }
            100% {
                transform: translateY(-100vh) translateX(50px);
                opacity: 0;
            }
        }
    </style>
</head>
<body>
    <div class="particles" id="particles"></div>
    
    <div class="container">
        <div class="logo">🔒</div>
        <h1>Auto-PROXY-SF</h1>
        <div class="subtitle">Securing your privacy...</div>
        
        <div class="loader"></div>
        
        <div class="status">
            <div>Redirecting through privacy proxy</div>
            <div class="instance-info">Instance: ${hostname}</div>
        </div>
        
        <div class="progress-bar">
            <div class="progress-fill"></div>
        </div>
        
        <div class="footer">
            Powered by <a href="https://anonymousik.is-a.dev" target="_blank">Anonymousik</a>
            <br>
            SecFerro Division
        </div>
    </div>

    <script>
        // Create floating particles
        var particlesContainer = document.getElementById('particles');
        for (var i = 0; i < 20; i++) {
            var particle = document.createElement('div');
            particle.className = 'particle';
            particle.style.left = Math.random() * 100 + '%';
            particle.style.animationDelay = Math.random() * 10 + 's';
            particle.style.animationDuration = (Math.random() * 10 + 10) + 's';
            particlesContainer.appendChild(particle);
        }

        // Redirect after animation completes
        setTimeout(function() {
            window.location.href = '${targetUrl}';
        }, 3000);
    </script>
</body>
</html>`;

            document.open();
            document.write(htmlContent);
            document.close();
        }
    }

    // Health monitoring system
    class HealthMonitor {
        constructor() {
            this.healthData = {};
            this.checking = new Set();
        }

        async initialize() {
            try {
                const cached = await GM.getValue('healthData', '{}');
                this.healthData = JSON.parse(cached);
                this.cleanExpiredData();
            } catch (error) {
                console.error('[Auto-PROXY-SF] Health data init failed:', error);
                this.healthData = {};
            }
        }

        cleanExpiredData() {
            const now = Date.now();
            const threshold = CONFIG.HEALTH_CHECK_INTERVAL * 2;
            
            for (const url in this.healthData) {
                if (this.healthData.hasOwnProperty(url)) {
                    const data = this.healthData[url];
                    if (now - data.lastCheck > threshold) {
                        delete this.healthData[url];
                    }
                }
            }
        }

        async checkHealth(url) {
            const self = this;
            
            // Prevent duplicate checks
            if (this.checking.has(url)) {
                return new Promise(function(resolve) {
                    const interval = setInterval(function() {
                        if (!self.checking.has(url)) {
                            clearInterval(interval);
                            const health = self.healthData[url];
                            resolve(health ? health.healthy : false);
                        }
                    }, 100);
                });
            }

            // Check cache
            const cached = this.healthData[url];
            if (cached && Date.now() - cached.lastCheck < CONFIG.HEALTH_CHECK_INTERVAL) {
                return cached.healthy;
            }

            this.checking.add(url);

            return new Promise(function(resolve) {
                const startTime = Date.now();
                let resolved = false;

                const finish = function(healthy, latency) {
                    if (resolved) return;
                    resolved = true;
                    
                    self.checking.delete(url);
                    self.updateHealth(url, healthy, latency);
                    resolve(healthy);
                };

                const timeoutId = setTimeout(function() {
                    finish(false, null);
                }, CONFIG.INSTANCE_TIMEOUT);

                GM.xmlHttpRequest({
                    method: 'HEAD',
                    url: url,
                    timeout: CONFIG.INSTANCE_TIMEOUT,
                    anonymous: true,
                    onload: function(response) {
                        clearTimeout(timeoutId);
                        const latency = Date.now() - startTime;
                        const healthy = response.status >= 200 && response.status < 400;
                        finish(healthy, latency);
                    },
                    onerror: function() {
                        clearTimeout(timeoutId);
                        finish(false, null);
                    },
                    ontimeout: function() {
                        clearTimeout(timeoutId);
                        finish(false, null);
                    }
                });
            });
        }

        updateHealth(url, healthy, latency) {
            const data = this.healthData[url] || {
                healthy: false,
                lastCheck: 0,
                failures: 0,
                latencies: []
            };

            data.healthy = healthy;
            data.lastCheck = Date.now();

            if (healthy) {
                data.failures = 0;
                if (latency !== null) {
                    data.latencies.push(latency);
                    if (data.latencies.length > 10) {
                        data.latencies.shift();
                    }
                    // Calculate average latency
                    const sum = data.latencies.reduce(function(a, b) {
                        return a + b;
                    }, 0);
                    data.avgLatency = sum / data.latencies.length;
                }
            } else {
                data.failures++;
            }

            this.healthData[url] = data;
            
            // Asynchronous save
            GM.setValue('healthData', JSON.stringify(this.healthData));
        }

        getScore(url) {
            const data = this.healthData[url];
            if (!data || !data.healthy) return 0;
            
            let score = 50; // Base score for healthy instance
            
            // Latency scoring (max 30 points)
            if (data.avgLatency) {
                const latencyScore = Math.max(5, 30 - (data.avgLatency / 100) * 3);
                score += latencyScore;
            }
            
            // Reliability scoring (max 20 points)
            const reliabilityScore = Math.min(20, (10 - data.failures) * 2);
            score += reliabilityScore;
            
            return score;
        }

        getBestInstance(instances) {
            if (!instances || instances.length === 0) return null;
            
            const self = this;
            const scored = instances.map(function(url) {
                return {
                    url: url,
                    score: self.getScore(url)
                };
            }).filter(function(item) {
                return item.score > 0;
            }).sort(function(a, b) {
                return b.score - a.score;
            });
            
            return scored.length > 0 ? scored[0].url : instances[0];
        }
    }

    // Instance manager
    class InstanceManager {
        constructor() {
            this.healthMonitor = new HealthMonitor();
            this.preferredNetwork = 'clearnet';
        }

        async initialize() {
            await this.healthMonitor.initialize();
            this.preferredNetwork = await GM.getValue('preferredNetwork', 'clearnet');
        }

        async getInstances(service) {
            if (this.preferredNetwork === 'i2p') {
                return I2P_INSTANCES[service] || [];
            } else {
                return CLEARNET_INSTANCES[service] || [];
            }
        }

        async getBestInstance(service, retryCount) {
            retryCount = retryCount || 0;
            
            const instances = await this.getInstances(service);
            if (instances.length === 0) {
                console.warn('[Auto-PROXY-SF] No instances for ' + service);
                return null;
            }

            const checkCount = Math.min(CONFIG.PARALLEL_CHECKS, instances.length);
            const toCheck = instances.slice(0, checkCount);
            
            const self = this;
            const checks = toCheck.map(function(url) {
                return self.healthMonitor.checkHealth(url);
            });
            
            await Promise.all(checks);

            const best = this.healthMonitor.getBestInstance(instances);
            
            if (!best && retryCount < CONFIG.MAX_RETRY_ATTEMPTS) {
                console.log('[Auto-PROXY-SF] No healthy instance, retry ' + (retryCount + 1));
                await new Promise(function(resolve) {
                    setTimeout(resolve, 1000);
                });
                return this.getBestInstance(service, retryCount + 1);
            }

            return best;
        }

        async setNetwork(network) {
            this.preferredNetwork = network;
            await GM.setValue('preferredNetwork', network);
        }
    }

    // URL processor
    class URLProcessor {
        constructor(manager) {
            this.manager = manager;
            this.processed = new WeakSet();
        }

        detectService(hostname, pathname) {
            for (const service in SERVICE_PATTERNS) {
                if (SERVICE_PATTERNS.hasOwnProperty(service)) {
                    const pattern = SERVICE_PATTERNS[service];
                    if (pattern.regex.test(hostname)) {
                        if (pattern.pathCheck && !pattern.pathCheck.test(pathname)) {
                            continue;
                        }
                        return service;
                    }
                }
            }
            return null;
        }

        async processURL(originalUrl) {
            try {
                const url = new URL(originalUrl);
                const service = this.detectService(url.hostname, url.pathname);
                
                if (!service) return null;

                const instance = await this.manager.getBestInstance(service);
                if (!instance) return null;

                const pattern = SERVICE_PATTERNS[service];
                const newPath = pattern.pathBuilder(url);
                
                return instance + newPath;
            } catch (error) {
                console.error('[Auto-PROXY-SF] URL processing error:', error);
                return null;
            }
        }

        async processLink(linkElement) {
            if (this.processed.has(linkElement)) return;
            this.processed.add(linkElement);

            const newUrl = await this.processURL(linkElement.href);
            
            if (newUrl) {
                linkElement.dataset.originalHref = linkElement.href;
                linkElement.href = newUrl;
                linkElement.style.color = '#2ea44f';
                linkElement.title = 'Redirects via privacy proxy';
            }
        }
    }

    // Page handler
    class PageHandler {
        constructor(processor, manager) {
            this.processor = processor;
            this.manager = manager;
        }

        async checkCurrentPage() {
            const url = new URL(window.location.href);
            const service = this.processor.detectService(url.hostname, url.pathname);
            
            if (service) {
                const instance = await this.manager.getBestInstance(service);
                if (instance) {
                    const pattern = SERVICE_PATTERNS[service];
                    const targetUrl = instance + pattern.pathBuilder(url);
                    LoadingPage.show(targetUrl, instance);
                }
            }
        }

        initialize() {
            this.checkCurrentPage();

            const self = this;
            const observer = new IntersectionObserver(
                function(entries) {
                    for (let i = 0; i < entries.length; i++) {
                        const entry = entries[i];
                        if (entry.isIntersecting) {
                            self.processor.processLink(entry.target);
                            observer.unobserve(entry.target);
                        }
                    }
                },
                { rootMargin: '100px' }
            );

            const processLinks = function() {
                const links = document.querySelectorAll('a[href^="http"]:not([data-proxy-processed])');
                
                for (let i = 0; i < links.length; i++) {
                    const link = links[i];
                    link.dataset.proxyProcessed = 'true';
                    observer.observe(link);
                }
            };

            if (document.readyState === 'loading') {
                document.addEventListener('DOMContentLoaded', processLinks);
            } else {
                processLinks();
            }

            if (document.body) {
                const mutationObserver = new MutationObserver(function() {
                    if (typeof requestIdleCallback === 'function') {
                        requestIdleCallback(processLinks, { timeout: 2000 });
                    } else {
                        setTimeout(processLinks, 100);
                    }
                });

                mutationObserver.observe(document.body, {
                    childList: true,
                    subtree: true
                });
            }
        }
    }

    // Main initialization
    async function main() {
        console.log('[Auto-PROXY-SF] v' + CONFIG.VERSION + ' by Anonymousik');
        
        const manager = new InstanceManager();
        await manager.initialize();
        
        const processor = new URLProcessor(manager);
        const handler = new PageHandler(processor, manager);
        handler.initialize();

        // Menu commands
        const currentNetwork = await GM.getValue('preferredNetwork', 'clearnet');
        
        GM.registerMenuCommand('Network: ' + currentNetwork.toUpperCase(), async function() {
            const networks = ['clearnet', 'i2p'];
            const currentIndex = networks.indexOf(currentNetwork);
            const newNetwork = networks[(currentIndex + 1) % networks.length];
            await manager.setNetwork(newNetwork);
            alert('Auto-PROXY-SF: Switched to ' + newNetwork.toUpperCase());
            window.location.reload();
        });

        GM.registerMenuCommand('Clear Cache', async function() {
            await GM.deleteValue('healthData');
            manager.healthMonitor.healthData = {};
            alert('Cache cleared successfully');
        });

        GM.registerMenuCommand('About', function() {
            alert('Auto-PROXY-SF v' + CONFIG.VERSION + '\n\nAuthor: Anonymousik\nSecFerro Division\n\nhttps://anonymousik.is-a.dev');
        });
    }

    // Start when ready
    if (document.readyState === 'loading') {
        document.addEventListener('DOMContentLoaded', main);
    } else {
        main();
    }

})();