PROXY_SERVER / proxy_server.py
Askhat777's picture
Update proxy_server.py
59b65c7 verified
Raw
History Blame Contribute Delete
24.7 kB
"""
🔗 PROXY SERVER 3.5 - ПУБЛИЧНЫЙ ПРОКСИ С ЛИЦЕНЗИОННОЙ СИСТЕМОЙ
Получает данные от Safe Backend, кэширует, отправляет в МТ5 с проверкой лицензий
"""
from fastapi import FastAPI, HTTPException, Request, Query
from fastapi.middleware.cors import CORSMiddleware
import httpx
import os
import time
from datetime import datetime
from typing import Optional, Dict, Any
import uvicorn
import hashlib
# ==================== КОНФИГУРАЦИЯ ====================
class Config:
SAFE_BACKEND_URL = os.environ.get("SAFE_BACKEND_URL", "").rstrip("/")
SAFE_API_TOKEN = os.environ.get("SAFE_API_TOKEN", "")
HF_TOKEN = os.environ.get("HF_TOKEN", "") # Токен для доступа к приватному Space
CACHE_TTL = 60 # 1 минута (быстрый кэш для MT5)
MAX_REQUESTS_PER_MINUTE = 2000 # Лимит для MT5 советников
TRIAL_KEY = "TRIAL_KEY_3DAYS" # Триальный ключ
# ==================== ПРОВЕРКА ====================
print("\n" + "="*90)
print("🚀 PROXY SERVER 3.5 - ИНИЦИАЛИЗАЦИЯ")
print("="*90)
if not Config.SAFE_BACKEND_URL:
print("❌ SAFE_BACKEND_URL не установлен в Secrets!")
print("⚠️ Должен быть вида: https://username-safe-backend.hf.space")
exit(1)
if not Config.SAFE_API_TOKEN:
print("❌ SAFE_API_TOKEN не установлен в Secrets!")
exit(1)
if not Config.HF_TOKEN:
print("⚠️ HF_TOKEN не установлен - не сможем достучаться до приватного Space!")
print(f"✅ Safe Backend: {Config.SAFE_BACKEND_URL}")
print(f"✅ API Токен: {Config.SAFE_API_TOKEN[:20]}...")
print(f"✅ HF Токен: {Config.HF_TOKEN[:20] + '...' if Config.HF_TOKEN else 'не установлен'}")
print(f"✅ Триальный ключ: {Config.TRIAL_KEY}")
print(f"✅ Кэш TTL: {Config.CACHE_TTL} сек")
print(f"✅ Rate Limit: {Config.MAX_REQUESTS_PER_MINUTE}/мин")
print("="*90)
# ==================== FASTAPI ====================
app = FastAPI(
title="Gold Proxy Server 3.5",
version="3.5",
docs_url="/docs",
redoc_url="/redoc"
)
app.add_middleware(
CORSMiddleware,
allow_origins=["*"],
allow_credentials=True,
allow_methods=["*"],
allow_headers=["*"],
)
# ==================== ЛОГИРОВАНИЕ ПРОКСИ ====================
class ProxyLogger:
def __init__(self):
self.requests = []
def log_request(self, license_key: str, dte: int, client_ip: str, status: str,
cache_hit: bool, client_id: str = None, plan: str = None):
"""Логирует запросы"""
entry = {
"timestamp": datetime.now().isoformat(),
"license_key": license_key,
"client_id": client_id,
"plan": plan,
"dte": dte,
"client_ip": client_ip,
"status": status,
"cache_hit": cache_hit
}
self.requests.append(entry)
# Сокращаем для красоты
lic_display = license_key[:10] + "..." if len(license_key) > 10 else license_key
client_display = client_id[:8] + "..." if client_id and len(client_id) > 8 else client_id or "N/A"
print(f"📊 [{datetime.now().strftime('%H:%M:%S')}] {lic_display} | {client_display} | DTE={dte} {status} (cache: {'HIT' if cache_hit else 'MISS'})")
def get_stats(self) -> Dict:
"""Статистика"""
if not self.requests:
return {"total": 0, "hits": 0, "misses": 0}
hits = len([r for r in self.requests if r['cache_hit']])
misses = len([r for r in self.requests if not r['cache_hit']])
# Группировка по планам
plans = {}
for r in self.requests:
plan = r.get('plan', 'unknown')
plans[plan] = plans.get(plan, 0) + 1
return {
"total_requests": len(self.requests),
"cache_hits": hits,
"cache_misses": misses,
"hit_rate": round(hits / (hits + misses) * 100, 1) if (hits + misses) > 0 else 0,
"requests_by_plan": plans,
"last_requests": self.requests[-10:]
}
proxy_logger = ProxyLogger()
# ==================== КЭШИРОВАНИЕ ====================
class ProxyCache:
def __init__(self):
self.cache = {}
self.client = None
self.stats = {
'backend_calls': 0,
'errors': 0,
'last_success': 0,
'backend_available': False,
'trial_requests': 0,
'paid_requests': 0
}
async def initialize(self):
"""Инициализация"""
self.client = httpx.AsyncClient(
timeout=30.0,
limits=httpx.Limits(max_connections=100, max_keepalive_connections=20),
follow_redirects=True
)
print("✅ HTTP клиент инициализирован")
await self._test_backend()
async def _test_backend(self):
"""Проверка Safe Backend"""
print("🔗 Проверка соединения с Safe Backend...")
try:
headers = {
"User-Agent": "Gold-Proxy/3.5",
"X-API-Token": Config.SAFE_API_TOKEN,
}
if Config.HF_TOKEN:
headers["Authorization"] = f"Bearer {Config.HF_TOKEN}"
print(f" URL: {Config.SAFE_BACKEND_URL}")
print(f" Headers: X-API-Token={Config.SAFE_API_TOKEN[:15]}..., Authorization=Bearer {Config.HF_TOKEN[:15] if Config.HF_TOKEN else 'NONE'}...")
# Проверяем health endpoint
response = await self.client.get(
f"{Config.SAFE_BACKEND_URL}/health",
headers=headers,
timeout=10.0,
follow_redirects=True
)
if response.status_code == 200:
data = response.json()
print(f" ✅ Статус: {response.status_code}")
print(f" ✅ Ответ: {data.get('status', 'unknown')}")
print(f" ✅ Лицензий: {data.get('licenses_count', 0)}")
print(f" ✅ Триальных пользователей: {data.get('trial_users_count', 0)}")
self.stats['backend_available'] = True
print(f"\n ✅✅✅ Safe Backend РАБОТАЕТ! ✅✅✅\n")
return True
else:
print(f" ❌ {response.status_code}: {response.text[:200]}")
return False
except Exception as e:
print(f" ❌ КРИТИЧЕСКАЯ ОШИБКА: {e}")
import traceback
print(f" Traceback: {traceback.format_exc()}")
return False
def _make_key(self, license_key: str, dte: int, client_id: str = None) -> str:
"""Создает ключ кэша"""
if client_id:
return f"{license_key}_{client_id}_{dte}"
return f"{license_key}_{dte}"
async def _fetch_from_backend(self, license_key: str, dte: int, client_id: str = None) -> Optional[Dict]:
"""Запрос к Safe Backend"""
if not self.client:
return None
url = f"{Config.SAFE_BACKEND_URL}/api/mt5/data"
headers = {
"X-API-Token": Config.SAFE_API_TOKEN,
"User-Agent": "Gold-Proxy/3.5",
}
if Config.HF_TOKEN:
headers["Authorization"] = f"Bearer {Config.HF_TOKEN}"
params = {
"license_key": license_key,
"dte": dte
}
# Добавляем client_id для триального ключа
if license_key == Config.TRIAL_KEY and client_id:
params["client_id"] = client_id
try:
print(f" 📡 Запрос к Safe Backend: {url}")
print(f" 📋 Параметры: license_key={license_key[:10]}..., dte={dte}, client_id={client_id[:10] + '...' if client_id and len(client_id) > 10 else client_id}")
response = await self.client.get(
url,
params=params,
headers=headers,
timeout=15.0
)
if response.status_code == 200:
data = response.json()
self.stats['backend_calls'] += 1
self.stats['last_success'] = time.time()
self.stats['backend_available'] = True
# Статистика по типу запросов
if license_key == Config.TRIAL_KEY:
self.stats['trial_requests'] += 1
else:
self.stats['paid_requests'] += 1
print(f" ✅ Успешно: {len(data.get('data', {}).get('calls', []))} calls, {len(data.get('data', {}).get('puts', []))} puts")
return data
elif response.status_code == 401:
print(f" 🔐 401: Неверный токен или лицензия!")
# Пытаемся получить детали ошибки
try:
error_data = response.json()
print(f" Ошибка: {error_data.get('detail', 'Unknown')[:100]}")
except:
pass
return None
elif response.status_code == 403:
print(f" 🔒 403: Доступ запрещен!")
return None
elif response.status_code == 429:
print(f" ⚠️ 429: Превышен лимит запросов!")
return None
else:
print(f" ❌ {response.status_code}: {response.text[:100]}")
return None
except Exception as e:
self.stats['errors'] += 1
print(f" ❌ Ошибка запроса: {type(e).__name__}: {str(e)}")
return None
async def get(self, license_key: str, dte: int, client_id: str = None) -> Optional[Dict]:
"""Получает данные"""
cache_key = self._make_key(license_key, dte, client_id)
current_time = time.time()
# Проверяем кэш
if cache_key in self.cache:
entry = self.cache[cache_key]
if current_time - entry['timestamp'] < Config.CACHE_TTL:
age = current_time - entry['timestamp']
print(f" 💾 Кэш HIT (возраст: {age:.1f}s)")
return entry['data']
# Запрашиваем от бэкенда
print(f" 📡 Запрос к Safe Backend...")
data = await self._fetch_from_backend(license_key, dte, client_id)
if data:
self.cache[cache_key] = {
'data': data,
'timestamp': current_time,
'source': 'backend',
'license_key': license_key,
'client_id': client_id
}
return data
# Используем старые данные если есть (stale cache)
if cache_key in self.cache:
entry = self.cache[cache_key]
age = current_time - entry['timestamp']
if age < Config.CACHE_TTL * 3: # Используем до 3 минут
print(f" ⚠️ Кэш STALE (возраст: {age:.1f}s, лимит: {Config.CACHE_TTL * 3}s)")
return entry['data']
else:
print(f" ❌ Кэш слишком старый ({age:.1f}s), не используем")
return None
def get_stats(self) -> Dict:
"""Статистика"""
now = time.time()
backend_age = round(now - self.stats['last_success'], 1) if self.stats['last_success'] > 0 else 9999
# Анализ кэша
cache_info = {}
for key, entry in list(self.cache.items())[:10]:
cache_info[key] = {
'age': round(now - entry['timestamp'], 1),
'license': entry['license_key'],
'client_id': entry['client_id'][:8] + '...' if entry['client_id'] and len(entry['client_id']) > 8 else entry['client_id']
}
return {
'cache_size': len(self.cache),
'backend_calls': self.stats['backend_calls'],
'trial_requests': self.stats['trial_requests'],
'paid_requests': self.stats['paid_requests'],
'errors': self.stats['errors'],
'backend_available': self.stats['backend_available'],
'backend_age_seconds': backend_age,
'backend_status': 'healthy' if backend_age < 300 else 'unhealthy',
'cached_entries': cache_info
}
cache = ProxyCache()
# ==================== RATE LIMITER ====================
class RateLimiter:
def __init__(self):
self.requests = {}
def check_limit(self, client_ip: str) -> bool:
now = time.time()
if client_ip not in self.requests:
self.requests[client_ip] = []
# Удаляем старые записи (старше 60 секунд)
self.requests[client_ip] = [
ts for ts in self.requests[client_ip]
if now - ts < 60
]
if len(self.requests[client_ip]) >= Config.MAX_REQUESTS_PER_MINUTE:
print(f" ⚠️ Rate limit для IP {client_ip}: {len(self.requests[client_ip])}/{Config.MAX_REQUESTS_PER_MINUTE}")
return False
self.requests[client_ip].append(now)
return True
rate_limiter = RateLimiter()
# ==================== API ====================
@app.get("/")
async def root():
"""Главная страница"""
cache_stats = cache.get_stats()
proxy_stats = proxy_logger.get_stats()
return {
"name": "Gold Proxy Server 3.5",
"version": "3.5",
"status": "online",
"architecture": "MT5 → Proxy (публичный) → Safe Backend (приватный) → Firebase",
"backend_url": Config.SAFE_BACKEND_URL,
"trial_key": Config.TRIAL_KEY,
"cache_stats": cache_stats,
"proxy_stats": proxy_stats,
"config": {
"cache_ttl": Config.CACHE_TTL,
"rate_limit": Config.MAX_REQUESTS_PER_MINUTE,
"has_hf_token": bool(Config.HF_TOKEN)
},
"contacts": {
"email": "omaralinovaskar95@gmail.com",
"website": "https://askhat707.github.io/gold-options-site/",
"buy_url": "https://askhat707.github.io/gold-options-site/buy"
},
"timestamp": datetime.now().isoformat()
}
@app.get("/health")
async def health():
"""Проверка здоровья"""
cache_stats = cache.get_stats()
backend_ok = cache_stats['backend_age_seconds'] < 300
return {
"status": "healthy" if backend_ok else "warning",
"backend_connection": backend_ok,
"backend_age_seconds": cache_stats['backend_age_seconds'],
"cache": cache_stats,
"proxy": proxy_logger.get_stats(),
"timestamp": datetime.now().isoformat()
}
@app.get("/test")
async def test():
"""Тестовый эндпоинт"""
print("\n🧪 ТЕСТОВЫЙ ЗАПРОС...")
test_cases = [
{"license_key": Config.TRIAL_KEY, "client_id": "TEST_CLIENT_001"},
{"license_key": "TEST_KEY_12345", "client_id": None},
{"license_key": "CLIENT_PRO_001", "client_id": None}
]
results = {}
for i, test_case in enumerate(test_cases):
print(f"\n🔍 Тест #{i+1}: {test_case['license_key']}")
data = await cache.get(test_case["license_key"], 0, test_case["client_id"])
if data:
results[test_case["license_key"]] = {
"success": True,
"calls": len(data['data']['calls']),
"puts": len(data['data']['puts']),
"license_plan": data.get('license_info', {}).get('plan', 'unknown'),
"warning": data.get('warning', 'none')
}
else:
results[test_case["license_key"]] = {"success": False}
return {
"status": "ok",
"message": "Proxy тестирование",
"results": results,
"cache_stats": cache.get_stats(),
"timestamp": datetime.now().isoformat()
}
@app.get("/api/mt5/data")
async def proxy_mt5_data(
request: Request,
license_key: str = Query(..., description="Ключ лицензии"),
dte: int = Query(0, description="Days to Expiration (0, 1, 2)"),
client_id: Optional[str] = Query(None, description="Идентификатор клиента (обязателен для триального ключа)")
):
"""
Основной API для МТ5 СОВЕТНИКОВ
Параметры:
- license_key: ключ лицензии (TRIAL_KEY_3DAYS для триала, или купленный ключ)
- dte: 0, 1 или 2
- client_id: обязателен для триального ключа
"""
client_ip = request.client.host if request.client else "unknown"
print(f"\n📥 МТ5 ЗАПРОС")
print(f" IP: {client_ip}")
print(f" Лицензия: {license_key}")
print(f" Client ID: {client_id}")
print(f" DTE: {dte}")
# Проверка DTE
if dte not in [0, 1, 2]:
proxy_logger.log_request(license_key, dte, client_ip, "ERROR", False, client_id)
raise HTTPException(status_code=400, detail="DTE must be 0, 1, or 2")
# Проверка client_id для триального ключа
if license_key == Config.TRIAL_KEY and not client_id:
proxy_logger.log_request(license_key, dte, client_ip, "NO_CLIENT_ID", False, client_id)
raise HTTPException(
status_code=400,
detail="Для триального ключа необходим client_id. Перезагрузите советник."
)
# Rate limit
if not rate_limiter.check_limit(client_ip):
proxy_logger.log_request(license_key, dte, client_ip, "RATE_LIMIT", False, client_id)
raise HTTPException(
status_code=429,
detail=f"Rate limit exceeded ({Config.MAX_REQUESTS_PER_MINUTE}/min)"
)
# Получаем данные
data = await cache.get(license_key, dte, client_id)
if not data:
proxy_logger.log_request(license_key, dte, client_ip, "BACKEND_ERROR", False, client_id)
# Более информативное сообщение об ошибке
error_detail = "Не удалось получить данные от сервера. Попробуйте позже."
if license_key == Config.TRIAL_KEY:
error_detail += "\n\nВозможно, ваш триальный период истек."
error_detail += "\n📧 Контакты: omaralinovaskar95@gmail.com"
error_detail += "\n🌐 Сайт: https://askhat707.github.io/gold-options-site/"
raise HTTPException(
status_code=503,
detail=error_detail
)
# Логируем успешный запрос
cache_key = cache._make_key(license_key, dte, client_id)
cache_hit = False
if cache_key in cache.cache:
entry = cache.cache[cache_key]
cache_hit = (time.time() - entry['timestamp'] < Config.CACHE_TTL)
# Получаем план для логирования
plan = data.get('license_info', {}).get('plan', 'unknown')
proxy_logger.log_request(license_key, dte, client_ip, "OK", cache_hit, client_id, plan)
print(f"📤 Ответ: {len(data['data']['calls'])} calls, {len(data['data']['puts'])} puts")
print(f" План: {plan}, Осталось запросов: {data['license_info'].get('requests_remaining', 'N/A')}")
if data.get('warning'):
print(f" ⚠️ Предупреждение: {data['warning']}")
return data
# ==================== ЭНДПОИНТ ДЛЯ ПРОВЕРКИ ЛИЦЕНЗИИ ====================
@app.get("/api/license/check")
async def check_license(
license_key: str = Query(...),
client_id: Optional[str] = Query(None)
):
"""Проверка лицензии через прокси"""
url = f"{Config.SAFE_BACKEND_URL}/api/license/check"
params = {
"license_key": license_key
}
if license_key == Config.TRIAL_KEY and client_id:
params["client_id"] = client_id
headers = {
"X-API-Token": Config.SAFE_API_TOKEN,
"User-Agent": "Gold-Proxy/3.5",
}
if Config.HF_TOKEN:
headers["Authorization"] = f"Bearer {Config.HF_TOKEN}"
try:
async with httpx.AsyncClient(timeout=10.0) as client:
response = await client.get(url, params=params, headers=headers)
if response.status_code == 200:
return response.json()
else:
return {
"is_valid": False,
"message": f"Ошибка проверки лицензии: {response.status_code}",
"license": {
"key": license_key,
"plan": "unknown",
"active": False
}
}
except Exception as e:
return {
"is_valid": False,
"message": f"Ошибка соединения: {str(e)}",
"license": {
"key": license_key,
"plan": "unknown",
"active": False
}
}
@app.on_event("startup")
async def startup_event():
await cache.initialize()
print("\n" + "="*90)
print("🚀 PROXY SERVER 3.5 ЗАПУЩЕН")
print("="*90)
print(f"Время: {datetime.now().isoformat()}")
print(f"Safe Backend: {Config.SAFE_BACKEND_URL}")
print(f"Триальный ключ: {Config.TRIAL_KEY}")
print(f"Кэш TTL: {Config.CACHE_TTL} сек")
print(f"Rate Limit: {Config.MAX_REQUESTS_PER_MINUTE}/мин")
print(f"Архитектура:")
print(" ┌─────────┐ 1000 запросов/мин ┌─────────────┐ 1 запрос/мин ┌──────────────────┐ 1 запрос/3 мин ┌─────────────┐")
print(" │ MT5 │─────────────────────────▶│ PROXY │────────────────────▶│ SAFE BACKEND │────────────────────▶│ FIREBASE │")
print(" │Советник │◀─────────────────────────│ (публичный)│◀───────────────────│ (приватный) │◀───────────────────│ │")
print(" └─────────┘ данные из кэша прокси └─────────────┘ данные из кэша бэкенда└──────────────────┘ реальные данные └─────────────┘")
print("="*90)
print("📌 ИНФОРМАЦИЯ ДЛЯ ПОЛЬЗОВАТЕЛЕЙ:")
print(f" Триальный ключ: {Config.TRIAL_KEY}")
print(f" Триальный период: 3 дня")
print(f" Проверка лицензии: каждые 15 минут")
print(f" Контакты: omaralinovaskar95@gmail.com")
print(f" Сайт: https://askhat707.github.io/gold-options-site/")
print("="*90)
if __name__ == "__main__":
port = int(os.environ.get("PORT", 7860))
uvicorn.run(app, host="0.0.0.0", port=port, log_level="info")