import tempfile from flask import jsonify import subprocess import os import logging import hmac import hashlib import yaml import traceback from pathlib import Path from dotenv import load_dotenv 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' 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 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}") invoice_id = data.get('invoiceId') if not invoice_id: logger.error("Missing invoiceId in webhook data") return jsonify({"error": "Missing invoiceId"}), 400 if invoice_id.startswith('__test__') and invoice_id.endswith('__test__'): invoice_id = invoice_id[8:-8] logger.info(f"Stripped test markers from invoice ID: {invoice_id}") 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) logger.info(f"Ansible playbook stdout: {result.stdout}") if result.stderr: logger.error(f"Ansible playbook stderr: {result.stderr}") if result.returncode != 0: error_msg = f"Ansible playbook failed with return code {result.returncode}" logger.error(error_msg) return jsonify({ "error": "Provisioning failed", "details": error_msg, "stdout": result.stdout, "stderr": result.stderr }), 500 logger.info(f"Successfully processed invoice {invoice_id}") return jsonify({ "status": "success", "invoice_id": invoice_id, "message": "VPN provisioning initiated" }) except Exception as e: logger.error(f"Error processing webhook: {str(e)}") return jsonify({ "error": str(e), "traceback": traceback.format_exc() }), 500