mirror of
https://github.com//cppla/ServerStatus
synced 2026-08-06 09:13:57 +08:00
version 1.1.8
This commit is contained in:
+7
-3
@@ -1,4 +1,5 @@
|
||||
OUT = sergate
|
||||
.DEFAULT_GOAL := $(OUT)
|
||||
|
||||
#CC = clang
|
||||
CC = gcc
|
||||
@@ -19,10 +20,13 @@ C_OBJS := $(patsubst $(SDIR)/%.c,$(ODIR)/%.o,$(C_SRCS))
|
||||
CXX_OBJS := $(patsubst $(SDIR)/%.cpp,$(ODIR)/%.o,$(CXX_SRCS))
|
||||
OBJS := $(C_OBJS) $(CXX_OBJS)
|
||||
|
||||
$(ODIR)/%.o: $(SDIR)/%.c
|
||||
$(ODIR):
|
||||
mkdir -p $(ODIR)
|
||||
|
||||
$(ODIR)/%.o: $(SDIR)/%.c | $(ODIR)
|
||||
$(CC) -c $(INC) $(CFLAGS) $< -o $@
|
||||
|
||||
$(ODIR)/%.o: $(SDIR)/%.cpp
|
||||
$(ODIR)/%.o: $(SDIR)/%.cpp | $(ODIR)
|
||||
$(CXX) -c $(INC) $(CXXFLAGS) $< -o $@
|
||||
|
||||
$(OUT): $(OBJS)
|
||||
@@ -31,4 +35,4 @@ $(OUT): $(OBJS)
|
||||
.PHONY: clean
|
||||
|
||||
clean:
|
||||
rm -f $(ODIR)/*.o $(OUT)
|
||||
rm -f $(ODIR)/*.o $(OUT)
|
||||
|
||||
+4
-4
@@ -19,14 +19,14 @@
|
||||
"monthstart": 1
|
||||
},
|
||||
{
|
||||
"disabled": true,
|
||||
"username": "s03",
|
||||
"name": "node3",
|
||||
"type": "hyper",
|
||||
"host": "host3",
|
||||
"location": "🇫🇷",
|
||||
"password": "USER_DEFAULT_PASSWORD",
|
||||
"monthstart": 1
|
||||
"monthstart": 1,
|
||||
"disabled": false
|
||||
},
|
||||
{
|
||||
"username": "s04",
|
||||
@@ -58,14 +58,14 @@
|
||||
"domain": "https://my.cloudcpp.com",
|
||||
"port": 443,
|
||||
"interval": 7200,
|
||||
"callback": "https://yourSMSurl"
|
||||
"callback": "https://yourSMSurl"
|
||||
},
|
||||
{
|
||||
"name": "tz.cloudcpp.com",
|
||||
"domain": "https://tz.cloudcpp.com",
|
||||
"port": 443,
|
||||
"interval": 7200,
|
||||
"callback": "https://yourSMSurl"
|
||||
"callback": "https://yourSMSurl"
|
||||
},
|
||||
{
|
||||
"name": "3.0.2.1",
|
||||
|
||||
@@ -0,0 +1,58 @@
|
||||
#!/bin/sh
|
||||
set -eu
|
||||
|
||||
: "${CONFIG_PATH:=/ServerStatus/server/config.json}"
|
||||
: "${WEB_DIR:=/usr/share/nginx/html}"
|
||||
: "${SERGATE_PID_FILE:=/tmp/serverstatus-sergate.pid}"
|
||||
: "${ADMIN_API_BIND:=127.0.0.1}"
|
||||
: "${ADMIN_API_PORT:=35602}"
|
||||
|
||||
STOPPING=0
|
||||
SERGATE_PID=""
|
||||
API_PID=""
|
||||
|
||||
stop_all() {
|
||||
STOPPING=1
|
||||
if [ -n "$SERGATE_PID" ] && kill -0 "$SERGATE_PID" 2>/dev/null; then
|
||||
kill -TERM "$SERGATE_PID" 2>/dev/null || true
|
||||
fi
|
||||
if [ -n "$API_PID" ] && kill -0 "$API_PID" 2>/dev/null; then
|
||||
kill -TERM "$API_PID" 2>/dev/null || true
|
||||
fi
|
||||
nginx -s quit 2>/dev/null || true
|
||||
}
|
||||
|
||||
trap 'stop_all; exit 0' INT TERM QUIT
|
||||
|
||||
mkdir -p "$WEB_DIR/json"
|
||||
nginx
|
||||
|
||||
CONFIG_PATH="$CONFIG_PATH" \
|
||||
SERGATE_PID_FILE="$SERGATE_PID_FILE" \
|
||||
ADMIN_API_BIND="$ADMIN_API_BIND" \
|
||||
ADMIN_API_PORT="$ADMIN_API_PORT" \
|
||||
ADMIN_TOKEN="${ADMIN_TOKEN:-}" \
|
||||
ADMIN_CORS_ORIGIN="${ADMIN_CORS_ORIGIN:-}" \
|
||||
python3 /ServerStatus/server/manage_api.py &
|
||||
API_PID="$!"
|
||||
if [ -n "${ADMIN_TOKEN:-}" ]; then
|
||||
echo "management API enabled on ${ADMIN_API_BIND}:${ADMIN_API_PORT}"
|
||||
else
|
||||
echo "management API running in read-only discovery mode; set ADMIN_TOKEN to enable writes"
|
||||
fi
|
||||
|
||||
while [ "$STOPPING" -eq 0 ]; do
|
||||
/ServerStatus/server/sergate --config="$CONFIG_PATH" --web-dir="$WEB_DIR" &
|
||||
SERGATE_PID="$!"
|
||||
echo "$SERGATE_PID" > "$SERGATE_PID_FILE"
|
||||
set +e
|
||||
wait "$SERGATE_PID"
|
||||
STATUS="$?"
|
||||
set -e
|
||||
rm -f "$SERGATE_PID_FILE"
|
||||
if [ "$STOPPING" -eq 1 ]; then
|
||||
exit "$STATUS"
|
||||
fi
|
||||
echo "sergate exited with status ${STATUS}; restarting in 1s"
|
||||
sleep 1
|
||||
done
|
||||
@@ -0,0 +1,485 @@
|
||||
#!/usr/bin/env python3
|
||||
import json
|
||||
import os
|
||||
import shutil
|
||||
import signal
|
||||
import tempfile
|
||||
import time
|
||||
import errno
|
||||
from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer
|
||||
from urllib.parse import unquote, urlparse
|
||||
|
||||
|
||||
CONFIG_PATH = os.environ.get("CONFIG_PATH", "/ServerStatus/server/config.json")
|
||||
SERGATE_PID_FILE = os.environ.get("SERGATE_PID_FILE", "/tmp/serverstatus-sergate.pid")
|
||||
ADMIN_TOKEN = os.environ.get("ADMIN_TOKEN", "")
|
||||
API_BIND = os.environ.get("ADMIN_API_BIND", "127.0.0.1")
|
||||
API_PORT = int(os.environ.get("ADMIN_API_PORT", "35602"))
|
||||
CORS_ORIGIN = os.environ.get("ADMIN_CORS_ORIGIN", "")
|
||||
|
||||
|
||||
class ApiError(Exception):
|
||||
def __init__(self, status, message, details=None):
|
||||
super().__init__(message)
|
||||
self.status = status
|
||||
self.message = message
|
||||
self.details = details
|
||||
|
||||
|
||||
def load_config():
|
||||
with open(CONFIG_PATH, "r", encoding="utf-8") as f:
|
||||
return json.load(f)
|
||||
|
||||
|
||||
def normalize_required_strings(item, required, kind, index=None):
|
||||
missing = [field for field in required if item.get(field) is None or not str(item.get(field, "")).strip()]
|
||||
if missing:
|
||||
raise ApiError(400, f"{kind} has missing required fields", {"missing": missing, "index": index})
|
||||
normalized = dict(item)
|
||||
for field in required:
|
||||
normalized[field] = str(normalized[field]).strip()
|
||||
return normalized
|
||||
|
||||
|
||||
def normalize_optional_string(item, field):
|
||||
value = item.get(field, "")
|
||||
item[field] = "" if value is None else str(value).strip()
|
||||
|
||||
|
||||
def normalize_int(value, field, index=None, default=None, min_value=None, max_value=None):
|
||||
if value in (None, ""):
|
||||
value = default
|
||||
try:
|
||||
value = int(value)
|
||||
except (TypeError, ValueError):
|
||||
raise ApiError(400, f"{field} must be an integer", {"index": index})
|
||||
if min_value is not None:
|
||||
value = max(min_value, value)
|
||||
if max_value is not None:
|
||||
value = min(max_value, value)
|
||||
return value
|
||||
|
||||
|
||||
def validate_server(server, index=None):
|
||||
if not isinstance(server, dict):
|
||||
raise ApiError(400, "server must be an object", {"index": index})
|
||||
required = ["username", "name", "type", "host", "location", "password"]
|
||||
normalized = normalize_required_strings(server, required, "server", index=index)
|
||||
normalized["monthstart"] = normalize_int(normalized.get("monthstart"), "monthstart", index=index, default=1, min_value=1, max_value=28)
|
||||
if "disabled" in normalized:
|
||||
normalized["disabled"] = bool(normalized["disabled"])
|
||||
return normalized
|
||||
|
||||
|
||||
def validate_monitor(monitor, index=None):
|
||||
if not isinstance(monitor, dict):
|
||||
raise ApiError(400, "monitor must be an object", {"index": index})
|
||||
normalized = normalize_required_strings(monitor, ["name", "host", "type"], "monitor", index=index)
|
||||
normalized["interval"] = normalize_int(normalized.get("interval"), "interval", index=index, default=600, min_value=1)
|
||||
return normalized
|
||||
|
||||
|
||||
def validate_sslcert(sslcert, index=None):
|
||||
if not isinstance(sslcert, dict):
|
||||
raise ApiError(400, "sslcert must be an object", {"index": index})
|
||||
normalized = normalize_required_strings(sslcert, ["name", "domain"], "sslcert", index=index)
|
||||
normalized["port"] = normalize_int(normalized.get("port"), "port", index=index, default=443, min_value=1, max_value=65535)
|
||||
normalized["interval"] = normalize_int(normalized.get("interval"), "interval", index=index, default=7200, min_value=1)
|
||||
normalize_optional_string(normalized, "callback")
|
||||
return normalized
|
||||
|
||||
|
||||
def validate_watchdog(watchdog, index=None):
|
||||
if not isinstance(watchdog, dict):
|
||||
raise ApiError(400, "watchdog must be an object", {"index": index})
|
||||
normalized = normalize_required_strings(watchdog, ["name", "rule"], "watchdog", index=index)
|
||||
normalized["interval"] = normalize_int(normalized.get("interval"), "interval", index=index, default=600, min_value=1)
|
||||
normalize_optional_string(normalized, "callback")
|
||||
return normalized
|
||||
|
||||
|
||||
COLLECTIONS = {
|
||||
"servers": {
|
||||
"item": "server",
|
||||
"id_field": "username",
|
||||
"validator": validate_server,
|
||||
"required": ["username", "name", "type", "host", "location", "password"],
|
||||
"optional": ["monthstart", "disabled"],
|
||||
},
|
||||
"monitors": {
|
||||
"item": "monitor",
|
||||
"id_field": "name",
|
||||
"validator": validate_monitor,
|
||||
"required": ["name", "host", "type"],
|
||||
"optional": ["interval"],
|
||||
},
|
||||
"sslcerts": {
|
||||
"item": "sslcert",
|
||||
"id_field": "name",
|
||||
"validator": validate_sslcert,
|
||||
"required": ["name", "domain"],
|
||||
"optional": ["port", "interval", "callback"],
|
||||
},
|
||||
"watchdog": {
|
||||
"item": "watchdog",
|
||||
"id_field": "name",
|
||||
"validator": validate_watchdog,
|
||||
"required": ["name", "rule"],
|
||||
"optional": ["interval", "callback"],
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
def validate_config(config):
|
||||
if not isinstance(config, dict):
|
||||
raise ApiError(400, "config must be a JSON object")
|
||||
config = dict(config)
|
||||
for key, meta in COLLECTIONS.items():
|
||||
items = config.get(key, [])
|
||||
if not isinstance(items, list):
|
||||
raise ApiError(400, f"{key} must be an array")
|
||||
normalized = []
|
||||
seen = set()
|
||||
for index, item in enumerate(items):
|
||||
entry = meta["validator"](item, index=index)
|
||||
if key == "servers":
|
||||
username = entry["username"]
|
||||
if username in seen:
|
||||
raise ApiError(409, "duplicate server username", {"username": username})
|
||||
seen.add(username)
|
||||
normalized.append(entry)
|
||||
config[key] = normalized
|
||||
return config
|
||||
|
||||
|
||||
def write_config(config):
|
||||
config = validate_config(config)
|
||||
directory = os.path.dirname(CONFIG_PATH) or "."
|
||||
os.makedirs(directory, exist_ok=True)
|
||||
timestamp = time.strftime("%Y%m%d-%H%M%S")
|
||||
if os.path.exists(CONFIG_PATH):
|
||||
shutil.copy2(CONFIG_PATH, f"{CONFIG_PATH}.bak-{timestamp}")
|
||||
mode = os.stat(CONFIG_PATH).st_mode
|
||||
else:
|
||||
mode = 0o644
|
||||
data = json.dumps(config, ensure_ascii=False, indent="\t") + "\n"
|
||||
fd, tmp_path = tempfile.mkstemp(prefix=".config.", suffix=".tmp", dir=directory)
|
||||
try:
|
||||
with os.fdopen(fd, "w", encoding="utf-8") as f:
|
||||
f.write(data)
|
||||
f.flush()
|
||||
os.fsync(f.fileno())
|
||||
os.chmod(tmp_path, mode)
|
||||
try:
|
||||
os.replace(tmp_path, CONFIG_PATH)
|
||||
except OSError as exc:
|
||||
if exc.errno != errno.EBUSY:
|
||||
raise
|
||||
# A single-file Docker bind mount cannot be atomically replaced.
|
||||
# Keep the backup above, then rewrite the mounted file in place.
|
||||
with open(CONFIG_PATH, "w", encoding="utf-8") as f:
|
||||
f.write(data)
|
||||
f.flush()
|
||||
os.fsync(f.fileno())
|
||||
finally:
|
||||
if os.path.exists(tmp_path):
|
||||
os.unlink(tmp_path)
|
||||
return config
|
||||
|
||||
|
||||
def write_and_reload(config):
|
||||
config = write_config(config)
|
||||
pid = signal_sergate(signal.SIGHUP)
|
||||
return config, pid
|
||||
|
||||
|
||||
def read_body(handler):
|
||||
length = int(handler.headers.get("Content-Length", "0") or "0")
|
||||
if length <= 0:
|
||||
return None
|
||||
if length > 1024 * 1024:
|
||||
raise ApiError(413, "request body is too large")
|
||||
raw = handler.rfile.read(length).decode("utf-8")
|
||||
try:
|
||||
return json.loads(raw)
|
||||
except json.JSONDecodeError as exc:
|
||||
raise ApiError(400, "invalid JSON body", {"error": str(exc)})
|
||||
|
||||
|
||||
def get_sergate_pid():
|
||||
try:
|
||||
with open(SERGATE_PID_FILE, "r", encoding="utf-8") as f:
|
||||
pid = int(f.read().strip())
|
||||
os.kill(pid, 0)
|
||||
return pid
|
||||
except Exception:
|
||||
pass
|
||||
proc_dir = "/proc"
|
||||
if not os.path.isdir(proc_dir):
|
||||
return None
|
||||
for name in os.listdir(proc_dir):
|
||||
if not name.isdigit():
|
||||
continue
|
||||
try:
|
||||
with open(os.path.join(proc_dir, name, "cmdline"), "rb") as f:
|
||||
cmdline = f.read().replace(b"\x00", b" ").decode("utf-8", "ignore")
|
||||
except Exception:
|
||||
continue
|
||||
if "sergate" in cmdline:
|
||||
return int(name)
|
||||
return None
|
||||
|
||||
|
||||
def signal_sergate(sig):
|
||||
pid = get_sergate_pid()
|
||||
if not pid:
|
||||
raise ApiError(503, "sergate process was not found")
|
||||
os.kill(pid, sig)
|
||||
return pid
|
||||
|
||||
|
||||
def find_server(config, username):
|
||||
servers = config.get("servers", [])
|
||||
for index, server in enumerate(servers):
|
||||
if server.get("username") == username:
|
||||
return index, server
|
||||
return -1, None
|
||||
|
||||
|
||||
def find_collection_item(config, key, item_id):
|
||||
items = config.get(key, [])
|
||||
if item_id.isdigit():
|
||||
index = int(item_id)
|
||||
if 0 <= index < len(items):
|
||||
return index, items[index]
|
||||
return -1, None
|
||||
id_field = COLLECTIONS[key]["id_field"]
|
||||
matches = [(index, item) for index, item in enumerate(items) if str(item.get(id_field, "")) == item_id]
|
||||
if len(matches) > 1:
|
||||
raise ApiError(409, f"{key} has duplicate {id_field}; use numeric index instead", {"id": item_id})
|
||||
if matches:
|
||||
return matches[0]
|
||||
return -1, None
|
||||
|
||||
|
||||
def collection_routes():
|
||||
endpoints = []
|
||||
for key in ["monitors", "sslcerts", "watchdog"]:
|
||||
endpoints.extend([
|
||||
{"method": "GET", "path": f"/api/{key}", "auth": True},
|
||||
{"method": "POST", "path": f"/api/{key}", "auth": True, "body": f"{COLLECTIONS[key]['item']} JSON"},
|
||||
{"method": "PUT", "path": f"/api/{key}/{{index-or-name}}", "auth": True, "body": f"{COLLECTIONS[key]['item']} JSON"},
|
||||
{"method": "DELETE", "path": f"/api/{key}/{{index-or-name}}", "auth": True},
|
||||
])
|
||||
return endpoints
|
||||
|
||||
|
||||
def api_schema():
|
||||
return {
|
||||
"auth": {
|
||||
"type": "bearer",
|
||||
"header": "Authorization: Bearer <ADMIN_TOKEN>",
|
||||
"enabled": bool(ADMIN_TOKEN),
|
||||
},
|
||||
"endpoints": [
|
||||
{"method": "GET", "path": "/api/health", "auth": False},
|
||||
{"method": "GET", "path": "/api/schema", "auth": False},
|
||||
{"method": "GET", "path": "/api/config", "auth": True},
|
||||
{"method": "PUT", "path": "/api/config", "auth": True, "body": "full config JSON"},
|
||||
{"method": "GET", "path": "/api/servers", "auth": True},
|
||||
{"method": "POST", "path": "/api/servers", "auth": True, "body": "server JSON"},
|
||||
{"method": "PUT", "path": "/api/servers/{username}", "auth": True, "body": "server JSON"},
|
||||
{"method": "DELETE", "path": "/api/servers/{username}", "auth": True},
|
||||
*collection_routes(),
|
||||
{"method": "POST", "path": "/api/reload", "auth": True},
|
||||
{"method": "POST", "path": "/api/restart", "auth": True},
|
||||
],
|
||||
"collections": {
|
||||
key: {
|
||||
"item": meta["item"],
|
||||
"idField": meta["id_field"],
|
||||
"required": meta["required"],
|
||||
"optional": meta["optional"],
|
||||
}
|
||||
for key, meta in COLLECTIONS.items()
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
class Handler(BaseHTTPRequestHandler):
|
||||
server_version = "ServerStatusManageAPI/1.0"
|
||||
|
||||
def log_message(self, fmt, *args):
|
||||
print("%s - %s" % (self.address_string(), fmt % args), flush=True)
|
||||
|
||||
def end_headers(self):
|
||||
self.send_header("Cache-Control", "no-store")
|
||||
if CORS_ORIGIN:
|
||||
self.send_header("Access-Control-Allow-Origin", CORS_ORIGIN)
|
||||
self.send_header("Access-Control-Allow-Headers", "Authorization, Content-Type, X-Admin-Token")
|
||||
self.send_header("Access-Control-Allow-Methods", "GET, POST, PUT, DELETE, OPTIONS")
|
||||
super().end_headers()
|
||||
|
||||
def send_json(self, status, payload):
|
||||
data = json.dumps(payload, ensure_ascii=False).encode("utf-8")
|
||||
self.send_response(status)
|
||||
self.send_header("Content-Type", "application/json; charset=utf-8")
|
||||
self.send_header("Content-Length", str(len(data)))
|
||||
self.end_headers()
|
||||
self.wfile.write(data)
|
||||
|
||||
def send_error_json(self, err):
|
||||
payload = {"ok": False, "error": err.message}
|
||||
if err.details is not None:
|
||||
payload["details"] = err.details
|
||||
self.send_json(err.status, payload)
|
||||
|
||||
def route(self):
|
||||
parsed = urlparse(self.path)
|
||||
path = parsed.path.rstrip("/") or "/"
|
||||
method = self.command.upper()
|
||||
if method == "OPTIONS":
|
||||
self.send_response(204)
|
||||
self.end_headers()
|
||||
return
|
||||
try:
|
||||
if path == "/api/health" and method == "GET":
|
||||
pid = get_sergate_pid()
|
||||
self.send_json(200, {
|
||||
"ok": True,
|
||||
"enabled": bool(ADMIN_TOKEN),
|
||||
"sergate": {"running": bool(pid), "pid": pid},
|
||||
"configPath": CONFIG_PATH,
|
||||
})
|
||||
return
|
||||
if path == "/api/schema" and method == "GET":
|
||||
self.send_json(200, {"ok": True, "schema": api_schema()})
|
||||
return
|
||||
self.require_auth()
|
||||
if path == "/api/config":
|
||||
if method == "GET":
|
||||
self.send_json(200, {"ok": True, "config": load_config()})
|
||||
return
|
||||
if method == "PUT":
|
||||
config = read_body(self)
|
||||
config, pid = write_and_reload(config)
|
||||
self.send_json(200, {"ok": True, "reloaded": True, "pid": pid, "config": config})
|
||||
return
|
||||
if path == "/api/servers":
|
||||
if method == "GET":
|
||||
config = load_config()
|
||||
self.send_json(200, {"ok": True, "servers": config.get("servers", [])})
|
||||
return
|
||||
if method == "POST":
|
||||
server = validate_server(read_body(self))
|
||||
config = load_config()
|
||||
if find_server(config, server["username"])[1] is not None:
|
||||
raise ApiError(409, "server username already exists", {"username": server["username"]})
|
||||
config.setdefault("servers", []).append(server)
|
||||
config, pid = write_and_reload(config)
|
||||
self.send_json(201, {"ok": True, "server": server, "reloaded": True, "pid": pid, "config": config})
|
||||
return
|
||||
if path.startswith("/api/servers/"):
|
||||
username = unquote(path[len("/api/servers/"):])
|
||||
if not username:
|
||||
raise ApiError(400, "username is required")
|
||||
config = load_config()
|
||||
index, existing = find_server(config, username)
|
||||
if index < 0:
|
||||
raise ApiError(404, "server was not found", {"username": username})
|
||||
if method == "PUT":
|
||||
server = validate_server(read_body(self))
|
||||
if server["username"] != username and find_server(config, server["username"])[1] is not None:
|
||||
raise ApiError(409, "server username already exists", {"username": server["username"]})
|
||||
config["servers"][index] = server
|
||||
config, pid = write_and_reload(config)
|
||||
self.send_json(200, {"ok": True, "server": server, "reloaded": True, "pid": pid, "config": config})
|
||||
return
|
||||
if method == "DELETE":
|
||||
removed = config["servers"].pop(index)
|
||||
config, pid = write_and_reload(config)
|
||||
self.send_json(200, {"ok": True, "removed": removed, "reloaded": True, "pid": pid, "config": config})
|
||||
return
|
||||
for key in ["monitors", "sslcerts", "watchdog"]:
|
||||
base = f"/api/{key}"
|
||||
meta = COLLECTIONS[key]
|
||||
if path == base:
|
||||
if method == "GET":
|
||||
config = load_config()
|
||||
self.send_json(200, {"ok": True, key: config.get(key, [])})
|
||||
return
|
||||
if method == "POST":
|
||||
item = meta["validator"](read_body(self))
|
||||
config = load_config()
|
||||
config.setdefault(key, []).append(item)
|
||||
config, pid = write_and_reload(config)
|
||||
self.send_json(201, {"ok": True, meta["item"]: item, "reloaded": True, "pid": pid, "config": config})
|
||||
return
|
||||
if path.startswith(base + "/"):
|
||||
item_id = unquote(path[len(base) + 1:])
|
||||
if not item_id:
|
||||
raise ApiError(400, "item id is required")
|
||||
config = load_config()
|
||||
index, existing = find_collection_item(config, key, item_id)
|
||||
if index < 0:
|
||||
raise ApiError(404, f"{meta['item']} was not found", {"id": item_id})
|
||||
if method == "PUT":
|
||||
item = meta["validator"](read_body(self))
|
||||
config[key][index] = item
|
||||
config, pid = write_and_reload(config)
|
||||
self.send_json(200, {"ok": True, meta["item"]: item, "reloaded": True, "pid": pid, "config": config})
|
||||
return
|
||||
if method == "DELETE":
|
||||
removed = config[key].pop(index)
|
||||
config, pid = write_and_reload(config)
|
||||
self.send_json(200, {"ok": True, "removed": removed, "reloaded": True, "pid": pid, "config": config})
|
||||
return
|
||||
if path == "/api/reload" and method == "POST":
|
||||
pid = signal_sergate(signal.SIGHUP)
|
||||
self.send_json(200, {"ok": True, "operation": "reload", "pid": pid})
|
||||
return
|
||||
if path == "/api/restart" and method == "POST":
|
||||
pid = signal_sergate(signal.SIGTERM)
|
||||
self.send_json(202, {"ok": True, "operation": "restart", "pid": pid})
|
||||
return
|
||||
raise ApiError(404, "endpoint was not found")
|
||||
except ApiError as err:
|
||||
self.send_error_json(err)
|
||||
except Exception as exc:
|
||||
self.send_error_json(ApiError(500, "internal server error", {"error": str(exc)}))
|
||||
|
||||
def require_auth(self):
|
||||
if not ADMIN_TOKEN:
|
||||
raise ApiError(503, "management API is disabled; set ADMIN_TOKEN to enable it")
|
||||
auth = self.headers.get("Authorization", "")
|
||||
token = ""
|
||||
if auth.lower().startswith("bearer "):
|
||||
token = auth[7:].strip()
|
||||
token = token or self.headers.get("X-Admin-Token", "").strip()
|
||||
if token != ADMIN_TOKEN:
|
||||
raise ApiError(401, "invalid or missing admin token")
|
||||
|
||||
def do_GET(self):
|
||||
self.route()
|
||||
|
||||
def do_POST(self):
|
||||
self.route()
|
||||
|
||||
def do_PUT(self):
|
||||
self.route()
|
||||
|
||||
def do_DELETE(self):
|
||||
self.route()
|
||||
|
||||
def do_OPTIONS(self):
|
||||
self.route()
|
||||
|
||||
|
||||
def main():
|
||||
httpd = ThreadingHTTPServer((API_BIND, API_PORT), Handler)
|
||||
print(f"manage-api listening on {API_BIND}:{API_PORT}", flush=True)
|
||||
httpd.serve_forever()
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -0,0 +1,26 @@
|
||||
server {
|
||||
listen 80;
|
||||
server_name _;
|
||||
|
||||
root /usr/share/nginx/html;
|
||||
index index.html;
|
||||
|
||||
location / {
|
||||
try_files $uri $uri/ =404;
|
||||
}
|
||||
|
||||
location /json/ {
|
||||
add_header Cache-Control "no-store";
|
||||
try_files $uri =404;
|
||||
}
|
||||
|
||||
location /api/ {
|
||||
proxy_pass http://127.0.0.1:35602;
|
||||
proxy_http_version 1.1;
|
||||
proxy_set_header Host $host;
|
||||
proxy_set_header X-Real-IP $remote_addr;
|
||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||
proxy_set_header X-Forwarded-Proto $scheme;
|
||||
proxy_read_timeout 30s;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user