added xterm for login

This commit is contained in:
2026-03-12 16:53:31 +01:00
parent 0beef70651
commit 3a82f9bab0
2 changed files with 161 additions and 101 deletions
+83 -68
View File
@@ -427,11 +427,15 @@ async function testProxy(prompt) {
// ── Claude Login im Proxy-Container ─────────────────────
let loginProcess = null;
function dockerExecStreaming(containerName, cmd, onData, onEnd) {
// ── Interaktives Terminal (xterm.js ↔ Docker Exec) ──────
const net = require("net");
function attachTerminal(clientWs, containerName, cmd) {
const createBody = JSON.stringify({
AttachStdout: true, AttachStderr: true, AttachStdin: false, Tty: true,
AttachStdin: true, AttachStdout: true, AttachStderr: true, Tty: true,
Cmd: Array.isArray(cmd) ? cmd : ["sh", "-c", cmd],
});
@@ -445,83 +449,92 @@ function dockerExecStreaming(containerName, cmd, onData, onEnd) {
res.on("data", (c) => data += c);
res.on("end", () => {
if (res.statusCode !== 201) {
onEnd(new Error(`Exec create: HTTP ${res.statusCode}`));
clientWs.send(JSON.stringify({ type: "term_error", error: `Exec create failed: HTTP ${res.statusCode}` }));
return;
}
const execId = JSON.parse(data).Id;
// Exec starten — raw TCP socket fuer bidirektionalen Stream
// Docker Exec mit Tty macht ein HTTP Upgrade auf raw stream
const startBody = JSON.stringify({ Detach: false, Tty: true });
const startReq = http.request({
socketPath: "/var/run/docker.sock",
path: `/exec/${execId}/start`,
method: "POST",
headers: { "Content-Type": "application/json", "Content-Length": Buffer.byteLength(startBody) },
}, (sRes) => {
// Tty=true: kein Multiplexing, direkt Text
sRes.on("data", (chunk) => {
const text = chunk.toString("utf-8");
if (text.trim()) onData(text);
});
sRes.on("end", () => onEnd(null));
sRes.on("error", (err) => onEnd(err));
const sock = net.connect({ path: "/var/run/docker.sock" }, () => {
// Raw HTTP Request senden (Upgrade-artig)
const req = `POST /exec/${execId}/start HTTP/1.1\r\nHost: localhost\r\nContent-Type: application/json\r\nConnection: Upgrade\r\nUpgrade: tcp\r\nContent-Length: ${Buffer.byteLength(startBody)}\r\n\r\n${startBody}`;
sock.write(req);
});
let headersParsed = false;
let headerBuf = "";
sock.on("data", (chunk) => {
if (!headersParsed) {
// HTTP Response Header parsen
headerBuf += chunk.toString("utf-8");
const headerEnd = headerBuf.indexOf("\r\n\r\n");
if (headerEnd === -1) return; // Noch nicht komplett
const headers = headerBuf.slice(0, headerEnd);
const remaining = chunk.slice(chunk.length - (headerBuf.length - headerEnd - 4));
headersParsed = true;
if (!headers.includes("200") && !headers.includes("101")) {
clientWs.send(JSON.stringify({ type: "term_error", error: `Exec start failed: ${headers.split("\r\n")[0]}` }));
sock.end();
return;
}
log("info", "proxy", "Terminal-Session gestartet");
clientWs.send(JSON.stringify({ type: "term_ready" }));
clientWs._termSock = sock;
// Verbleibende Daten nach dem Header
if (remaining.length > 0) {
clientWs.send(JSON.stringify({ type: "term_data", data: remaining.toString("base64") }));
}
return;
}
// Tty=true: Rohdaten direkt durchreichen
if (clientWs.readyState === WebSocket.OPEN) {
clientWs.send(JSON.stringify({ type: "term_data", data: chunk.toString("base64") }));
}
});
sock.on("end", () => {
log("info", "proxy", "Terminal-Session beendet");
if (clientWs.readyState === WebSocket.OPEN) {
clientWs.send(JSON.stringify({ type: "term_exit" }));
}
clientWs._termSock = null;
setTimeout(() => checkProxyAuth(), 1000);
});
sock.on("error", (err) => {
log("error", "proxy", `Terminal-Socket-Fehler: ${err.message}`);
if (clientWs.readyState === WebSocket.OPEN) {
clientWs.send(JSON.stringify({ type: "term_error", error: err.message }));
}
});
// Wenn Browser-Client disconnected, Socket schliessen
clientWs.on("close", () => {
if (sock && !sock.destroyed) sock.end();
});
startReq.on("error", (err) => onEnd(err));
startReq.write(startBody);
startReq.end();
});
});
createReq.on("error", (err) => onEnd(err));
createReq.on("error", (err) => {
clientWs.send(JSON.stringify({ type: "term_error", error: err.message }));
});
createReq.write(createBody);
createReq.end();
}
function stripAnsi(str) {
// ANSI escape sequences entfernen (Farben, Cursor, etc.)
return str.replace(/\x1b\[[0-9;?]*[a-zA-Z]/g, "")
.replace(/\x1b\][^\x07]*\x07/g, "")
.replace(/\x1b[><=][0-9]*/g, "")
.replace(/[\x00-\x08\x0e-\x1f]/g, "");
}
function startProxyLogin() {
if (loginProcess) {
log("warn", "proxy", "Login laeuft bereits");
return;
function handleTermInput(clientWs, data) {
if (clientWs._termSock && !clientWs._termSock.destroyed) {
clientWs._termSock.write(Buffer.from(data, "base64"));
}
loginProcess = true;
log("info", "proxy", "Starte Claude Login im Proxy-Container...");
broadcast({ type: "login_status", status: "starting" });
// TERM=dumb deaktiviert TUI, echo 1 waehlt Dark Mode bei Theme-Auswahl
dockerExecStreaming("aria-proxy", "echo 1 | TERM=dumb NO_COLOR=1 CI=1 claude login 2>&1",
(chunk) => {
const clean = stripAnsi(chunk).trim();
if (!clean) return;
log("info", "proxy", `[login] ${clean}`);
// URLs erkennen und hervorheben
const urlMatch = clean.match(/https?:\/\/[^\s\]]+/);
if (urlMatch) {
broadcast({ type: "login_url", url: urlMatch[0], raw: clean });
} else {
broadcast({ type: "login_output", text: clean });
}
},
(err) => {
loginProcess = null;
if (err) {
log("error", "proxy", `Login fehlgeschlagen: ${err.message}`);
broadcast({ type: "login_status", status: "error", error: err.message });
} else {
log("info", "proxy", "Login-Prozess beendet");
broadcast({ type: "login_status", status: "done" });
// Auth nochmal pruefen
setTimeout(() => checkProxyAuth(), 1000);
}
}
);
}
// Credentials manuell einfuegen (von einem Rechner wo Claude eingeloggt ist)
@@ -737,7 +750,9 @@ wss.on("connection", (ws) => {
} else if (msg.action === "check_proxy_auth") {
checkProxyAuth();
} else if (msg.action === "proxy_login") {
startProxyLogin();
attachTerminal(ws, "aria-proxy", "claude login");
} else if (msg.action === "term_input") {
handleTermInput(ws, msg.data);
} else if (msg.action === "write_credentials") {
writeProxyCredentials(msg.credentials);
} else if (msg.action === "docker_logs") {