import tempfile from flask import jsonify import subprocess import os import logging import hmac import hashlib import yaml import datetime import traceback from pathlib import Path from dotenv import load_dotenv from ..utils.db.operations import DatabaseManager from ..utils.db.models import SubscriptionStatus load_dotenv() logging.basicConfig( level=logging.INFO, format='%(asctime)s - %(name)s - %(levelname)s - %(message)s' ) logger = logging.getLogger(__name__) BASE_DIR = Path(__file__).resolve().parent.parent.parent PLAYBOOK_PATH = BASE_DIR / 'ansible' / 'playbooks' / 'vpn_provision.yml' CLEANUP_PLAYBOOK = BASE_DIR / 'ansible' / 'playbooks' / 'vpn_cleanup.yml' def get_vault_values(): """Get decrypted values from Ansible vault""" try: vault_pass = os.getenv('ANSIBLE_VAULT_PASSWORD', '') if not vault_pass: raise Exception("Vault password not found in environment variables") with tempfile.NamedTemporaryFile(mode='w', delete=False) as vault_pass_file: vault_pass_file.write(vault_pass) vault_pass_file.flush() result = subprocess.run( ['ansible-vault', 'view', str(BASE_DIR / 'ansible/group_vars/vpn_servers/vault.yml')], capture_output=True, text=True, env={**os.environ, 'ANSIBLE_VAULT_PASSWORD_FILE': vault_pass_file.name} ) os.unlink(vault_pass_file.name) if result.returncode != 0: raise Exception(f"Failed to decrypt vault: {result.stderr}") vault_contents = yaml.safe_load(result.stdout) vault_contents['webhook_full_url'] = ( f"{vault_contents['btcpay_base_url']}" f"{vault_contents['btcpay_webhook_path']}" ) return vault_contents except Exception as e: logger.error(f"Error reading vault: {str(e)}") raise def verify_signature(payload_body, signature_header): """Verify BTCPay webhook signature""" try: vault_values = get_vault_values() secret = vault_values['webhook_secret'] expected_signature = hmac.new( secret.encode('utf-8'), payload_body, hashlib.sha256 ).hexdigest() return hmac.compare_digest( signature_header.lower(), f"sha256={expected_signature}".lower() ) except Exception as e: logger.error(f"Signature verification failed: {str(e)}") return False def run_ansible_playbook(invoice_id): """Run the VPN provisioning playbook""" vault_pass = os.getenv('ANSIBLE_VAULT_PASSWORD', '') if not vault_pass: raise Exception("Vault password not found in environment variables") with tempfile.NamedTemporaryFile(mode='w', delete=False) as vault_pass_file: vault_pass_file.write(vault_pass) vault_pass_file.flush() cmd = [ 'ansible-playbook', str(PLAYBOOK_PATH), '-i', str(BASE_DIR / 'inventory.ini'), '-e', f'invoice_id={invoice_id}', '--vault-password-file', vault_pass_file.name, '-vvv' ] logger.info(f"Running ansible-playbook command: {' '.join(cmd)}") result = subprocess.run( cmd, capture_output=True, text=True ) os.unlink(vault_pass_file.name) return result def handle_subscription_status(data): """Handle SubscriptionStatusUpdated webhook""" sub_id = data['subscriptionId'] status = data['status'] logger.info(f"Processing subscription status update: {sub_id} -> {status}") subscription = DatabaseManager.get_subscription_by_invoice(sub_id) if not subscription: logger.error(f"Subscription {sub_id} not found") return jsonify({"error": "Subscription not found"}), 404 if status == 'Active': DatabaseManager.activate_subscription(sub_id) else: # Run cleanup for inactive subscriptions result = subprocess.run([ 'ansible-playbook', str(CLEANUP_PLAYBOOK), '-i', str(BASE_DIR / 'inventory.ini'), '-e', f'subscription_id={sub_id}', '-vvv' ], capture_output=True, text=True) if result.returncode != 0: logger.error(f"Failed to clean up subscription {sub_id}: {result.stderr}") DatabaseManager.expire_subscription(subscription.id) logger.info(f"Subscription {sub_id} is no longer active") return jsonify({ "status": "success", "message": f"Subscription {sub_id} status updated to {status}" }) def handle_subscription_renewal(data): """Handle SubscriptionRenewalRequested webhook""" sub_id = data['subscriptionId'] logger.info(f"Processing subscription renewal request: {sub_id}") subscription = DatabaseManager.get_subscription_by_invoice(sub_id) if not subscription: logger.error(f"Subscription {sub_id} not found") return jsonify({"error": "Subscription not found"}), 404 # TODO: Send renewal notification to user return jsonify({ "status": "success", "message": f"Subscription {sub_id} renewal requested" }) def handle_payment_webhook(request): """Handle BTCPay Server webhook for VPN provisioning""" try: vault_values = get_vault_values() logger.info(f"Processing webhook on endpoint: {vault_values['webhook_full_url']}") signature = request.headers.get('BTCPay-Sig') if not signature: logger.error("Missing BTCPay-Sig header") return jsonify({"error": "Missing signature"}), 401 is_valid = verify_signature(request.get_data(), signature) if not is_valid: logger.error("Invalid signature") return jsonify({"error": "Invalid signature"}), 401 data = request.json logger.info(f"Received webhook data: {data}") # Handle test webhooks invoice_id = data.get('invoiceId', '') if invoice_id.startswith('__test__'): logger.info(f"Received test webhook, acknowledging: {data.get('type')}") return jsonify({ "status": "success", "message": "Test webhook acknowledged" }) webhook_type = data.get('type') if webhook_type == 'SubscriptionStatusUpdated': return handle_subscription_status(data) elif webhook_type == 'SubscriptionRenewalRequested': return handle_subscription_renewal(data) elif webhook_type in ['InvoiceSettled', 'InvoicePaymentSettled']: invoice_id = data.get('invoiceId') if not invoice_id: logger.error("Missing invoiceId in webhook data") return jsonify({"error": "Missing invoiceId"}), 400 # Get subscription and run Ansible playbook logger.info(f"Starting VPN provisioning for invoice {invoice_id}") result = run_ansible_playbook(invoice_id) if result.returncode != 0: error_msg = f"Ansible playbook failed with return code {result.returncode}" logger.error(error_msg) logger.error(f"Ansible stdout: {result.stdout}") logger.error(f"Ansible stderr: {result.stderr}") return jsonify({ "error": "Provisioning failed", "details": error_msg, "stdout": result.stdout, "stderr": result.stderr }), 500 # Get subscription and activate it subscription = DatabaseManager.get_subscription_by_invoice(invoice_id) if subscription: subscription = DatabaseManager.activate_subscription(invoice_id) DatabaseManager.record_payment( subscription.user_id, subscription.id, invoice_id, data.get('amount', 0) ) logger.info(f"VPN provisioning completed for invoice {invoice_id}") return jsonify({ "status": "success", "invoice_id": invoice_id, "message": "VPN provisioning completed" }) else: logger.info(f"Received {webhook_type} webhook - no action required") return jsonify({ "status": "success", "message": f"Webhook {webhook_type} acknowledged" }) except Exception as e: logger.error(f"Error processing webhook: {str(e)}") logger.error(traceback.format_exc()) return jsonify({ "error": str(e), "traceback": traceback.format_exc() }), 500