Auto-PROXY-SF

Advanced privacy proxy redirector with intelligent instance selection and I2P support

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

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

})();