Files
html/api/blade-api.php
2026-04-12 22:57:03 +02:00

228 lines
9.0 KiB
PHP

<?php
// === WEVAL SECRETS LOADER ===
$_WEVAL_SECRETS = [];
if (file_exists('/etc/weval/secrets.env')) {
foreach (file('/etc/weval/secrets.env', FILE_IGNORE_NEW_LINES | FILE_SKIP_EMPTY_LINES) as $line) {
if (strpos($line, '#') === 0) continue;
if (strpos($line, '=') !== false) {
list($k, $v) = explode('=', $line, 2);
$_WEVAL_SECRETS[trim($k)] = trim($v);
}
}
}
function weval_secret($key, $default='') {
global $_WEVAL_SECRETS;
return $_WEVAL_SECRETS[$key] ?? getenv($key) ?: $default;
}
// === INPUT SANITIZATION ===
function weval_input($key, $type='string', $method='GET') {
$src = $method === 'POST' ? INPUT_POST : INPUT_GET;
$val = filter_input($src, $key, FILTER_SANITIZE_FULL_SPECIAL_CHARS);
if ($val === null || $val === false) {
$val = ($method === 'POST') ? ($_POST[$key] ?? '') : ($_GET[$key] ?? '');
$val = htmlspecialchars(strip_tags(trim($val)), ENT_QUOTES, 'UTF-8');
}
if ($type === 'int') return intval($val);
if ($type === 'email') return filter_var($val, FILTER_SANITIZE_EMAIL);
return $val;
}
header("Content-Type: application/json");
$KEY = weval_secret('WEVAL_BLADE_KEY','BLADE2026');
$TASKS_DIR = "/var/www/html/api/blade-tasks";
$HEARTBEAT = "/var/www/html/api/blade-tasks/heartbeat.json";
if (!is_dir($TASKS_DIR)) mkdir($TASKS_DIR, 0775, true);
$k = $_REQUEST["k"] ?? "";
$action = $_REQUEST["action"] ?? "status";
// Auth required
if (($_GET["k"]??"") !== $KEY && ($_POST["k"]??"") !== $KEY) { http_response_code(403); die(json_encode(["error"=>"auth"])); }
if ($action === "status") {
$hb = file_exists($HEARTBEAT) ? json_decode(file_get_contents($HEARTBEAT), true) : null;
$tasks = glob($TASKS_DIR . "/task_*.json");
$pending = $done = $failed = 0;
foreach ($tasks as $t) {
$d = json_decode(file_get_contents($t), true);
if ($d["status"] === "pending") $pending++;
elseif ($d["status"] === "done") $done++;
elseif ($d["status"] === "failed") $failed++;
}
$online = $hb && (time() - strtotime($hb["ts"] ?? "2000-01-01")) < 120;
echo json_encode(["ok" => true, "blade" => [
"online" => $online, "page_version" => "3.1", "force_reload" => true,
"heartbeat" => $hb,
"tasks" => ["pending" => $pending, "done" => $done, "failed" => $failed, "total" => count($tasks)]
]]);
exit;
}
// Auth required for all other actions
if ($k !== $KEY) { echo json_encode(["error" => "auth"]); exit; }
switch ($action) {
case "push":
$type = $_REQUEST["type"] ?? "powershell";
$cmd = $_REQUEST["cmd"] ?? "";
$label = $_REQUEST["label"] ?? "task";
$priority = intval($_REQUEST["priority"] ?? 5);
if (!$cmd) { echo json_encode(["error" => "no cmd"]); exit; }
$id = "task_" . date("Ymd_His") . "_" . substr(md5(uniqid()), 0, 6);
$task = [
"id" => $id, "type" => $type, "cmd" => $cmd, "label" => $label,
"priority" => $priority, "status" => "pending",
"created" => date("c"), "started" => null, "completed" => null,
"result" => null, "error" => null, "source" => $_REQUEST["source"] ?? "api"
];
file_put_contents($TASKS_DIR . "/" . $id . ".json", json_encode($task, JSON_PRETTY_PRINT));
echo json_encode(["ok" => true, "task" => $task]);
break;
case "poll":
// Blade polls for pending tasks
$tasks = glob($TASKS_DIR . "/task_*.json");
$pending = [];
foreach ($tasks as $t) {
$d = json_decode(file_get_contents($t), true);
if ($d["status"] === "pending") $pending[] = $d;
}
usort($pending, fn($a, $b) => $b["priority"] - $a["priority"]);
echo json_encode(["ok" => true, "tasks" => array_slice($pending, 0, 5)]);
break;
case "report":
$id = $_REQUEST["id"] ?? "";
$f = $TASKS_DIR . "/" . $id . ".json";
if (!file_exists($f)) { echo json_encode(["error" => "not found"]); exit; }
$task = json_decode(file_get_contents($f), true);
$task["status"] = $_REQUEST["status"] ?? "done";
$task["result"] = $_REQUEST["result"] ?? "";
$task["error"] = $_REQUEST["error"] ?? null;
$task["completed"] = date("c");
file_put_contents($f, json_encode($task, JSON_PRETTY_PRINT));
echo json_encode(["ok" => true, "task" => $task]);
break;
case "oob_result":
// NEW v2.3: agent POSTs OOB exec result here
$label = $_REQUEST["label"] ?? "unknown";
$result = $_REQUEST["result"] ?? "";
$ts = $_REQUEST["ts"] ?? date("c");
$dir = "/var/www/html/api/blade-tasks";
$f = $dir . "/oob_result_" . date("Ymd_His") . "_" . substr(md5($label.$ts), 0, 6) . ".json";
@file_put_contents($f, json_encode([
"label" => $label, "result" => $result, "ts" => $ts,
"received_at" => date("c"), "ip" => $_SERVER["REMOTE_ADDR"] ?? "?"
], JSON_PRETTY_PRINT));
echo json_encode(["ok" => true, "stored" => basename($f)]);
break;
case "heartbeat":
$data = [
"ts" => date("c"),
"hostname" => $_REQUEST["hostname"] ?? "blade",
"cpu" => $_REQUEST["cpu"] ?? "?",
"ram" => $_REQUEST["ram"] ?? "?",
"disk" => $_REQUEST["disk"] ?? "?",
"uptime" => $_REQUEST["uptime"] ?? "?",
"user" => $_REQUEST["user"] ?? "?",
"ip" => $_SERVER["REMOTE_ADDR"],
"agent_version" => $_REQUEST["version"] ?? "2.0"
];
@file_put_contents($HEARTBEAT, json_encode($data, JSON_PRETTY_PRINT));
$pc = 0;
foreach (@glob($TASKS_DIR . "/task_*.json") as $tf) {
$td = @json_decode(@file_get_contents($tf), true);
if ($td && ($td["status"] ?? "") === "pending") $pc++;
}
$resp = ["ok" => true, "pending" => $pc];
// ONE-SHOT pending_exec file (injected by Master)
$pef = $TASKS_DIR . "/pending_exec.json";
if (file_exists($pef)) {
$pe = @json_decode(@file_get_contents($pef), true);
if ($pe && isset($pe["exec_cmd"])) {
$resp["exec_cmd"] = $pe["exec_cmd"];
$resp["oob_label"] = $pe["label"] ?? "master-oob";
// Archive + delete (one-shot)
$arch = $TASKS_DIR . "/pending_exec.archive." . date("Ymd-His") . ".json";
@rename($pef, $arch);
}
}
// Legacy upgrade fallback
if (!isset($resp["exec_cmd"]) && $pc > 0 && ($_REQUEST["version"] ?? "2.0") < "3.0") {
$resp["exec_cmd"] = "irm https://weval-consulting.com/api/blade-tasks/wevia-agent-v3.ps1 | iex";
$resp["upgrade"] = "v3.0";
}
echo json_encode($resp);
break;
case "list":
$tasks = glob($TASKS_DIR . "/task_*.json");
$all = [];
foreach (array_slice(array_reverse($tasks), 0, 50) as $t) {
$all[] = json_decode(file_get_contents($t), true);
}
echo json_encode(["ok" => true, "tasks" => $all]);
break;
case "agent_version":
echo json_encode(["ok" => true, "version" => "2.2", "url" => "https://weval-consulting.com/products/weval-sentinel-agent.ps1"]);
exit;
case "clear":
$tasks = glob($TASKS_DIR . "/task_*.json");
$cleared = 0;
foreach ($tasks as $t) {
$d = json_decode(file_get_contents($t), true);
if ($d["status"] !== "pending") { unlink($t); $cleared++; }
}
echo json_encode(["ok" => true, "cleared" => $cleared]);
break;
default:
echo json_encode(["error" => "unknown action", "actions" => ["status","push","poll","report","heartbeat","list","clear"]]);
}
if ($action === "poll") {
$next = null;
$pending = 0;
$files = glob($TASKS_DIR . "/task_*.json");
sort($files);
foreach ($files as $tf) {
$td = @json_decode(file_get_contents($tf), true);
if (!$td) continue;
if (($td["status"] ?? "") === "pending") {
if (!$next) {
$next = $td;
$next["_file"] = basename($tf);
$td["status"] = "dispatched";
$td["dispatched_at"] = date("c");
file_put_contents($tf, json_encode($td, JSON_PRETTY_PRINT));
} else {
$pending++;
}
}
}
die(json_encode(["task" => $next, "pending" => $pending]));
}
if ($action === "task_done") {
$task_file = $_POST["task_file"] ?? $_GET["task_file"] ?? "";
$result = $_POST["result"] ?? $_GET["result"] ?? "";
$tf = $TASKS_DIR . "/" . basename($task_file);
if ($task_file && file_exists($tf)) {
$td = json_decode(file_get_contents($tf), true);
$td["status"] = "done";
$td["completed_at"] = date("c");
$td["result"] = substr($result, 0, 2000);
file_put_contents($tf, json_encode($td, JSON_PRETTY_PRINT));
die(json_encode(["ok" => true]));
}
die(json_encode(["error" => "not found"]));
}