Files
html/api/opus5-ssh-tmux-stream.php
2026-04-17 18:19:00 +02:00

167 lines
7.1 KiB
PHP

<?php
// OPUS5 — SSH multiplexé tmux pour commandes longues (doctrine 79)
// Actions: create_session, send_cmd, capture, list_sessions, kill_session
// Usage: long apt upgrades, batch imports, audits complets sur S95
header('Content-Type: application/json');
$R = ['ts'=>date('c'), 'source'=>'opus5-ssh-tmux-stream'];
$S95_HOST = '10.1.0.3';
$S95_PORT = '49222';
$S95_USER = 'root';
// Whitelist commands (safety)
$ALLOWED_CMDS = ['ls', 'find', 'grep', 'cat', 'head', 'tail', 'wc', 'ps', 'df', 'du',
'curl', 'wget', 'apt-get', 'apt', 'systemctl', 'journalctl', 'netstat', 'ss',
'psql', 'redis-cli', 'tmux', 'echo', 'date', 'uname',
'docker', 'pgrep', 'pkill', 'kill', 'pkill',
'awk', 'sed', 'sort', 'uniq', 'cut', 'tr',
'rsync', 'tar', 'zcat', 'gzip', 'gunzip',
'python3', 'php', 'bash', 'sh'];
function ssh_exec($cmd, $S95_USER, $S95_HOST, $S95_PORT, $timeout = 10) {
$ec = escapeshellcmd($cmd);
$ssh = "ssh -o StrictHostKeyChecking=no -o ConnectTimeout=3 -p $S95_PORT $S95_USER@$S95_HOST ";
return shell_exec("timeout $timeout $ssh " . escapeshellarg($cmd) . " 2>&1");
}
function is_cmd_allowed($cmd, $allowed) {
$tokens = preg_split('/[\s|&;]+/', trim($cmd));
foreach ($tokens as $t) {
$t = trim($t);
if (!$t || $t[0] === '-' || $t[0] === '$') continue;
$t = basename($t);
if (in_array($t, $allowed)) return true;
}
return false;
}
$raw = file_get_contents('php://input');
$d = json_decode($raw, true) ?: [];
$action = $_GET['action'] ?? ($d['action'] ?? 'list');
if ($action === 'health') {
$out = ssh_exec('tmux -V', $S95_USER, $S95_HOST, $S95_PORT, 5);
$R['s95_reachable'] = (bool)$out && strpos($out, 'tmux') !== false;
$R['s95_tmux_version'] = trim((string)$out);
$R['host'] = $S95_HOST;
echo json_encode($R, JSON_PRETTY_PRINT);
exit;
}
if ($action === 'list_sessions') {
$out = ssh_exec('tmux ls 2>/dev/null || echo NONE', $S95_USER, $S95_HOST, $S95_PORT, 8);
$R['raw'] = trim((string)$out);
$sessions = [];
if ($R['raw'] && $R['raw'] !== 'NONE') {
foreach (explode("\n", $R['raw']) as $line) {
if (preg_match('/^([a-z0-9_-]+):\s+(\d+)\s+windows/i', $line, $m)) {
$sessions[] = ['name' => $m[1], 'windows' => (int)$m[2]];
}
}
}
$R['sessions'] = $sessions;
$R['count'] = count($sessions);
echo json_encode($R, JSON_PRETTY_PRINT);
exit;
}
if ($action === 'create_session' && $_SERVER['REQUEST_METHOD'] === 'POST') {
$name = preg_replace('/[^a-z0-9_-]/i', '', (string)($d['name'] ?? 'wevia_' . bin2hex(random_bytes(3))));
$cmd = (string)($d['cmd'] ?? 'htop'); // default : just open a session
if (!is_cmd_allowed($cmd, $ALLOWED_CMDS)) {
http_response_code(403);
echo json_encode(['err'=>'cmd_not_whitelisted', 'cmd'=>$cmd]);
exit;
}
// Log file on S95 pour capture ultérieure
$log = "/tmp/wevia_tmux_$name.log";
// tmux new -d -s <name> "bash -c 'exec > >(tee <log>) 2>&1; <cmd>'"
$tmux_cmd = "tmux new -d -s $name 'bash -c \"exec >$log 2>&1; $cmd; sleep 1\"'";
$out = ssh_exec($tmux_cmd, $S95_USER, $S95_HOST, $S95_PORT, 10);
// Verify
$verify = ssh_exec("tmux has-session -t $name 2>&1 && echo EXISTS || echo MISSING", $S95_USER, $S95_HOST, $S95_PORT, 5);
$R['session'] = $name;
$R['cmd'] = $cmd;
$R['log_path_on_s95'] = $log;
$R['verify'] = trim((string)$verify);
$R['created'] = trim((string)$verify) === 'EXISTS';
$R['create_raw'] = trim((string)$out);
// Log in PG task table aussi (unifié)
try {
$pdo = new PDO('pgsql:host=10.1.0.3;port=5432;dbname=adx_system;user=admin;password=admin123', null, null, [PDO::ATTR_ERRMODE=>PDO::ERRMODE_EXCEPTION, PDO::ATTR_TIMEOUT=>3]);
$tid = 'ssh_tmux_' . date('YmdHis') . '_' . substr($name, -6);
$stmt = $pdo->prepare("INSERT INTO admin.wevia_tasks (task_id, type, status, input, session, started_at) VALUES (?, 'ssh_tmux', 'running', ?, ?, NOW())");
$stmt->execute([$tid, $cmd, $name]);
$R['task_id'] = $tid;
} catch (Throwable $e) { $R['pg_warn'] = substr($e->getMessage(), 0, 100); }
echo json_encode($R, JSON_PRETTY_PRINT);
exit;
}
if ($action === 'capture') {
$name = preg_replace('/[^a-z0-9_-]/i', '', (string)($_GET['name'] ?? $d['name'] ?? ''));
if (!$name) { http_response_code(400); echo json_encode(['err'=>'no_session_name']); exit; }
$lines = (int)($_GET['lines'] ?? 100);
$lines = max(10, min(1000, $lines));
// 2 captures : pane content + log file tail
$pane = ssh_exec("tmux capture-pane -t $name -p 2>/dev/null | tail -$lines", $S95_USER, $S95_HOST, $S95_PORT, 8);
$log_tail = ssh_exec("tail -$lines /tmp/wevia_tmux_$name.log 2>/dev/null || echo '[no log]'", $S95_USER, $S95_HOST, $S95_PORT, 8);
$R['session'] = $name;
$R['pane_output'] = (string)$pane;
$R['log_tail'] = (string)$log_tail;
$R['lines'] = $lines;
// Check if still alive
$alive = ssh_exec("tmux has-session -t $name 2>&1 && echo ALIVE || echo DEAD", $S95_USER, $S95_HOST, $S95_PORT, 5);
$R['alive'] = trim((string)$alive) === 'ALIVE';
echo json_encode($R, JSON_PRETTY_PRINT|JSON_UNESCAPED_UNICODE);
exit;
}
if ($action === 'send_cmd' && $_SERVER['REQUEST_METHOD'] === 'POST') {
$name = preg_replace('/[^a-z0-9_-]/i', '', (string)($d['name'] ?? ''));
$cmd = (string)($d['cmd'] ?? '');
if (!$name || !$cmd) { http_response_code(400); echo json_encode(['err'=>'missing name or cmd']); exit; }
if (!is_cmd_allowed($cmd, $ALLOWED_CMDS)) {
http_response_code(403);
echo json_encode(['err'=>'cmd_not_whitelisted', 'cmd'=>$cmd]);
exit;
}
// tmux send-keys "<cmd>" Enter
$esc = str_replace('"', '\\"', $cmd);
$out = ssh_exec("tmux send-keys -t $name \"$esc\" Enter", $S95_USER, $S95_HOST, $S95_PORT, 8);
$R['session'] = $name;
$R['cmd_sent'] = $cmd;
$R['raw'] = trim((string)$out);
$R['ok'] = !$out || strpos((string)$out, 'error') === false;
echo json_encode($R, JSON_PRETTY_PRINT);
exit;
}
if ($action === 'kill_session' && $_SERVER['REQUEST_METHOD'] === 'POST') {
$name = preg_replace('/[^a-z0-9_-]/i', '', (string)($d['name'] ?? ''));
if (!$name) { http_response_code(400); echo json_encode(['err'=>'no_session_name']); exit; }
$out = ssh_exec("tmux kill-session -t $name 2>&1", $S95_USER, $S95_HOST, $S95_PORT, 5);
$R['session'] = $name;
$R['killed'] = true;
$R['raw'] = trim((string)$out);
// Update PG task
try {
$pdo = new PDO('pgsql:host=10.1.0.3;port=5432;dbname=adx_system;user=admin;password=admin123', null, null, [PDO::ATTR_ERRMODE=>PDO::ERRMODE_EXCEPTION, PDO::ATTR_TIMEOUT=>3]);
$pdo->prepare("UPDATE admin.wevia_tasks SET status='killed', finished_at=NOW() WHERE session=? AND status='running'")->execute([$name]);
} catch (Throwable $e) {}
echo json_encode($R, JSON_PRETTY_PRINT);
exit;
}
http_response_code(400);
echo json_encode(['err'=>'unknown_action', 'available'=>['health','list_sessions','create_session','send_cmd','capture','kill_session'], 'doctrine'=>'76 — SSH multiplexé tmux']);