Paskelbtas šaltinis
Atsisiųsti šaltinį ZIP
CH-J Server Manager
Naršyti aplankus ir failus konkrečiai programai išleisti.
CH-J / Server Manager
368 failai
core-20261017T201717Z-057348c4a70f
Pridėta: Data neregistruota · Pastatytas:
1
"use strict";3
const { SessionError, shellQuote } = require("./sessionManager");5
// Shared Core service for bounded privileged remote operations over an existing SSH session.6
// Callers pass a fixed script plus validated arguments; there is no API for arbitrary commands.7
// The sudo password is written to stdin only after sudo prints our prompt marker and is never8
// stored, logged or placed in argv. Payloads ("frames") are sent only after the script's READY marker.9
const SUDO_PROMPT = "[chj-sudo-auth]";10
const READY_MARKER = "\x1eCHJ_READY\n";11
const ERROR_MARKER = /\x1eCHJ_ERR=([A-Z0-9_]{2,64})\r?\n/;12
const MAX_PASSWORD_LENGTH = 1024;13
const PROBE_SCRIPT = String.raw`LC_ALL=C; export LC_ALL14
PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin; export PATH15
printf 'user=%s\nuid=%s\ngroups=%s\nclient=%s\n' "$(id -un)" "$(id -u)" "$(id -Gn 2>/dev/null | tr ' ' ',')" "${"$"}{SSH_CLIENT%% *}"16
if command -v sudo >/dev/null 2>&1; then17
out=$(sudo -n -p '' true 2>&1 </dev/null); code=$?18
printf 'sudoInstalled=1\nsudoNonInteractive=%s\nsudoMessage=%s\n' "$code" "$(printf '%s' "$out" | head -n 1 | tr -d '\r\n' | cut -c1-300)"19
else20
printf 'sudoInstalled=0\n'21
fi`;23
function normalizePassword(value) {24
if (value === undefined || value === null || value === "") return "";25
const password = String(value);26
if (password.length > MAX_PASSWORD_LENGTH || /[\0\r\n]/.test(password)) throw new SessionError("SUDO_PASSWORD_INVALID", "Invalid sudo password.");27
return password;28
}30
function countOccurrences(text, token) {31
let count = 0;32
for (let index = text.indexOf(token); index >= 0; index = text.indexOf(token, index + token.length)) count += 1;33
return count;34
}36
function cleanRemoteMessage(text, limit = 600) {37
return String(text || "")38
.split(SUDO_PROMPT).join("")39
.replace(/[\x00-\x08\x0b-\x1f\x7f]/g, " ")40
.split(/\r?\n/).map((line) => line.trim()).filter(Boolean)41
.slice(-6).join(" | ")42
.slice(0, limit);43
}45
function classifySudoFailure(stderr, code, state) {46
const text = String(stderr || "");47
if (state.authFailure) return state.authFailure;48
if (/incorrect password attempt|Sorry, try again/i.test(text)) return "SUDO_PASSWORD_INCORRECT";49
if (/is not in the sudoers file|is not allowed to run sudo|may not run sudo|not allowed to execute|is not allowed to run/i.test(text)) return "SUDO_NOT_PERMITTED";50
if (/must have a tty|a terminal is required|no tty present/i.test(text)) return "SUDO_TTY_REQUIRED";51
if (/a password is required|no password was provided/i.test(text)) return "SUDO_PASSWORD_REQUIRED";52
if (/account.*(locked|expired)|authentication (failure|token)|maximum number of tries/i.test(text)) return "SUDO_AUTH_FAILED";53
if (code === 127 || /sudo: (command )?not found/i.test(text)) return "SUDO_UNAVAILABLE";54
return "SUDO_FAILED";55
}57
const SUDO_MESSAGES = Object.freeze({58
SUDO_PASSWORD_REQUIRED: "This operation requires the sudo password of the SSH account.",59
SUDO_PASSWORD_INCORRECT: "The sudo password was rejected.",60
SUDO_NOT_PERMITTED: "The SSH account is not permitted to run this operation with sudo.",61
SUDO_TTY_REQUIRED: "The sudo policy requires a terminal (requiretty); CH-J does not bypass it.",62
SUDO_AUTH_FAILED: "sudo authentication failed or the account is locked.",63
SUDO_UNAVAILABLE: "sudo is not installed and the SSH account is not root.",64
SUDO_FAILED: "sudo did not start the operation."65
});67
function parseKeyValues(text) {68
const values = {};69
for (const line of String(text || "").split(/\r?\n/)) {70
const index = line.indexOf("=");71
if (index > 0) values[line.slice(0, index)] = line.slice(index + 1);72
}73
return values;74
}76
function publicPrivilege(privilege) {77
return {78
mode: privilege.mode,79
verified: privilege.verified === true,80
username: privilege.username,81
sudoInstalled: privilege.sudoInstalled === true,82
groups: [...(privilege.groups || [])],83
clientAddress: privilege.clientAddress || "",84
message: privilege.message || "",85
checkedAt: privilege.checkedAt86
};87
}89
class PrivilegedExecutor {90
constructor(options) {91
this.sessionManager = options.sessionManager;92
this.logger = options.logger;93
}95
// Detects what the SSH account can do without prompting: root, sudo without password,96
// sudo with password (not yet verified), or no sudo at all. Cached per live session.97
async probe(sessionId, { refresh = false } = {}) {98
const record = this.sessionManager._requireConnected(sessionId);99
if (record.privilege && !refresh) return publicPrivilege(record.privilege);100
const result = await this.sessionManager._execStream(record, `sh -c ${shellQuote(PROBE_SCRIPT)}`, { timeoutMs: 15000 }).done;101
const values = parseKeyValues(result.stdout);102
const privilege = {103
username: values.user || record.username,104
sudoInstalled: values.sudoInstalled === "1",105
groups: String(values.groups || "").split(",").filter(Boolean).slice(0, 256),106
clientAddress: String(values.client || "").trim().slice(0, 64),107
message: cleanRemoteMessage(values.sudoMessage || "", 300),108
checkedAt: new Date().toISOString(),109
mode: "unknown",110
verified: false111
};112
if (values.uid === "0") Object.assign(privilege, { mode: "root", verified: true });113
else if (!privilege.sudoInstalled) Object.assign(privilege, { mode: "none", verified: true });114
else if (values.sudoNonInteractive === "0") Object.assign(privilege, { mode: "sudo-nopasswd", verified: true });115
else if (/password is required/i.test(values.sudoMessage || "")) privilege.mode = "sudo-password";116
else if (/not allowed|may not run sudo|not in the sudoers/i.test(values.sudoMessage || "")) Object.assign(privilege, { mode: "none", verified: true });117
record.privilege = privilege;118
return publicPrivilege(privilege);119
}121
// Runs a fixed script. With privileged=true it runs as root (root session) or via sudo -S.122
// Frames are { tag, data } pairs written as base64 lines after READY. interact(channel) may drive123
// a confirmation protocol; otherwise stdin is closed after the frames.124
async run(sessionId, options) {125
const record = this.sessionManager._requireConnected(sessionId);126
const privileged = options.privileged !== false;127
const password = normalizePassword(options.sudoPassword);128
const privilege = privileged ? await this.probe(sessionId) : null;129
if (privileged && privilege.mode === "none" && privilege.verified) {130
throw new SessionError(privilege.sudoInstalled ? "SUDO_NOT_PERMITTED" : "SUDO_UNAVAILABLE",131
privilege.sudoInstalled ? SUDO_MESSAGES.SUDO_NOT_PERMITTED : SUDO_MESSAGES.SUDO_UNAVAILABLE);132
}133
const useSudo = privileged && privilege.mode !== "root";134
const body = `sh -c ${shellQuote(options.script)} chj ${(options.args || []).map((value) => shellQuote(value)).join(" ")}`;135
const command = useSudo ? `LC_ALL=C sudo -S -p ${shellQuote(SUDO_PROMPT)} -- ${body}` : `LC_ALL=C ${body}`;136
const frames = (options.frames || []).map(({ tag, data }) => {137
if (!/^[A-Z]{2,16}$/.test(tag)) throw new SessionError("PROTOCOL_ERROR", "Invalid frame tag.");138
return `CHJ${tag}:${Buffer.from(data ?? "", typeof data === "string" ? "utf8" : undefined).toString("base64")}\n`;139
});140
const state = { answered: 0, started: false, authFailure: null, interaction: null };141
const listeners = new Set();142
const channel = this.sessionManager._execStream(record, command, {143
timeoutMs: options.timeoutMs || 60000,144
outputLimit: options.outputLimit || 4 * 1024 * 1024,145
onUpdate: (controller) => {146
if (useSudo && !state.started) {147
const prompts = countOccurrences(controller.stderr, SUDO_PROMPT);148
if (prompts > state.answered) {149
if (state.answered === 0 && password) {150
state.answered = 1;151
controller.write(`${password}\n`);152
} else {153
state.answered = prompts;154
state.authFailure = password ? "SUDO_PASSWORD_INCORRECT" : "SUDO_PASSWORD_REQUIRED";155
controller.end();156
controller.close();157
}158
}159
}160
if (!state.started && controller.stdout.includes(READY_MARKER)) {161
state.started = true;162
for (const frame of frames) controller.write(frame);163
if (options.interact) {164
// Any failure closes stdin; the remote script treats EOF as "roll back".165
state.interaction = Promise.resolve()166
.then(() => options.interact(createInteraction(controller, listeners)))167
.catch((error) => { controller.end(); throw error; });168
state.interaction.catch(() => {});169
} else controller.end();170
}171
for (const listener of listeners) listener();172
}173
});174
const result = await channel.done;175
if (!state.started) {176
const code = classifySudoFailure(result.stderr, result.code, state);177
this._rememberFailure(record, code);178
this.logger?.warn("Privileged remote operation was not started.", { sessionId: record.sessionId, host: record.host, purpose: options.purpose, code });179
throw new SessionError(code, SUDO_MESSAGES[code] || SUDO_MESSAGES.SUDO_FAILED, { remoteMessage: cleanRemoteMessage(result.stderr) });180
}181
if (useSudo) {182
const mode = password || record.privilege.mode === "sudo-password" ? "sudo-password" : "sudo-nopasswd";183
record.privilege = { ...record.privilege, verified: true, mode };184
}185
const stdout = result.stdout.slice(result.stdout.indexOf(READY_MARKER) + READY_MARKER.length);186
const marker = stdout.match(ERROR_MARKER);187
if (marker) {188
throw new SessionError(`${options.errorPrefix || "REMOTE_"}${marker[1]}`, cleanRemoteMessage(result.stderr) || marker[1], { remoteExitCode: result.code });189
}190
const interaction = state.interaction ? await state.interaction : undefined;191
return { code: result.code, stdout, stderr: cleanRemoteMessage(result.stderr), interaction, privileged };192
}194
_rememberFailure(record, code) {195
if (!record.privilege) return;196
if (code === "SUDO_NOT_PERMITTED") record.privilege = { ...record.privilege, mode: "none", verified: true };197
else if (code === "SUDO_UNAVAILABLE") record.privilege = { ...record.privilege, mode: "none", verified: true, sudoInstalled: false };198
else if ((code === "SUDO_PASSWORD_REQUIRED" || code === "SUDO_PASSWORD_INCORRECT") && record.privilege.mode !== "sudo-password") {199
record.privilege = { ...record.privilege, mode: "sudo-password" };200
}201
}202
}204
// Minimal line protocol over a running exec channel: wait for a marker, then answer on stdin.205
function createInteraction(controller, listeners) {206
let consumed = controller.stdout.indexOf(READY_MARKER) + READY_MARKER.length;207
const waiters = new Set();208
const settleAll = (error) => {209
for (const waiter of waiters) { clearTimeout(waiter.timer); waiter.reject(error); }210
waiters.clear();211
};212
const check = () => {213
for (const waiter of [...waiters]) {214
const text = controller.stdout.slice(consumed);215
const match = text.match(waiter.pattern);216
if (!match) continue;217
waiters.delete(waiter);218
clearTimeout(waiter.timer);219
consumed += match.index + match[0].length;220
waiter.resolve({ match, before: text.slice(0, match.index) });221
}222
};223
listeners.add(check);224
controller.done.then(() => { check(); settleAll(new SessionError("PROTOCOL_ERROR", "The remote operation ended unexpectedly.")); }, settleAll);225
return {226
waitFor(pattern, timeoutMs = 60000) {227
return new Promise((resolve, reject) => {228
const waiter = { pattern, resolve, reject };229
waiter.timer = setTimeout(() => { waiters.delete(waiter); reject(new SessionError("REMOTE_COMMAND_TIMEOUT", "The remote operation did not respond in time.")); }, timeoutMs);230
waiter.timer.unref?.();231
waiters.add(waiter);232
check();233
});234
},235
send(line) { controller.write(`${line}\n`); },236
end() { controller.end(); }237
};238
}240
module.exports = { PROBE_SCRIPT, PrivilegedExecutor, READY_MARKER, SUDO_MESSAGES, SUDO_PROMPT, classifySudoFailure, cleanRemoteMessage };SHA-256
34a78f3232e496e9d16481c206658d0090a15651b32d83248f68231e80b00eabCH-J Proprietary Software License 1.14 · Šaltinis pateiktas pagal CH-J Proprietary Software License 1.14. Jo prieinamumas nekeičia licencijos sąlygų ir nesuteikia papildomų teisių.
Archyvas SHA-256
cf4455b698f0f28e456a37b1834976a0194d8486acb7101052d59a838745cca3