fix: 收紧 WebGUI 默认安全配置
This commit is contained in:
+43
-7
@@ -27,6 +27,15 @@ RCLONE_RC_URL = os.environ.get("RCLONE_RC_URL", "http://rclone:8080").rstrip("/"
|
||||
HOST = os.environ.get("JOBS_API_HOST", "0.0.0.0")
|
||||
PORT = int(os.environ.get("JOBS_API_PORT", "8081"))
|
||||
SCHEDULER_INTERVAL_SECONDS = int(os.environ.get("JOBS_SCHEDULER_INTERVAL", "15"))
|
||||
MAX_BODY_BYTES = int(os.environ.get("JOBS_API_MAX_BODY_BYTES", str(64 * 1024)))
|
||||
ALLOWED_ORIGINS = {
|
||||
origin.strip().rstrip("/")
|
||||
for origin in os.environ.get(
|
||||
"JOBS_API_ALLOWED_ORIGINS",
|
||||
"http://localhost:5580,http://127.0.0.1:5580",
|
||||
).split(",")
|
||||
if origin.strip()
|
||||
}
|
||||
VALID_ACTIONS = {"copy", "sync", "move"}
|
||||
VALID_SCHEDULE_KINDS = {"daily", "weekly", "monthly"}
|
||||
|
||||
@@ -646,11 +655,22 @@ def scheduler_loop() -> None:
|
||||
traceback.print_exc()
|
||||
|
||||
|
||||
def scheduler_initial_tick() -> None:
|
||||
try:
|
||||
scheduler_tick()
|
||||
except Exception:
|
||||
traceback.print_exc()
|
||||
|
||||
|
||||
class Handler(BaseHTTPRequestHandler):
|
||||
protocol_version = "HTTP/1.1"
|
||||
|
||||
def do_OPTIONS(self) -> None:
|
||||
self.send_json(204, None)
|
||||
try:
|
||||
self.require_allowed_origin()
|
||||
self.send_json(204, None)
|
||||
except ApiError as exc:
|
||||
self.send_json(exc.status, {"error": exc.message})
|
||||
|
||||
def do_GET(self) -> None:
|
||||
self.handle_request("GET")
|
||||
@@ -663,6 +683,7 @@ class Handler(BaseHTTPRequestHandler):
|
||||
|
||||
def handle_request(self, method: str) -> None:
|
||||
try:
|
||||
self.require_allowed_origin()
|
||||
parsed = urlparse(self.path)
|
||||
path = parsed.path.rstrip("/") or "/"
|
||||
parts = [p for p in path.split("/") if p]
|
||||
@@ -704,8 +725,22 @@ class Handler(BaseHTTPRequestHandler):
|
||||
traceback.print_exc()
|
||||
self.send_json(500, {"error": str(exc)})
|
||||
|
||||
def require_allowed_origin(self) -> None:
|
||||
origin = self.headers.get("Origin")
|
||||
if not origin:
|
||||
return
|
||||
if origin.rstrip("/") not in ALLOWED_ORIGINS:
|
||||
raise ApiError(403, "origin not allowed")
|
||||
|
||||
def read_json(self) -> dict[str, Any]:
|
||||
length = int(self.headers.get("Content-Length") or "0")
|
||||
try:
|
||||
length = int(self.headers.get("Content-Length") or "0")
|
||||
except ValueError:
|
||||
raise ApiError(400, "invalid Content-Length") from None
|
||||
if length < 0:
|
||||
raise ApiError(400, "invalid Content-Length")
|
||||
if length > MAX_BODY_BYTES:
|
||||
raise ApiError(413, "request body too large")
|
||||
if length == 0:
|
||||
return {}
|
||||
raw = self.rfile.read(length)
|
||||
@@ -719,8 +754,11 @@ class Handler(BaseHTTPRequestHandler):
|
||||
|
||||
def send_json(self, status: int, body: Any) -> None:
|
||||
raw = b"" if body is None else json.dumps(body, ensure_ascii=False).encode("utf-8")
|
||||
origin = self.headers.get("Origin")
|
||||
self.send_response(status)
|
||||
self.send_header("Access-Control-Allow-Origin", "*")
|
||||
if origin and origin.rstrip("/") in ALLOWED_ORIGINS:
|
||||
self.send_header("Access-Control-Allow-Origin", origin)
|
||||
self.send_header("Vary", "Origin")
|
||||
self.send_header("Access-Control-Allow-Headers", "Content-Type")
|
||||
self.send_header("Access-Control-Allow-Methods", "GET, POST, DELETE, OPTIONS")
|
||||
self.send_header("Content-Type", "application/json; charset=utf-8")
|
||||
@@ -747,10 +785,8 @@ def main() -> None:
|
||||
init_db()
|
||||
thread = threading.Thread(target=scheduler_loop, name="job-scheduler", daemon=True)
|
||||
thread.start()
|
||||
try:
|
||||
scheduler_tick()
|
||||
except Exception:
|
||||
traceback.print_exc()
|
||||
initial_thread = threading.Thread(target=scheduler_initial_tick, name="job-scheduler-initial", daemon=True)
|
||||
initial_thread.start()
|
||||
server = ThreadingHTTPServer((HOST, PORT), Handler)
|
||||
print(f"jobs api listening on {HOST}:{PORT}, db={DB_PATH}, rclone={RCLONE_RC_URL}")
|
||||
try:
|
||||
|
||||
Reference in New Issue
Block a user