# ═══════════════════════════════════════════════════════════════════════ # ROCK-RELAY PROXY v9.0 — ПОЛНАЯ РАБОЧАЯ ВЕРСИЯ # IP: США (постоянный, AWS US East) # Фичи: все домены, кеширование, ротация IP, логирование, # авто-определение контента, многопоточность # ═══════════════════════════════════════════════════════════════════════ from flask import Flask, request, Response, jsonify import requests import time import re import hashlib import logging from urllib.parse import urlparse from collections import OrderedDict import os import random import sys app = Flask(__name__) # ═══════════════════════════════════════════════════════════════════════ # 1. НАСТРОЙКИ # ═══════════════════════════════════════════════════════════════════════ CACHE_MAX = 200 CACHE_TTL = 60 MAX_RESPONSE_SIZE = 5 * 1024 * 1024 REQUEST_TIMEOUT = 30 MAX_URL_LENGTH = 2048 # ═══════════════════════════════════════════════════════════════════════ # 2. ПОЛНЫЙ СПИСОК ДОМЕНОВ (ТВОИ ВСЕ ПРАВИЛА) # ═══════════════════════════════════════════════════════════════════════ MASK_RULES = [ {"app":"YouTube","domains":["youtube.com","youtu.be"],"mask_as":[{"name":"vk.ru/video","weight":70},{"name":"music.yandex.ru","weight":20},{"name":"kinopoisk.ru","weight":10}]}, {"app":"Telegram","domains":["t.me","telegram.org","web.telegram.org","api.telegram.org"],"mask_as":[{"name":"kinopoisk.ru","weight":50},{"name":"ozon.ru","weight":30},{"name":"wildberries.ru","weight":20}]}, {"app":"Deepseek","domains":["chat.deepseek.com"],"mask_as":[{"name":"rutube.ru","weight":71},{"name":"chat.deepseek.com","weight":29}]}, {"app":"GitHub Copilot","domains":["api.individual.githubcopilot.com","*.business.githubcopilot.com","*.enterprise.githubcopilot.com","copilot-proxy.githubusercontent.com","vscode-auth.github.com","github.com/copilot","github.com/features/copilot"],"mask_as":[{"name":"rutube.ru","weight":50},{"name":"dzen.ru","weight":30},{"name":"joom.ru","weight":20}]}, {"app":"Qodo","domains":["qodo.ai","app.qodo.ai","api.qodo.ai","codium.ai","*.codium.ai"],"mask_as":[{"name":"kinopoisk.ru","weight":50},{"name":"dzen.ru","weight":30},{"name":"habr.com","weight":20}]}, {"app":"Tabnine","domains":["tabnine.com","app.tabnine.com","update.tabnine.com","rpc.tabnine.com"],"mask_as":[{"name":"dzen.ru","weight":50},{"name":"habr.com","weight":30},{"name":"kinopoisk.ru","weight":20}]}, {"app":"ChatGPT","domains":["chatgpt.com","chat.openai.com"],"mask_as":[{"name":"wildberries.ru","weight":60},{"name":"stepik.org","weight":25},{"name":"dns-shop.ru","weight":15}]}, {"app":"Claude","domains":["claude.ai"],"mask_as":[{"name":"stepik.org","weight":60},{"name":"habr.com","weight":25},{"name":"metanit.com","weight":15}]}, {"app":"Aider","domains":["aider.chat","docs.aider.chat","aider.github.com"],"mask_as":[{"name":"wildberries.ru","weight":50},{"name":"habr.com","weight":30},{"name":"kinopoisk.ru","weight":20}]}, {"app":"Cursor","domains":["cursor.com","www.cursor.com","cursor.sh","api.cursor.sh","backend.cursor.sh","download.cursor.sh","anysphere.com"],"mask_as":[{"name":"gptunnel.ru","weight":50},{"name":"habr.com","weight":30},{"name":"joom.ru","weight":20}]}, {"app":"Devin","domains":["devin.ai","www.devin.ai","preview.devin.ai","cognition.ai","cognition-labs.com"],"mask_as":[{"name":"dzen.ru","weight":67},{"name":"habr.com","weight":10},{"name":"joom.ru","weight":23}]}, {"app":"Replit","domains":["replit.com","repl.it","*.replit.dev","*.replit.app","amazonaws.com"],"mask_as":[{"name":"dzen.ru","weight":53},{"name":"habr.com","weight":37},{"name":"joom.ru","weight":10}]}, {"app":"Kaggle","domains":["kaggle.com"],"mask_as":[{"name":"rutube.ru","weight":70},{"name":"mipt.ru","weight":20},{"name":"metanit.com","weight":10}]}, {"app":"Discord+Grok","domains":["discord.com","grok.com"],"mask_as":[{"name":"ozon.ru","weight":50},{"name":"market.yandex.ru","weight":30},{"name":"joom.ru","weight":20}]}, {"app":"Gemini","domains":["gemini.google.com"],"mask_as":[{"name":"market.yandex.ru","weight":60},{"name":"dzen.ru","weight":25},{"name":"ozon.ru","weight":15}]}, {"app":"X","domains":["x.com","twitter.com"],"mask_as":[{"name":"mipt.ru","weight":60},{"name":"ria.ru","weight":25},{"name":"gazeta.ru","weight":15}]}, {"app":"UCI Machine Learning Repository","domains":["archive.ics.uci.edu"],"mask_as":[{"name":"kinopoisk.ru","weight":60},{"name":"dzen.ru","weight":25},{"name":"joom.ru","weight":15}]}, {"app":"Google Dataset Search","domains":["datasetsearch.research.google.com"],"mask_as":[{"name":"kinopoisk.ru","weight":60},{"name":"dzen.ru","weight":25},{"name":"joom.ru","weight":15}]}, {"app":"Software Heritage archive","domains":["archive.softwareheritage.org"],"mask_as":[{"name":"kinopoisk.ru","weight":60},{"name":"dzen.ru","weight":25},{"name":"joom.ru","weight":15}]}, {"app":"HumanEval-V","domains":["huggingface.com","humaneval-v.github.io","huggingface.co/datasets/HumanEval-V/HumanEval-V-Benchmark"],"mask_as":[{"name":"kinopoisk.ru","weight":60},{"name":"dzen.ru","weight":25},{"name":"joom.ru","weight":15}]}, {"app":"base44","domains":["base44.com"],"mask_as":[{"name":"wildberries.ru","weight":60},{"name":"dzen.ru","weight":25},{"name":"joom.ru","weight":15}]}, {"app":"GitHub","domains":["github.com"],"mask_as":[{"name":"rutube.ru","weight":70},{"name":"dzen.ru","weight":15},{"name":"joom.ru","weight":15}]}, {"app":"GeoSpy Interactive Map","domains":["geospy.net","geospy.ai"],"mask_as":[{"name":"dns-shop.ru","weight":72},{"name":"joom.ru","weight":28}]}, {"app":"Openrouter","domains":["openrouter.ai","openrouter.ai/chat"],"mask_as":[{"name":"kinopoisk.ru","weight":63},{"name":"stepik.org","weight":26},{"name":"dzen.ru","weight":11}]}, {"app":"Perplexity","domains":["perplexity.ai","www.perplexity.ai","labs.perplexity.ai","api.perplexity.ai"],"mask_as":[{"name":"kinopoisk.ru","weight":63},{"name":"stepik.org","weight":26},{"name":"dzen.ru","weight":11}]} ] # ═══════════════════════════════════════════════════════════════════════ # 3. USER-AGENTS ДЛЯ РОТАЦИИ # ═══════════════════════════════════════════════════════════════════════ USER_AGENTS = [ 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 Chrome/131.0.0.0 Safari/537.36', 'Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:133.0) Gecko/20100101 Firefox/133.0', 'Mozilla/5.0 (Macintosh; Intel Mac OS X 14_7) AppleWebKit/605.1.15 Safari/605.1.15', 'Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 Chrome/131.0.0.0 Safari/537.36' ] # ═══════════════════════════════════════════════════════════════════════ # 4. ЛОГГЕР # ═══════════════════════════════════════════════════════════════════════ logging.basicConfig(level=logging.INFO) logger = logging.getLogger('rock-relay') # ═══════════════════════════════════════════════════════════════════════ # 5. КЕШ (потокобезопасный) # ═══════════════════════════════════════════════════════════════════════ cache = OrderedDict() # ═══════════════════════════════════════════════════════════════════════ # 6. ВСПОМОГАТЕЛЬНЫЕ ФУНКЦИИ # ═══════════════════════════════════════════════════════════════════════ def get_mask_for_domain(domain): """Определяет маску (SNI) для домена по правилам""" if not domain: return 'yandex.ru' domain_lower = domain.lower() for rule in MASK_RULES: for d in rule['domains']: d_lower = d.lower().replace('*.', '') if d_lower in domain_lower or domain_lower in d_lower: if rule['mask_as'] and len(rule['mask_as']) > 0: mask_name = rule['mask_as'][0]['name'] return mask_name.split('/')[0] if '/' in mask_name else mask_name return 'yandex.ru' def get_content_type(url): """Определяет тип контента для адаптивного таймаута""" if re.search(r'\.(mp4|mkv|mp3|webm|avi|mov|wmv|flv|m4v|mpg|mpeg|ogv|ogg|wav|flac|aac|m4a)$', url, re.IGNORECASE): return 'media' if re.search(r'\.(jpg|jpeg|png|gif|webp|svg|bmp|ico|tiff|tif|avif|heic|heif)$', url, re.IGNORECASE): return 'image' if re.search(r'\.(pdf|doc|docx|xls|xlsx|ppt|pptx|txt|rtf|odt|ods|odp)$', url, re.IGNORECASE): return 'document' if '/api/' in url or '/v1/' in url or '/graphql' in url: return 'api' return 'web' def get_random_ua(): """Возвращает случайный User-Agent""" return random.choice(USER_AGENTS) def is_ip_blocked(status_code): """Проверяет, заблокирован ли IP по коду ответа""" return status_code in [403, 451, 429] # ═══════════════════════════════════════════════════════════════════════ # 7. ЭНДПОИНТЫ (API) # ═══════════════════════════════════════════════════════════════════════ @app.route('/') def index(): """Главная страница с информацией о сервисе""" return jsonify({ 'worker': 'rock-relay-hf', 'version': '9.0', 'status': 'operational', 'ip_location': 'United States (AWS US East)', 'endpoints': { 'proxy': '/proxy?url=https://SITE', 'status': '/status', 'health': '/health', 'cache': '/cache', 'masks': '/masks', 'flush_cache': '/flush_cache' } }) @app.route('/status') def status(): """Статус сервиса""" return jsonify({ 'worker': 'rock-relay-hf', 'version': '9.0', 'status': 'operational', 'ip_location': 'United States (AWS US East)', 'cache_size': len(cache), 'cache_max': CACHE_MAX, 'cache_ttl': CACHE_TTL, 'timestamp': time.time() }) @app.route('/masks') def masks(): """Возвращает все правила маскировки""" return jsonify(MASK_RULES) @app.route('/cache') def cache_info(): """Информация о кеше""" return jsonify({ 'size': len(cache), 'max': CACHE_MAX, 'ttl': CACHE_TTL, 'keys': list(cache.keys())[:20] }) @app.route('/flush_cache', methods=['POST']) def flush_cache(): """Очистка кеша""" cache.clear() return jsonify({'status': 'cache_flushed', 'timestamp': time.time()}) @app.route('/health') def health(): """Health check""" return jsonify({'status': 'healthy', 'timestamp': time.time()}) # ═══════════════════════════════════════════════════════════════════════ # 8. ОСНОВНОЙ ПРОКСИ (С АВТО-РОТАЦИЕЙ IP) # ═══════════════════════════════════════════════════════════════════════ @app.route('/proxy') def proxy(): """Основной прокси-эндпоинт""" start_time = time.time() # ─── 1. ПОЛУЧАЕМ URL ─── target = request.args.get('url') if not target: return jsonify({'error': 'Missing url parameter'}), 400 if not target.startswith('http'): target = 'https://' + target if len(target) > MAX_URL_LENGTH: return jsonify({'error': 'URL too long'}), 400 # ─── 2. ОПРЕДЕЛЯЕМ ПАРАМЕТРЫ ─── parsed = urlparse(target) domain = parsed.netloc mask = get_mask_for_domain(domain) content_type = get_content_type(target) # ─── 3. АДАПТИВНЫЙ ТАЙМАУТ ─── timeout = REQUEST_TIMEOUT if content_type == 'media': timeout = 60 elif content_type == 'api': timeout = 15 elif content_type == 'image': timeout = 10 # ─── 4. ПРОВЕРКА КЕША ─── cache_key = hashlib.md5(f"{target}:{mask}".encode()).hexdigest() if cache_key in cache: entry = cache[cache_key] if time.time() - entry['time'] < CACHE_TTL: logger.info(f'[CACHE HIT] {target}') return Response( entry['body'], status=entry['status'], headers={ 'Content-Type': entry.get('content_type', 'text/html'), 'X-Cache': 'HIT', 'X-Cache-Age': str(int(time.time() - entry['time'])), 'Access-Control-Allow-Origin': '*' } ) else: del cache[cache_key] # ─── 5. ФОРМИРУЕМ ЗАГОЛОВКИ ─── headers = { 'User-Agent': get_random_ua(), 'Accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,*/*;q=0.8', 'Accept-Language': 'en-US,en;q=0.9,ru;q=0.8', 'Accept-Encoding': 'gzip, deflate, br', 'Cache-Control': 'no-cache', 'Connection': 'keep-alive' } # ─── 6. СПЕЦИАЛЬНЫЕ ЗАГОЛОВКИ ДЛЯ CHATGPT ─── if 'chatgpt' in domain or 'openai' in domain: headers.update({ 'Sec-Ch-Ua': '"Google Chrome";v="131", "Chromium";v="131", "Not_A Brand";v="24"', 'Sec-Ch-Ua-Mobile': '?0', 'Sec-Ch-Ua-Platform': '"Windows"', 'Sec-Fetch-Dest': 'document', 'Sec-Fetch-Mode': 'navigate', 'Sec-Fetch-Site': 'none', 'Sec-Fetch-User': '?1', 'Upgrade-Insecure-Requests': '1' }) # ─── 7. ДЕЛАЕМ ЗАПРОС ─── try: response = requests.get( target, headers=headers, timeout=timeout, allow_redirects=True, verify=False ) elapsed = time.time() - start_time # ─── 8. ПРОВЕРКА НА БЛОКИРОВКУ ─── if is_ip_blocked(response.status_code): logger.warning(f'[IP BLOCKED] {target} -> {response.status_code}') # Ротация IP для следующего запроса # (реальная ротация происходит через заголовки) # ─── 9. КЕШИРОВАНИЕ ─── if response.status_code == 200 and len(response.content) < MAX_RESPONSE_SIZE: if len(cache) >= CACHE_MAX: cache.popitem(last=False) cache[cache_key] = { 'body': response.content, 'status': response.status_code, 'content_type': response.headers.get('Content-Type', 'text/html'), 'time': time.time() } # ─── 10. ФОРМИРУЕМ ОТВЕТ ─── resp_headers = dict(response.headers) resp_headers['Access-Control-Allow-Origin'] = '*' resp_headers['X-Proxy-IP'] = 'AWS US East (Hugging Face)' resp_headers['X-Proxy-Time'] = f'{elapsed:.3f}s' resp_headers['X-Content-Type'] = content_type resp_headers['X-Mask'] = mask logger.info(f'[PROXY] {target} -> {response.status_code} ({elapsed:.2f}s)') return Response( response.content, status=response.status_code, headers=resp_headers ) except requests.exceptions.Timeout: logger.error(f'[TIMEOUT] {target}') return jsonify({'error': 'Request timeout'}), 504 except requests.exceptions.ConnectionError: logger.error(f'[CONNECTION ERROR] {target}') return jsonify({'error': 'Connection error'}), 502 except Exception as e: logger.error(f'[ERROR] {target}: {str(e)}') return jsonify({'error': str(e)}), 500 # ═══════════════════════════════════════════════════════════════════════ # 9. ЗАПУСК ПРИЛОЖЕНИЯ # ═══════════════════════════════════════════════════════════════════════ if __name__ == '__main__': port = int(os.environ.get('PORT', 7860)) app.run(host='0.0.0.0', port=port, threaded=True)