import uuid import random import asyncio from datetime import datetime, timedelta import re from playwright.async_api import async_playwright from urllib.parse import urlparse, urlencode, parse_qs, urlunparse import logging import os from aiohttp import web, ClientSession, WSMsgType from markupsafe import escape from threading import Lock import json # import ssl # Удаляем импорт ssl, так как Nginx будет обрабатывать SSL class PaymentSystem: """ A class to manage the payment system with URL cleaning and redirect proxying, running entirely on AIOHTTP. """ # --- Configuration --- # Изменяем порт на внутренний HTTP-порт, так как Nginx будет обрабатывать HTTPS PROXY_PORT = 8080 DONATION_URL = os.getenv("DONATION_URL", "https://www.donationalerts.com/r/galaxymine") WIDGET_TOKEN = os.getenv("WIDGET_TOKEN", "jgpyLAyF57cgUEncoUcW") # ОЧЕНЬ ВАЖНО: Измените это значение в продакшене и используйте переменную окружения! API_SECRET_TOKEN = os.getenv("API_SECRET_TOKEN", "dlaqklqdmorkca") PAYMENT_TIMEOUT = timedelta(hours=6) FINAL_REDIRECT_URL = "https://bill.vortexhost.pro" STATIC_EMAIL = "static@example.com" SITE_TITLE = "VORTEXHOST PAYMENT" MIN_AMOUNTS = { "EU": 100.0, # Minimum 100 RUB for EU "RU": 1.0, # Default minimum for RU "RUCARD": 1.0 # Default minimum for RUCARD } # --- Data --- MESSAGES = [ "Удачи со стримам!", "Отличный стрим!", "Ты просто лучший!", "Я тебя обожаю!", "Крутой контент!", "Продолжай в том же духе!", "Ты огонь!", "Супер стрим!" ] # --- State Management --- payment_sessions = {} websocket_clients = {} used_messages = set() sessions_lock = Lock() websockets_lock = Lock() # --- Logger --- logger = logging.getLogger('PaymentSystem') if not logger.handlers: logger.setLevel(logging.INFO) handler = logging.FileHandler('/var/log/payment_system.log') # Log to file handler.setFormatter(logging.Formatter('%(asctime)s - %(name)s - %(levelname)s - %(message)s')) logger.addHandler(handler) # Also log to console console_handler = logging.StreamHandler() console_handler.setFormatter(logging.Formatter('%(asctime)s - %(name)s - %(levelname)s - %(message)s')) logger.addHandler(console_handler) def __init__(self): self.loop = None self.WIDGET_URL = f"https://www.donationalerts.com/widget/alerts?group_id=1&token={self.WIDGET_TOKEN}" def _get_unique_message(self): available_messages = [msg for msg in self.MESSAGES if msg not in self.used_messages] return random.choice(available_messages) if available_messages else None def generate_payment_link(self, payment_id): params = {"payment_id": payment_id} # Ссылка для клиента всегда должна быть HTTPS, так как Nginx будет обрабатывать SSL return f"https://pay.vortexhost.pro/payment?{urlencode(params)}" def notify_websocket_clients(self, payment_id, message): if self.loop and self.loop.is_running(): asyncio.run_coroutine_threadsafe(self.async_notify_websocket_clients(payment_id, message), self.loop) async def async_notify_websocket_clients(self, payment_id, message): with self.websockets_lock: clients = list(self.websocket_clients.get(payment_id, [])) for ws in clients: try: await ws.send_json(message) self.logger.info(f"Sent WebSocket message for payment {payment_id}: {message}") except Exception as e: self.logger.error(f"Error sending WebSocket message for payment {payment_id}: {e}") async def automate_donation_form(self, amount, message, payment_id, payment_method): for attempt in range(3): async with async_playwright() as p: browser = await p.chromium.launch(headless=True, timeout=120000) context = await browser.new_context() page = await context.new_page() page.on("request", lambda req: self.logger.debug( f"[Playwright Request] Payment {payment_id}: {req.method} {req.url}")) page.on("response", lambda res: asyncio.create_task( self.log_playwright_response(res, payment_id))) try: self.logger.info( f"Attempt {attempt + 1}: Navigating to {self.DONATION_URL} for payment {payment_id}") await page.goto(self.DONATION_URL, wait_until='domcontentloaded', timeout=60000) amount_input = await page.wait_for_selector("input.form-control.base-input", state="visible", timeout=15000) await amount_input.click() for _ in range(3): await page.keyboard.press("Backspace") await asyncio.sleep(random.uniform(0.01, 0.03)) amount_str = str(amount) for char in amount_str: await page.keyboard.type(char) await asyncio.sleep(random.uniform(0.01, 0.04)) if random.random() > 0.5: await page.keyboard.press("Backspace") await asyncio.sleep(0.02) await page.keyboard.type(amount_str[-1]) await asyncio.sleep(0.02) message_input = await page.wait_for_selector("textarea.form-control.base-textarea.editor", state="visible", timeout=15000) await message_input.click() for char in message: await page.keyboard.type(char) await asyncio.sleep(random.uniform(0.01, 0.03)) if random.random() < 0.05: await asyncio.sleep(0.1) await page.click("button.button.button-primary.button-lg", timeout=15000) await asyncio.sleep(0.5) await page.wait_for_selector("div.payment-method", state="visible", timeout=30000) if payment_method == "RU": self.logger.info(f"Selecting RU payment method for {payment_id}") await page.click("div.payment-method:nth-child(1)", timeout=15000) elif payment_method == "EU": self.logger.info(f"Selecting EU payment method for {payment_id}") await page.wait_for_selector(".methods-additional-button", state="visible", timeout=30000) await page.click(".methods-additional-button", timeout=15000) await asyncio.sleep(1) await page.wait_for_selector("div.payment-method:nth-child(4)", state="visible", timeout=30000) await page.click("div.payment-method:nth-child(4)", timeout=15000) elif payment_method == "RUCARD": self.logger.info(f"Selecting RUCARD payment method for {payment_id}") await page.wait_for_selector("div.payment-method:nth-child(2)", state="visible", timeout=30000) await page.click("div.payment-method:nth-child(2)", timeout=15000) else: self.logger.warning( f"Unknown payment method '{payment_method}' for {payment_id}. Defaulting to general card.") await page.click("div.payment-method:nth-child(1)", timeout=15000) await asyncio.sleep(0.5) email_input = await page.wait_for_selector("input.form-control.base-input.padded-left", state="visible", timeout=15000) await email_input.click() for char in self.STATIC_EMAIL: await page.keyboard.type(char) await asyncio.sleep(random.uniform(0.01, 0.03)) submit_button = await page.wait_for_selector("button.button.button-primary.button-lg", state="visible", timeout=15000) await asyncio.sleep(0.5) await submit_button.click() await asyncio.sleep(5) gateway_url = page.url self.logger.info(f"[Playwright] Final URL after clicks: {gateway_url}") if not gateway_url or gateway_url == self.DONATION_URL: raise Exception("Navigation to payment page failed or returned to initial page.") self.logger.info(f"Captured payment gateway URL for proxying: {gateway_url}") with self.sessions_lock: if payment_id in self.payment_sessions: session = self.payment_sessions[payment_id] session['status'] = 'processed' session['cookies'] = await context.cookies() session['gateway_url'] = gateway_url parsed_gateway = urlparse(gateway_url) original_query_params = parse_qs(parsed_gateway.query) # Здесь важно, чтобы клиентская ссылка была HTTPS, так как Nginx будет SSL-терминировать proxied_url_parts = list(urlparse(f"https://pay.vortexhost.pro/pay/{payment_id}")) proxied_url_parts[4] = urlencode(original_query_params, doseq=True) proxied_url_with_params = urlunparse(proxied_url_parts) self.logger.info(f"Sending obfuscated URL to client (with params): {proxied_url_with_params}") self.notify_websocket_clients(payment_id, {"status": "redirect_ready", "redirect_url": proxied_url_with_params}) return except Exception as e: self.logger.error(f"Attempt {attempt + 1} failed for payment {payment_id}: {e}", exc_info=True) if attempt == 2: self.release_payment_resources(payment_id) self.notify_websocket_clients(payment_id, {"status": "error", "message": "Failed to create payment link."}) finally: try: await page.close() await context.close() await browser.close() except Exception as e: self.logger.error(f"Error closing browser: {e}", exc_info=True) async def log_playwright_response(self, response_event, payment_id): if response_event.status >= 300 and response_event.status < 400: self.logger.info( f"--- REDIRECT DETECTED (Playwright) --- Payment {payment_id}: URL: {response_event.url}, Status: {response_event.status}, Location: {response_event.headers.get('Location')}") else: self.logger.debug( f"[Playwright Response] Payment {payment_id}: URL: {response_event.url}, Status: {response_event.status}") async def monitor_donations(self): if not self.WIDGET_TOKEN: self.logger.error("WIDGET_TOKEN is not set. Donation monitor cannot start.") return async with async_playwright() as p: browser = await p.chromium.launch(headless=True, timeout=120000) page = await browser.new_page() try: self.logger.info(f"Navigating to widget URL: {self.WIDGET_URL}") await page.goto(self.WIDGET_URL, wait_until='domcontentloaded', timeout=60000) self.logger.info("Monitoring donations...") while True: text_content = await page.evaluate("() => document.body.innerText || ''") id_matches = re.findall(r'\[ID:([0-9a-f-]{36})\]', text_content) with self.sessions_lock: for payment_id in id_matches: session = self.payment_sessions.get(payment_id) if session and session['status'] != 'completed': self.logger.info(f"Payment {payment_id} detected. Processing completion.") self.process_completed_payment(payment_id) self.cleanup_expired_payments() await asyncio.sleep(2) except Exception as e: self.logger.error(f"Error in monitor_donations: {e}", exc_info=True) finally: try: await page.close() await browser.contexts[0].close() await browser.close() except Exception as e: self.logger.error(f"Error closing browser: {e}", exc_info=True) def release_payment_resources(self, payment_id): with self.sessions_lock: if payment_id in self.payment_sessions: session = self.payment_sessions.pop(payment_id) base_message = session['message'].split(' [ID:')[0] self.used_messages.discard(base_message) self.logger.info(f"Released resources for payment {payment_id}") def process_completed_payment(self, payment_id): self.logger.info(f"Confirmation for payment ID {payment_id} would be sent here.") self.payment_sessions[payment_id]['status'] = 'completed' self.notify_websocket_clients(payment_id, {"status": "completed", "redirect_to": self.FINAL_REDIRECT_URL}) def cleanup_expired_payments(self): now = datetime.now() with self.sessions_lock: expired_pids = [ pid for pid, session in self.payment_sessions.items() if now > session['timestamp'] + self.PAYMENT_TIMEOUT ] for pid in expired_pids: self.logger.info(f"Cleaning up expired payment ID: {pid}") self.release_payment_resources(pid) self.notify_websocket_clients(pid, {"status": "expired"}) async def handle_payment_request(self, request): payment_id = request.query.get('payment_id') with self.sessions_lock: session = self.payment_sessions.get(payment_id) if not session: return web.Response(status=404, text="Payment not found.", content_type="text/plain") payment_amount, message_with_id = session['amount'], session['message'] payment_method = session.get('payment_method', 'RU') session['status'] = 'processing' asyncio.create_task(self.automate_donation_form(payment_amount, message_with_id, payment_id, payment_method)) html_content = f""" Создание платежа

