CH-J Server ManagerSSH-এর মাধ্যমে সার্ভার ব্যবস্থাপনা
মেনু
প্রকাশিত সোর্স

CH-J Server Manager

নির্দিষ্ট একটি অ্যাপ্লিকেশন রিলিজের ডিরেক্টরি ও ফাইল ব্রাউজ করুন।

সোর্স ZIP ডাউনলোড করুন
CH-J / Server Manager 377 টি ফাইল
core-20261019T111616Z-160a00bfcddc যোগ করা হয়েছে: · বিল্ড হয়েছে:
privilegedExec.js যোগ করা হয়েছে: তারিখ নথিভুক্ত নেই ১৩.৩ KB · 272 লাইন
কোড ফাইল ডাউনলোড করুন
1"use strict";
3const { 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 never
8// stored, logged or placed in argv. Payloads ("frames") are sent only after the script's READY marker.
9const SUDO_PROMPT = "[chj-sudo-auth]";
10const READY_MARKER = "\x1eCHJ_READY\n";
11const ERROR_MARKER = /\x1eCHJ_ERR=([A-Z0-9_]{2,64})\r?\n/;
12const MAX_PASSWORD_LENGTH = 1024;
13// Opt-in, memory-only reuse of a verified sudo password for one live SSH session (sudo itself
14// cannot cache across separate exec channels). Never persisted; dropped on disconnect, on a
15// rejected password and after the idle period.
16const SUDO_CACHE_IDLE_MS = 10 * 60 * 1000;
17const sudoCache = new WeakMap();
18const PROBE_SCRIPT = String.raw`LC_ALL=C; export LC_ALL
19PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin; export PATH
20printf 'user=%s\nuid=%s\ngroups=%s\nclient=%s\n' "$(id -un)" "$(id -u)" "$(id -Gn 2>/dev/null | tr ' ' ',')" "${"$"}{SSH_CLIENT%% *}"
21if command -v sudo >/dev/null 2>&1; then
22 out=$(sudo -n -p '' true 2>&1 </dev/null); code=$?
23 printf 'sudoInstalled=1\nsudoNonInteractive=%s\nsudoMessage=%s\n' "$code" "$(printf '%s' "$out" | head -n 1 | tr -d '\r\n' | cut -c1-300)"
24else
25 printf 'sudoInstalled=0\n'
26fi`;
28function normalizePassword(value) {
29 if (value === undefined || value === null || value === "") return "";
30 const password = String(value);
31 if (password.length > MAX_PASSWORD_LENGTH || /[\0\r\n]/.test(password)) throw new SessionError("SUDO_PASSWORD_INVALID", "Invalid sudo password.");
32 return password;
35function countOccurrences(text, token) {
36 let count = 0;
37 for (let index = text.indexOf(token); index >= 0; index = text.indexOf(token, index + token.length)) count += 1;
38 return count;
41function cleanRemoteMessage(text, limit = 600) {
42 return String(text || "")
43 .split(SUDO_PROMPT).join("")
44 .replace(/[\x00-\x08\x0b-\x1f\x7f]/g, " ")
45 .split(/\r?\n/).map((line) => line.trim()).filter(Boolean)
46 .slice(-6).join(" | ")
47 .slice(0, limit);
50function classifySudoFailure(stderr, code, state) {
51 const text = String(stderr || "");
52 if (state.authFailure) return state.authFailure;
53 if (/incorrect password attempt|Sorry, try again/i.test(text)) return "SUDO_PASSWORD_INCORRECT";
54 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";
55 if (/must have a tty|a terminal is required|no tty present/i.test(text)) return "SUDO_TTY_REQUIRED";
56 if (/a password is required|no password was provided/i.test(text)) return "SUDO_PASSWORD_REQUIRED";
57 if (/account.*(locked|expired)|authentication (failure|token)|maximum number of tries/i.test(text)) return "SUDO_AUTH_FAILED";
58 if (code === 127 || /sudo: (command )?not found/i.test(text)) return "SUDO_UNAVAILABLE";
59 return "SUDO_FAILED";
62const SUDO_MESSAGES = Object.freeze({
63 SUDO_PASSWORD_REQUIRED: "This operation requires the sudo password of the SSH account.",
64 SUDO_PASSWORD_INCORRECT: "The sudo password was rejected.",
65 SUDO_NOT_PERMITTED: "The SSH account is not permitted to run this operation with sudo.",
66 SUDO_TTY_REQUIRED: "The sudo policy requires a terminal (requiretty); CH-J does not bypass it.",
67 SUDO_AUTH_FAILED: "sudo authentication failed or the account is locked.",
68 SUDO_UNAVAILABLE: "sudo is not installed and the SSH account is not root.",
69 SUDO_FAILED: "sudo did not start the operation."
70});
72function parseKeyValues(text) {
73 const values = {};
74 for (const line of String(text || "").split(/\r?\n/)) {
75 const index = line.indexOf("=");
76 if (index > 0) values[line.slice(0, index)] = line.slice(index + 1);
77 }
78 return values;
81function publicPrivilege(privilege) {
82 return {
83 mode: privilege.mode,
84 verified: privilege.verified === true,
85 username: privilege.username,
86 sudoInstalled: privilege.sudoInstalled === true,
87 groups: [...(privilege.groups || [])],
88 clientAddress: privilege.clientAddress || "",
89 message: privilege.message || "",
90 checkedAt: privilege.checkedAt
91 };
94class PrivilegedExecutor {
95 constructor(options) {
96 this.sessionManager = options.sessionManager;
97 this.logger = options.logger;
98 }
100 // Detects what the SSH account can do without prompting: root, sudo without password,
101 // sudo with password (not yet verified), or no sudo at all. Cached per live session.
102 async probe(sessionId, { refresh = false } = {}) {
103 const record = this.sessionManager._requireConnected(sessionId);
104 if (record.privilege && !refresh) return publicPrivilege(record.privilege);
105 const result = await this.sessionManager._execStream(record, `sh -c ${shellQuote(PROBE_SCRIPT)}`, { timeoutMs: 15000 }).done;
106 const values = parseKeyValues(result.stdout);
107 const privilege = {
108 username: values.user || record.username,
109 sudoInstalled: values.sudoInstalled === "1",
110 groups: String(values.groups || "").split(",").filter(Boolean).slice(0, 256),
111 clientAddress: String(values.client || "").trim().slice(0, 64),
112 message: cleanRemoteMessage(values.sudoMessage || "", 300),
113 checkedAt: new Date().toISOString(),
114 mode: "unknown",
115 verified: false
116 };
117 if (values.uid === "0") Object.assign(privilege, { mode: "root", verified: true });
118 else if (!privilege.sudoInstalled) Object.assign(privilege, { mode: "none", verified: true });
119 else if (values.sudoNonInteractive === "0") Object.assign(privilege, { mode: "sudo-nopasswd", verified: true });
120 else if (/password is required/i.test(values.sudoMessage || "")) privilege.mode = "sudo-password";
121 else if (/not allowed|may not run sudo|not in the sudoers/i.test(values.sudoMessage || "")) Object.assign(privilege, { mode: "none", verified: true });
122 record.privilege = privilege;
123 return publicPrivilege(privilege);
124 }
126 // Runs a fixed script. With privileged=true it runs as root (root session) or via sudo -S.
127 // Frames are { tag, data } pairs written as base64 lines after READY. interact(channel) may drive
128 // a confirmation protocol; otherwise stdin is closed after the frames.
129 async run(sessionId, options) {
130 const record = this.sessionManager._requireConnected(sessionId);
131 const privileged = options.privileged !== false;
132 let password = normalizePassword(options.sudoPassword);
133 const cached = !password && options.useCachedPassword === true ? this._cachedPassword(record) : "";
134 if (cached) password = cached;
135 const privilege = privileged ? await this.probe(sessionId) : null;
136 if (privileged && privilege.mode === "none" && privilege.verified) {
137 throw new SessionError(privilege.sudoInstalled ? "SUDO_NOT_PERMITTED" : "SUDO_UNAVAILABLE",
138 privilege.sudoInstalled ? SUDO_MESSAGES.SUDO_NOT_PERMITTED : SUDO_MESSAGES.SUDO_UNAVAILABLE);
139 }
140 const useSudo = privileged && privilege.mode !== "root";
141 const body = `sh -c ${shellQuote(options.script)} chj ${(options.args || []).map((value) => shellQuote(value)).join(" ")}`;
142 const command = useSudo ? `LC_ALL=C sudo -S -p ${shellQuote(SUDO_PROMPT)} -- ${body}` : `LC_ALL=C ${body}`;
143 const frames = (options.frames || []).map(({ tag, data }) => {
144 if (!/^[A-Z]{2,16}$/.test(tag)) throw new SessionError("PROTOCOL_ERROR", "Invalid frame tag.");
145 return `CHJ${tag}:${Buffer.from(data ?? "", typeof data === "string" ? "utf8" : undefined).toString("base64")}\n`;
146 });
147 const state = { answered: 0, started: false, authFailure: null, interaction: null };
148 const listeners = new Set();
149 const channel = this.sessionManager._execStream(record, command, {
150 timeoutMs: options.timeoutMs || 60000,
151 outputLimit: options.outputLimit || 4 * 1024 * 1024,
152 onUpdate: (controller) => {
153 if (useSudo && !state.started) {
154 const prompts = countOccurrences(controller.stderr, SUDO_PROMPT);
155 if (prompts > state.answered) {
156 if (state.answered === 0 && password) {
157 state.answered = 1;
158 controller.write(`${password}\n`);
159 } else {
160 state.answered = prompts;
161 state.authFailure = password ? "SUDO_PASSWORD_INCORRECT" : "SUDO_PASSWORD_REQUIRED";
162 controller.end();
163 controller.close();
164 }
165 }
166 }
167 if (!state.started && controller.stdout.includes(READY_MARKER)) {
168 state.started = true;
169 for (const frame of frames) controller.write(frame);
170 if (options.interact) {
171 // Any failure closes stdin; the remote script treats EOF as "roll back".
172 state.interaction = Promise.resolve()
173 .then(() => options.interact(createInteraction(controller, listeners)))
174 .catch((error) => { controller.end(); throw error; });
175 state.interaction.catch(() => {});
176 } else controller.end();
177 }
178 for (const listener of listeners) listener();
179 }
180 });
181 const result = await channel.done;
182 if (!state.started) {
183 const code = classifySudoFailure(result.stderr, result.code, state);
184 this._rememberFailure(record, code);
185 if (code === "SUDO_PASSWORD_INCORRECT" || code === "SUDO_PASSWORD_REQUIRED") sudoCache.delete(record);
186 this.logger?.warn("Privileged remote operation was not started.", { sessionId: record.sessionId, host: record.host, purpose: options.purpose, code });
187 throw new SessionError(code, SUDO_MESSAGES[code] || SUDO_MESSAGES.SUDO_FAILED, { remoteMessage: cleanRemoteMessage(result.stderr) });
188 }
189 if (useSudo) {
190 const mode = (password && state.answered) || record.privilege.mode === "sudo-password" ? "sudo-password" : "sudo-nopasswd";
191 record.privilege = { ...record.privilege, verified: true, mode };
192 if (password && state.answered && (options.rememberPassword === true || cached)) {
193 sudoCache.set(record, { password, expiresAt: Date.now() + SUDO_CACHE_IDLE_MS });
194 }
195 }
196 const stdout = result.stdout.slice(result.stdout.indexOf(READY_MARKER) + READY_MARKER.length);
197 const marker = stdout.match(ERROR_MARKER);
198 if (marker) {
199 throw new SessionError(`${options.errorPrefix || "REMOTE_"}${marker[1]}`, cleanRemoteMessage(result.stderr) || marker[1], { remoteExitCode: result.code });
200 }
201 const interaction = state.interaction ? await state.interaction : undefined;
202 return { code: result.code, stdout, stderr: cleanRemoteMessage(result.stderr), interaction, privileged };
203 }
205 hasCachedPassword(sessionId) {
206 const record = this.sessionManager.sessions?.get(sessionId);
207 return Boolean(record && this._cachedPassword(record));
208 }
210 forgetPassword(sessionId) {
211 const record = this.sessionManager.sessions?.get(sessionId);
212 if (record) sudoCache.delete(record);
213 }
215 _cachedPassword(record) {
216 const entry = sudoCache.get(record);
217 if (!entry) return "";
218 if (record.finalized || record.state !== "connected" || entry.expiresAt <= Date.now()) {
219 sudoCache.delete(record);
220 return "";
221 }
222 entry.expiresAt = Date.now() + SUDO_CACHE_IDLE_MS;
223 return entry.password;
224 }
226 _rememberFailure(record, code) {
227 if (!record.privilege) return;
228 if (code === "SUDO_NOT_PERMITTED") record.privilege = { ...record.privilege, mode: "none", verified: true };
229 else if (code === "SUDO_UNAVAILABLE") record.privilege = { ...record.privilege, mode: "none", verified: true, sudoInstalled: false };
230 else if ((code === "SUDO_PASSWORD_REQUIRED" || code === "SUDO_PASSWORD_INCORRECT") && record.privilege.mode !== "sudo-password") {
231 record.privilege = { ...record.privilege, mode: "sudo-password" };
232 }
233 }
236// Minimal line protocol over a running exec channel: wait for a marker, then answer on stdin.
237function createInteraction(controller, listeners) {
238 let consumed = controller.stdout.indexOf(READY_MARKER) + READY_MARKER.length;
239 const waiters = new Set();
240 const settleAll = (error) => {
241 for (const waiter of waiters) { clearTimeout(waiter.timer); waiter.reject(error); }
242 waiters.clear();
243 };
244 const check = () => {
245 for (const waiter of [...waiters]) {
246 const text = controller.stdout.slice(consumed);
247 const match = text.match(waiter.pattern);
248 if (!match) continue;
249 waiters.delete(waiter);
250 clearTimeout(waiter.timer);
251 consumed += match.index + match[0].length;
252 waiter.resolve({ match, before: text.slice(0, match.index) });
253 }
254 };
255 listeners.add(check);
256 controller.done.then(() => { check(); settleAll(new SessionError("PROTOCOL_ERROR", "The remote operation ended unexpectedly.")); }, settleAll);
257 return {
258 waitFor(pattern, timeoutMs = 60000) {
259 return new Promise((resolve, reject) => {
260 const waiter = { pattern, resolve, reject };
261 waiter.timer = setTimeout(() => { waiters.delete(waiter); reject(new SessionError("REMOTE_COMMAND_TIMEOUT", "The remote operation did not respond in time.")); }, timeoutMs);
262 waiter.timer.unref?.();
263 waiters.add(waiter);
264 check();
265 });
266 },
267 send(line) { controller.write(`${line}\n`); },
268 end() { controller.end(); }
269 };
272module.exports = { PROBE_SCRIPT, PrivilegedExecutor, READY_MARKER, SUDO_CACHE_IDLE_MS, SUDO_MESSAGES, SUDO_PROMPT, classifySudoFailure, cleanRemoteMessage };
SHA-2567f767f711adc6b0b13900a70a5e642b7b804370608a4831872ef855aa4b3009f

CH-J Proprietary Software License 1.14 · সোর্স CH-J Proprietary Software License 1.14-এর অধীনে দেওয়া হয়। এর প্রাপ্যতা লাইসেন্সের শর্ত বদলায় না এবং অতিরিক্ত কোনো অধিকারও দেয় না।

আর্কাইভ SHA-256a9605c0870a223b6a8542bb0705edd51d8b04f3069582653417a8397fabcc09a
ফাইল