Slavar077's picture
Update app.py
1f53da8 verified
Raw
History Blame Contribute Delete
19 kB
# ═══════════════════════════════════════════════════════════════════════
# 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)