vpn-btcpay-provisioner/app/handlers/webhook_handler.py

182 lines
6.2 KiB
Python
Raw Normal View History

# 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)