# app/handlers/webhook_handler.py import sys from pathlib import Path sys.path.append(str(Path(__file__).resolve().parent.parent.parent)) from flask import Flask, request, jsonify import subprocess import os import logging import json from pathlib import Path import hmac import hashlib import yaml import traceback import tempfile from dotenv import load_dotenv from app.utils.vault_helper import decrypt_vault_file # Load environment variables from .env file load_dotenv() # Configure logging logging.basicConfig( level=logging.INFO, format='%(asctime)s - %(name)s - %(levelname)s - %(message)s' ) logger = logging.getLogger(__name__) app = Flask(__name__) # Update paths for new structure BASE_DIR = Path(__file__).resolve().parent.parent.parent PLAYBOOK_PATH = BASE_DIR / 'ansible' / 'playbooks' / 'vpn_provision.yml' VAULT_PATH = BASE_DIR / 'ansible' / 'group_vars' / 'vpn_servers' / 'vault.yml' INVENTORY_PATH = BASE_DIR / 'inventory.ini' def get_vault_values(): """Get decrypted values from Ansible vault""" try: # Use vault helper to decrypt contents vault_contents = decrypt_vault_file(VAULT_PATH) vault_data = yaml.safe_load(vault_contents) # Construct full webhook URL vault_data['webhook_full_url'] = ( f"{vault_data['btcpay_base_url']}" f"{vault_data['btcpay_webhook_path']}" ) return vault_data 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'] logger.debug(f"Secret type in verify_signature: {type(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 @app.route('/webhook/vpn', methods=['POST']) def handle_payment(): try: # Get vault values first to ensure we can access them vault_values = get_vault_values() logger.info(f"Processing webhook on endpoint: {vault_values['webhook_full_url']}") # Get the signature from headers signature = request.headers.get('BTCPay-Sig') if not signature: logger.error("Missing BTCPay-Sig header") return jsonify({"error": "Missing signature"}), 401 # Verify signature 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 # Remove __test__ prefix/suffix for test webhooks if invoice_id.startswith('__test__') and invoice_id.endswith('__test__'): invoice_id = invoice_id[8:-8] # Strip the test markers logger.info(f"Stripped test markers from invoice ID: {invoice_id}") # Get vault password from environment vault_pass = os.getenv('ANSIBLE_VAULT_PASSWORD') if not vault_pass: raise Exception("Vault password not found in environment variables") # Create temporary vault password file for ansible-playbook 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(INVENTORY_PATH), '-e', f'invoice_id={invoice_id}', '--vault-password-file', vault_pass_file.name, '-vvv' # Add verbose output ] logger.info(f"Running ansible-playbook command: {' '.join(cmd)}") result = subprocess.run( cmd, capture_output=True, text=True ) # Clean up the temporary file os.unlink(vault_pass_file.name) # Log the complete output regardless of success/failure 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 if __name__ == '__main__': # Verify we can read the vault and construct URL before starting try: vault_values = get_vault_values() logger.info(f"Successfully loaded vault values") logger.info(f"Configured webhook URL: {vault_values['webhook_full_url']}") except Exception as e: logger.error(f"Failed to load vault values: {str(e)}") exit(1) logger.info(f"Starting webhook handler, watching for playbook at {PLAYBOOK_PATH}") app.run(host='0.0.0.0', port=5000)