#!/usr/bin/env python3 """ frp-manager API 提供端點讓 VM 可以動態新增子域名到 Caddy """ from flask import Flask, request, jsonify import requests import os import json app = Flask(__name__) CADDY_ADMIN = os.getenv('CADDY_ADMIN', 'http://localhost:2019') FRP_TOKEN = os.getenv('FRP_TOKEN', 'your_secure_token') DOMAIN = os.getenv('DOMAIN', 'test.yourdomain.com') def verify_token(): """驗證請求的 token""" token = request.headers.get('Authorization', '').replace('Bearer ', '') return token == FRP_TOKEN @app.route('/health', methods=['GET']) def health(): return jsonify({'status': 'ok'}) @app.route('/domains', methods=['POST']) def add_domain(): """新增一個子域名到 Caddy""" if not verify_token(): return jsonify({'error': 'Unauthorized'}), 401 data = request.json subdomain = data.get('subdomain') local_port = data.get('local_port', 8080) if not subdomain: return jsonify({'error': 'subdomain is required'}), 400 full_domain = f"{subdomain}.{DOMAIN}" caddy_config = { "@id": full_domain, "match": [{"host": [full_domain]}], "handle": [{ "handler": "reverse_proxy", "upstreams": [{"dial": f"localhost:{local_port}"}], "headers": { "request": { "set": { "Host": ["{http.request.host}"], "X-Real-IP": ["{http.request.remote.host}"], "X-Forwarded-For": ["{http.request.remote.host}"], "X-Forwarded-Proto": ["{http.request.scheme}"] } } } }] } try: url = f"{CADDY_ADMIN}/config/apps/http/servers/srv0/routes/0" response = requests.post(url, json=caddy_config) if response.status_code in [200, 201]: return jsonify({ 'success': True, 'domain': full_domain, 'url': f"https://{full_domain}", 'message': 'Domain added successfully. HTTPS will be ready in a few moments.' }) else: return jsonify({ 'error': 'Failed to add domain to Caddy', 'details': response.text }), 500 except Exception as e: return jsonify({'error': str(e)}), 500 @app.route('/domains/', methods=['DELETE']) def remove_domain(subdomain): """移除一個子域名""" if not verify_token(): return jsonify({'error': 'Unauthorized'}), 401 full_domain = f"{subdomain}.{DOMAIN}" try: url = f"{CADDY_ADMIN}/id/{full_domain}" response = requests.delete(url) if response.status_code == 200: return jsonify({ 'success': True, 'message': f'Domain {full_domain} removed' }) else: return jsonify({ 'error': 'Failed to remove domain', 'details': response.text }), 500 except Exception as e: return jsonify({'error': str(e)}), 500 if __name__ == '__main__': # 使用 0.0.0.0 讓容器外部可以訪問 app.run(host='0.0.0.0', port=5000, debug=False)