scripts/hestia_runner.py

130 lines
3.8 KiB
Python

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"