diff --git a/.flaskenv b/.flaskenv new file mode 100644 index 0000000..7547390 --- /dev/null +++ b/.flaskenv @@ -0,0 +1,2 @@ +FLASK_APP=app +FLASK_ENV=development \ No newline at end of file diff --git a/app/__init__.py b/app/__init__.py index e69de29..c7a01d2 100644 --- a/app/__init__.py +++ b/app/__init__.py @@ -0,0 +1,19 @@ +from flask import Flask, request, jsonify +import logging +from .handlers.webhook_handler import handle_payment_webhook + +# Set up logging +logging.basicConfig( + level=logging.INFO, + format='%(asctime)s - %(name)s - %(levelname)s - %(message)s' +) +logger = logging.getLogger(__name__) + +app = Flask(__name__) + +@app.route('/webhook/vpn', methods=['POST']) +def handle_payment(): + return handle_payment_webhook(request) + +if __name__ == '__main__': + app.run(host='0.0.0.0', port=5000) \ No newline at end of file diff --git a/app/handlers/webhook_handler.py b/app/handlers/webhook_handler.py index 2a684aa..19d7a71 100644 --- a/app/handlers/webhook_handler.py +++ b/app/handlers/webhook_handler.py @@ -1,55 +1,56 @@ -# 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 tempfile +from flask import jsonify import subprocess import os import logging -import json -from pathlib import Path import hmac import hashlib import yaml import traceback -import tempfile +from pathlib import Path 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) + vault_pass = os.getenv('ANSIBLE_VAULT_PASSWORD', '') + if not vault_pass: + raise Exception("Vault password not found in environment variables") - # Construct full webhook URL - vault_data['webhook_full_url'] = ( - f"{vault_data['btcpay_base_url']}" - f"{vault_data['btcpay_webhook_path']}" + 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_data + return vault_contents except Exception as e: logger.error(f"Error reading vault: {str(e)}") @@ -61,8 +62,6 @@ def verify_signature(payload_body, signature_header): 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, @@ -77,20 +76,17 @@ def verify_signature(payload_body, signature_header): logger.error(f"Signature verification failed: {str(e)}") return False -@app.route('/webhook/vpn', methods=['POST']) -def handle_payment(): +def handle_payment_webhook(request): + """Handle BTCPay Server webhook for VPN provisioning""" 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") @@ -104,17 +100,14 @@ def handle_payment(): 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 + invoice_id = invoice_id[8:-8] logger.info(f"Stripped test markers from invoice ID: {invoice_id}") - # Get vault password from environment - vault_pass = os.getenv('ANSIBLE_VAULT_PASSWORD') + 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() @@ -122,10 +115,10 @@ def handle_payment(): cmd = [ 'ansible-playbook', str(PLAYBOOK_PATH), - '-i', str(INVENTORY_PATH), + '-i', str(BASE_DIR / 'inventory.ini'), '-e', f'invoice_id={invoice_id}', '--vault-password-file', vault_pass_file.name, - '-vvv' # Add verbose output + '-vvv' ] logger.info(f"Running ansible-playbook command: {' '.join(cmd)}") @@ -136,10 +129,8 @@ def handle_payment(): 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}") @@ -166,17 +157,4 @@ def handle_payment(): 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) \ No newline at end of file + }), 500 \ No newline at end of file diff --git a/run.py b/run.py new file mode 100644 index 0000000..7bcfcd9 --- /dev/null +++ b/run.py @@ -0,0 +1,4 @@ +from app import app + +if __name__ == '__main__': + app.run(host='0.0.0.0', port=5000) \ No newline at end of file