import os import re import subprocess from typing import List from fastapi import FastAPI, Form, HTTPException, Header, Request, Depends from fastapi.responses import JSONResponse from dotenv import load_dotenv from slowapi import Limiter, _rate_limit_exceeded_handler from slowapi.util import get_remote_address from slowapi.errors import RateLimitExceeded # Load .env variables load_dotenv() API_KEY = os.getenv("API_KEY") WHITELISTED_IPS = os.getenv("WHITELISTED_IPS", "").split(",") # Constants VESTA_BIN = "/usr/local/hestia/bin" DEFAULT_RATE_LIMIT = "10/minute" # FastAPI setup app = FastAPI() limiter = Limiter(key_func=get_remote_address) app.state.limiter = limiter app.add_exception_handler(RateLimitExceeded, _rate_limit_exceeded_handler) # ---------------------- Helpers ---------------------- def check_api_key(x_api_key: str = Header(...)): if x_api_key != API_KEY: raise HTTPException(status_code=403, detail="Invalid API key") def check_whitelisted_ip(request: Request): ip = request.client.host if WHITELISTED_IPS and ip not in WHITELISTED_IPS: raise HTTPException(status_code=403, detail="IP not allowed") def valid_username(name: str): return re.match(r"^[a-zA-Z0-9_]+$", name) def run_cmd(cmd: List[str]): try: result = subprocess.run(cmd, capture_output=True, text=True, check=True) return result.stdout.strip() except subprocess.CalledProcessError as e: raise HTTPException(status_code=500, detail=f"Command error: {e.stderr.strip()}") # ---------------------- Endpoints ---------------------- @app.post("/vesta/add-user") @limiter.limit(DEFAULT_RATE_LIMIT) async def add_user( request: Request, user: str = Form(...), password: str = Form(...), email: str = Form(...), package: str = Form("default"), auth: None = Depends(check_api_key), _: None = Depends(check_whitelisted_ip) ): if not valid_username(user): raise HTTPException(status_code=400, detail="Invalid username") cmd = [f"{VESTA_BIN}/v-add-user", user, password, email, package] output = run_cmd(cmd) return {"message": "User created", "output": output} @app.post("/vesta/add-domain") @limiter.limit(DEFAULT_RATE_LIMIT) async def add_domain( request: Request, user: str = Form(...), domain: str = Form(...), ssl: bool = Form(False), php: bool = Form(False), auth: None = Depends(check_api_key), _: None = Depends(check_whitelisted_ip) ): if not valid_username(user): raise HTTPException(status_code=400, detail="Invalid username") cmd = [f"{VESTA_BIN}/v-add-web-domain", user, domain] output = run_cmd(cmd) ssl_out, php_out = None, None if ssl: ssl_cmd = [f"{VESTA_BIN}/v-add-web-domain-ssl", user, domain] ssl_out = run_cmd(ssl_cmd) if php: php_cmd = [f"{VESTA_BIN}/v-add-web-php", user, domain] php_out = run_cmd(php_cmd) return { "message": "Domain added", "domain_output": output, "ssl_output": ssl_out, "php_output": php_out } # ---------------------- Launch ---------------------- if __name__ == "__main__": import uvicorn uvicorn.run("hestia_runner:app", host="0.0.0.0", port=3001) # .env #API_KEY=your-secret-key #WHITELISTED_IPS=127.0.0.1,192.168.1.10 # cd /root/hestia_runner/ && source .venv/bin/activate.fish && uvicorn hestia_runner:app --host 0.0.0.0 --port 1002 #curl -X POST http://your-server-ip:3001/vesta/add-user \ # -H "x-api-key: your_api_key_here" \ # -F "user=testuser" \ # -F "password=securepass123" \ # -F "email=test@example.com" \ # -F "package=default" #curl -X POST http://your-server-ip:3001/vesta/add-domain \ # -H "x-api-key: your_api_key_here" \ # -F "user=testuser" \ # -F "domain=example.com" \ # -F "ssl=true" \ # -F "php=true"