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

160 lines
5.4 KiB
Python
Raw Normal View History

2024-12-11 09:05:26 +00:00
import tempfile
from flask import jsonify
import subprocess
import os
import logging
import hmac
import hashlib
import yaml
import traceback
2024-12-11 09:05:26 +00:00
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:
2024-12-11 09:05:26 +00:00
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}
)
2024-12-11 09:05:26 +00:00
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']}"
)
2024-12-11 09:05:26 +00:00
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
2024-12-11 09:05:26 +00:00
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__'):
2024-12-11 09:05:26 +00:00
invoice_id = invoice_id[8:-8]
logger.info(f"Stripped test markers from invoice ID: {invoice_id}")
2024-12-11 09:05:26 +00:00
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),
2024-12-11 09:05:26 +00:00
'-i', str(BASE_DIR / 'inventory.ini'),
'-e', f'invoice_id={invoice_id}',
'--vault-password-file', vault_pass_file.name,
2024-12-11 09:05:26 +00:00
'-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()
2024-12-11 09:05:26 +00:00
}), 500