Создаем ссылку для оплаты...

Сумма: {escape(payment_amount)} RUB

Не закрывайте эту страницу.

""" return web.Response(text=html_content, content_type="text/html") async def handle_websocket_proxy(self, request, target_url, cookies): client_ws = web.WebSocketResponse() await client_ws.prepare(request) # Целевой URL для WebSocket теперь будет HTTP, так как Nginx обрабатывает SSL target_ws_url = target_url.replace('https://', 'ws://').replace('wss://', 'ws://') self.logger.info(f"Proxying WebSocket to {target_ws_url}") ws_headers = {k: v for k, v in request.headers.items() if k.lower().startswith('sec-websocket')} try: async with ClientSession(cookies=cookies) as session: async with session.ws_connect(target_ws_url, headers=ws_headers) as target_ws: self.logger.info("WebSocket proxy connection established.") async def forward(source, dest): async for msg in source: if msg.type == WSMsgType.TEXT: await dest.send_str(msg.data) elif msg.type == WSMsgType.BINARY: await dest.send_bytes(msg.data) elif msg.type == WSMsgType.CLOSE: await dest.close() break # Важно выйти из цикла при закрытии elif msg.type == WSMsgType.ERROR: self.logger.error(f"WebSocket message error: {msg.data}") break await asyncio.gather( forward(client_ws, target_ws), forward(target_ws, client_ws) ) except Exception as e: self.logger.error(f"WebSocket proxy error: {e}", exc_info=True) if not client_ws.closed: await client_ws.close() finally: self.logger.info(f"WebSocket proxy session to {target_ws_url} ended.") return client_ws async def proxy_handler(self, request): payment_id = request.match_info.get('payment_id') relative_path = request.match_info.get('path', '') with self.sessions_lock: session = self.payment_sessions.get(payment_id) if not session or not session.get('gateway_url'): return web.Response(status=404, text="Payment session not found or not ready.") gateway_url_from_session = session['gateway_url'] playwright_cookies = session['cookies'] parsed_gateway = urlparse(gateway_url_from_session) # Базовый URL целевого сайта должен быть HTTP, если Nginx терминирует SSL base_target_url = f"{parsed_gateway.scheme}://{parsed_gateway.netloc}" if not request.query_string and parsed_gateway.query: original_query_params = parse_qs(parsed_gateway.query) proxied_url_parts = list(urlparse(f"https://pay.vortexhost.pro/pay/{payment_id}/{relative_path}")) proxied_url_parts[4] = urlencode(original_query_params, doseq=True) full_redirect_url = urlunparse(proxied_url_parts) self.logger.info(f"[Proxy] Redirecting browser to inject params: {full_redirect_url}") return web.HTTPFound(full_redirect_url) simple_cookies = {cookie['name']: cookie['value'] for cookie in playwright_cookies} target_path = parsed_gateway.path if not relative_path else f"/{relative_path.lstrip('/')}" target_url = f"{base_target_url}{target_path}" if request.query_string: target_url += f"?{request.query_string}" headers = request.headers is_websocket = ('upgrade' in headers.get('Connection', '').lower() and headers.get('Upgrade', '').lower() == 'websocket') if is_websocket: # WebSocket URL для проксирования должен быть WS, так как Nginx терминирует SSL ws_target_url = f"{base_target_url.replace('https://', 'ws://').replace('wss://', 'ws://')}{target_path}" if request.query_string: ws_target_url += f"?{request.query_string}" return await self.handle_websocket_proxy(request, ws_target_url, simple_cookies) self.logger.info( f"[Proxy Request] Payment {payment_id}: {request.method} {request.url} -> Target: {target_url}") http_headers = {k: v for k, v in headers.items() if k.lower() not in ('host', 'connection', 'upgrade-insecure-requests', 'referer', 'accept-encoding')} # Referer должен быть исходным URL целевого сайта, а не прокси-URL http_headers['Referer'] = base_target_url + parsed_gateway.path async with ClientSession(cookies=simple_cookies) as http_session: try: async with http_session.request( request.method, target_url, headers=http_headers, data=await request.read(), allow_redirects=False ) as resp: headers_to_remove = ( 'content-encoding', 'content-length', 'transfer-encoding', 'connection', 'content-security-policy', 'content-security-policy-report-only', 'set-cookie', 'content-type' ) response_headers = {k: v for k, v in resp.headers.items() if k.lower() not in headers_to_remove} self.logger.info(f"[Proxy Response] Payment {payment_id}: {resp.status} {resp.url}") if 'Location' in resp.headers: self.logger.info(f" Location Header: {resp.headers['Location']}") if resp.status in (301, 302, 303, 307, 308) and 'Location' in resp.headers: location = resp.headers['Location'] parsed_loc = urlparse(location) if parsed_loc.netloc == urlparse(self.DONATION_URL).netloc and \ parsed_loc.path.endswith('/success'): self.logger.info( f"Detected success redirect for {payment_id}. Redirecting to final URL: {self.FINAL_REDIRECT_URL}") return web.HTTPFound(self.FINAL_REDIRECT_URL) # Переписываем Location-заголовок, чтобы он указывал на наш прокси # Важно, чтобы схема была HTTPS для клиента if parsed_loc.netloc and parsed_loc.netloc != parsed_gateway.netloc: new_loc_path = f"/pay/{payment_id}/{parsed_loc.netloc}{parsed_loc.path}" new_loc = urlunparse(( request.url.scheme, # Используем схему запроса от клиента (HTTPS) request.url.host, # Используем хост запроса от клиента (pay.vortexhost.pro) new_loc_path, '', parsed_loc.query, '' )) response_headers['Location'] = new_loc self.logger.info( f"[Proxy Redirect Handled] Rewriting external redirect to proxy: {location} -> {new_loc}") else: new_loc_path = parsed_loc.path if not new_loc_path.startswith(f'/pay/{payment_id}'): new_loc_path = f"/pay/{payment_id}{new_loc_path}" new_loc = urlunparse(( request.url.scheme, # Используем схему запроса от клиента (HTTPS) request.url.host, # Используем хост запроса от клиента (pay.vortexhost.pro) new_loc_path, '', parsed_loc.query, '' )) response_headers['Location'] = new_loc self.logger.info( f"[Proxy Redirect Handled] Rewriting internal redirect to proxy: {location} -> {new_loc}") body = await resp.read() if 'text/html' in resp.headers.get('Content-Type', '').lower(): # Здесь можно добавить логику для перезаписи URL в HTML-содержимом, # чтобы все ссылки и ресурсы внутри страницы также указывали на ваш прокси. # Это сложная задача, требующая парсинга HTML. # Например: body = self.rewrite_html_urls(body, payment_id, base_target_url, request.url.host) pass return web.Response( body=body, status=resp.status, headers=response_headers, content_type=resp.content_type ) except Exception as e: self.logger.error(f"Error during proxy request to {target_url}: {e}", exc_info=True) return web.Response(status=500, text=f"Proxy error: {e}") async def handle_websocket_connections(self, request): payment_id = request.query.get('payment_id') if not payment_id: return web.Response(status=400, text="Payment ID is required for WebSocket connection.") ws = web.WebSocketResponse() await ws.prepare(request) self.logger.info(f"WebSocket client connected for payment_id: {payment_id}") with self.websockets_lock: if payment_id not in self.websocket_clients: self.websocket_clients[payment_id] = [] self.websocket_clients[payment_id].append(ws) try: async for msg in ws: if msg.type == WSMsgType.TEXT: self.logger.debug(f"Received WS message from client for {payment_id}: {msg.data}") elif msg.type == WSMsgType.ERROR: self.logger.error(f"WebSocket connection error for {payment_id}: {ws.exception()}") elif msg.type == WSMsgType.CLOSE: self.logger.info(f"WebSocket client disconnected for {payment_id}.") finally: with self.websockets_lock: if payment_id in self.websocket_clients: self.websocket_clients[payment_id].remove(ws) if not self.websocket_clients[payment_id]: del self.websocket_clients[payment_id] self.logger.info(f"WebSocket for {payment_id} closed and client removed.") return ws async def create_payment_handler(self, request: web.Request): self.logger.info("Received request to /create_payment") try: data = await request.json() except json.JSONDecodeError: self.logger.warning("Invalid JSON data provided in /create_payment request") return web.json_response({"error": "Invalid JSON data provided"}, status=400) if not data: self.logger.warning("No JSON data provided in /create_payment request") return web.json_response({"error": "No JSON data provided"}, status=400) if data.get('token') != self.API_SECRET_TOKEN: self.logger.warning(f"Unauthorized payment creation attempt with token: {data.get('token', '')}") return web.json_response({"error": "Unauthorized"}, status=401) try: amount = float(data.get('amount')) payment_method = data.get('payment_method', 'RU') except (ValueError, TypeError): self.logger.warning(f"Invalid amount provided in request: {data.get('amount')}") return web.json_response({"error": "Invalid or missing 'amount'"}, status=400) min_amount = self.MIN_AMOUNTS.get(payment_method, self.MIN_AMOUNTS["RU"]) if amount < min_amount: self.logger.warning(f"Payment amount {amount} for method {payment_method} is below minimum {min_amount}.") return web.json_response({"error": f"Amount for {payment_method} must be at least {min_amount} RUB"}, status=400) payment_id = str(uuid.uuid4()) message = self._get_unique_message() if not message: self.logger.error("No unique messages available for payment creation") return web.json_response({"error": "No unique messages available"}, status=500) full_message = f"{message} [ID:{payment_id}]" with self.sessions_lock: self.payment_sessions[payment_id] = { 'amount': amount, 'message': full_message, 'timestamp': datetime.now(), 'status': 'pending', 'payment_method': payment_method, 'gateway_url': None, 'cookies': None } self.used_messages.add(message) payment_link = self.generate_payment_link(payment_id) self.logger.info(f"Payment link generated: {payment_link}") return web.json_response({ "status": "success", "payment_id": payment_id, "payment_link": payment_link, "message": "Payment session created. Redirect the user to the payment_link." }, status=200) async def run(self): self.loop = asyncio.get_event_loop() aio_app = web.Application() # Удаляем SSL-контекст, так как Nginx будет обрабатывать SSL # ssl_context = ssl.create_default_context(ssl.Purpose.SERVER_AUTH) # ssl_context.load_cert_chain( # certfile='/etc/letsencrypt/live/pay.vortexhost.pro/fullchain.pem', # keyfile='/etc/letsencrypt/live/pay.vortexhost.pro/privkey.pem' # ) aio_app.router.add_post('/create_payment', self.create_payment_handler) aio_app.router.add_get('/payment', self.handle_payment_request) aio_app.router.add_get('/pay/{payment_id}/{path:.*}', self.proxy_handler) aio_app.router.add_post('/pay/{payment_id}/{path:.*}', self.proxy_handler) aio_app.router.add_get('/pay/{payment_id}', self.proxy_handler) aio_app.router.add_get('/ws', self.handle_websocket_connections) runner = web.AppRunner(aio_app) await runne