شائع شدہ سورس
سورس ZIP ڈاؤن لوڈ کریں
CH-J Server Manager
ایپلیکیشن کی کسی مخصوص ریلیز کی ڈائریکٹریز اور فائلیں دیکھیں۔
CH-J / Server Manager
368 فائلیں
core-20261017T201717Z-057348c4a70f
شامل کیا گیا: تاریخ درج نہیں · بلڈ کیا گیا:
1
"use strict";3
const net = require("node:net");5
// Per-user SSH policy stored in one CH-J managed sshd drop-in. The "# chj-policy" lines are the6
// source of truth; the directives are regenerated from them, so any manual edit is detected.7
// permitRootLogin applies to the root account only (Linux). It is written into the policy comment only when8
// it differs from "inherit", so managed files created before this option stay byte-identical.9
const POLICY_DEFAULTS = Object.freeze({ access: "allow", password: "inherit", pubkey: "inherit", ipv4: "allow", ipv6: "allow", permitRootLogin: "inherit" });10
const POLICY_VALUES = Object.freeze({11
access: ["allow", "deny"],12
password: ["inherit", "yes", "no"],13
pubkey: ["inherit", "yes", "no"],14
ipv4: ["allow", "deny"],15
ipv6: ["allow", "deny"],16
permitRootLogin: ["inherit", "no", "prohibit-password", "forced-commands-only", "yes"]17
});18
const BASE_POLICY_KEYS = ["access", "password", "pubkey", "ipv4", "ipv6"];20
function policyComment(username, policy) {21
const keys = policy.permitRootLogin && policy.permitRootLogin !== "inherit" ? [...BASE_POLICY_KEYS, "permitRootLogin"] : BASE_POLICY_KEYS;22
return `# chj-policy ${username} ${keys.map((key) => `${key}=${policy[key]}`).join(" ")}`;23
}24
const MANAGED_PATH = "/etc/ssh/sshd_config.d/01-chj-users.conf";25
const HEADER = [26
"# CH-J Server Manager - chj.users managed SSH access policy.",27
"# Generated file. CH-J refuses to overwrite it after manual edits; keep custom rules in other files."28
];29
const USERNAME_PATTERN = /^[a-z_][a-z0-9_-]{0,31}$/;30
const IPV4_ALL = "0.0.0.0/0";31
const IPV6_ALL = "::/0";32
// Windows OpenSSH rewrites "/" to "\\" in AllowUsers/DenyUsers (domain/user syntax), so CIDR cannot be used there;33
// address-family wildcards are used instead: dotted IPv4 strings and colon-separated IPv6 strings.34
const WINDOWS_IPV4_PATTERN = "*.*.*.*";35
const WINDOWS_IPV6_PATTERN = "*:*";36
const MANAGED_HOST_PATTERNS = [IPV4_ALL, IPV6_ALL, WINDOWS_IPV4_PATTERN, WINDOWS_IPV6_PATTERN];37
const MULTI_VALUE_KEYS = new Set(["denyusers", "allowusers", "denygroups", "allowgroups", "listenaddress"]);39
class PolicyError extends Error {40
constructor(code, message) {41
super(message);42
this.code = code;43
}44
}46
function normalizePolicy(input = {}, username = null) {47
const policy = {};48
for (const [key, allowed] of Object.entries(POLICY_VALUES)) {49
const value = input[key] === undefined || input[key] === null ? POLICY_DEFAULTS[key] : String(input[key]);50
if (!allowed.includes(value)) throw new PolicyError("USERS_SSH_POLICY_INVALID", `Invalid SSH policy value for ${key}.`);51
policy[key] = value;52
}53
if (username !== null && username !== "root" && policy.permitRootLogin !== "inherit") throw new PolicyError("USERS_SSH_POLICY_INVALID", "PermitRootLogin can only be set for the root account.");54
return policy;55
}57
function isDefaultPolicy(policy) {58
return Object.keys(POLICY_DEFAULTS).every((key) => policy[key] === POLICY_DEFAULTS[key]);59
}61
function generateManagedConfig(policies) {62
const entries = [...policies.entries()].filter(([, policy]) => !isDefaultPolicy(policy)).sort(([left], [right]) => left.localeCompare(right));63
if (!entries.length) return "";64
const lines = [...HEADER];65
for (const [username, policy] of entries) {66
lines.push(policyComment(username, policy));67
}68
for (const [username, policy] of entries) {69
if (policy.access === "deny") lines.push(`DenyUsers ${username}`);70
if (policy.ipv4 === "deny") lines.push(`DenyUsers ${username}@${IPV4_ALL}`);71
if (policy.ipv6 === "deny") lines.push(`DenyUsers ${username}@${IPV6_ALL}`);72
}73
for (const [username, policy] of entries) {74
if (policy.password === "inherit" && policy.pubkey === "inherit" && policy.permitRootLogin === "inherit") continue;75
lines.push(`Match User ${username}`);76
if (policy.permitRootLogin !== "inherit") lines.push(`\tPermitRootLogin ${policy.permitRootLogin}`);77
if (policy.password !== "inherit") {78
lines.push(`\tPasswordAuthentication ${policy.password}`);79
if (policy.password === "no") lines.push("\tKbdInteractiveAuthentication no");80
}81
if (policy.pubkey !== "inherit") lines.push(`\tPubkeyAuthentication ${policy.pubkey}`);82
}83
// Ends any Match context for files that sshd reads after this one.84
lines.push("Match all");85
return `${lines.join("\n")}\n`;86
}88
function parseManagedConfig(text) {89
const source = String(text || "");90
const policies = new Map();91
for (const line of source.split(/\r?\n/)) {92
const match = line.match(/^# chj-policy ([a-z_][a-z0-9_-]{0,31}) (.+)$/);93
if (!match) continue;94
const values = Object.fromEntries(match[2].split(" ").map((pair) => pair.split("=")));95
try { policies.set(match[1], normalizePolicy(values, match[1])); }96
catch { return { policies: new Map(), consistent: false }; }97
}98
const consistent = source === "" ? true : generateManagedConfig(policies) === source;99
return { policies, consistent };100
}102
function parseEffective(text) {103
const result = { values: {}, errors: [] };104
for (const raw of String(text || "").split(/\r?\n/)) {105
const line = raw.trim();106
if (!line) continue;107
const match = line.match(/^([a-z0-9]+) (.*)$/);108
if (!match) { result.errors.push(line.slice(0, 300)); continue; }109
const [, key, value] = match;110
// sshd -T prints one entry per line; an entry may contain spaces (AllowGroups "openssh users").111
if (MULTI_VALUE_KEYS.has(key)) (result.values[key] ||= []).push(value.trim());112
else if (!(key in result.values)) result.values[key] = value;113
}114
return result;115
}117
function wildcardMatch(pattern, value) {118
const expression = String(pattern).replace(/[.+^${}()|[\]\\]/g, "\\$&").replace(/\*/g, ".*").replace(/\?/g, ".");119
return new RegExp(`^${expression}$`).test(String(value));120
}122
// Returns true/false, or null when the host part needs DNS names CH-J cannot evaluate.123
function hostMatches(pattern, address) {124
if (!pattern) return true;125
const family = net.isIP(address);126
if (pattern.includes("/")) {127
const [network, bitsText] = pattern.split("/");128
const networkFamily = net.isIP(network);129
const bits = Number(bitsText);130
if (!networkFamily || !Number.isInteger(bits)) return null;131
if (networkFamily !== family) return false;132
const list = new net.BlockList();133
try { list.addSubnet(network, bits, networkFamily === 6 ? "ipv6" : "ipv4"); }134
catch { return null; }135
return list.check(address, family === 6 ? "ipv6" : "ipv4");136
}137
if (net.isIP(pattern)) return net.isIP(pattern) === family && pattern.toLowerCase() === address.toLowerCase();138
if (/^[0-9a-fA-F:.*?]+$/.test(pattern)) return wildcardMatch(pattern.toLowerCase(), address.toLowerCase());139
return null;140
}142
function userEntryMatches(entry, username, address) {143
if (entry.startsWith("!")) return null;144
const at = entry.indexOf("@");145
const userPattern = at >= 0 ? entry.slice(0, at) : entry;146
if (!wildcardMatch(userPattern, username)) return false;147
return at >= 0 ? hostMatches(entry.slice(at + 1), address) : true;148
}150
function methodSatisfiable(authenticationMethods, method) {151
const value = String(authenticationMethods || "any").trim();152
if (!value || value === "any") return true;153
const accepted = method === "password" ? /^(password|keyboard-interactive(:[a-z]+)?)$/ : /^publickey$/;154
return value.split(/\s+/).some((list) => list.split(",").every((item) => accepted.test(item)));155
}157
// Evaluates sshd -T output the way sshd's allowed_user() does for user and address rules.158
// keyboardInteractive=false for Windows OpenSSH, which does not support KbdInteractiveAuthentication.159
function evaluateAccess(effective, { username, groups = [], address, method = null, keyboardInteractive = true }) {160
const values = effective.values || {};161
const reasons = [];162
let allowed = true;163
const consider = (result, reason) => {164
if (result === true) { allowed = false; reasons.push(reason); }165
else if (result === null && allowed !== false) { allowed = null; reasons.push({ ...reason, uncertain: true }); }166
};167
for (const entry of values.denyusers || []) {168
const managed = entry === username || MANAGED_HOST_PATTERNS.some((pattern) => entry === `${username}@${pattern}`);169
consider(userEntryMatches(entry, username, address), { rule: "DenyUsers", entry, managed });170
}171
if ((values.allowusers || []).length) {172
const results = values.allowusers.map((entry) => userEntryMatches(entry, username, address));173
if (!results.includes(true)) consider(results.includes(null) ? null : true, { rule: "AllowUsers", entry: "not listed", managed: false });174
}175
for (const pattern of values.denygroups || []) {176
if (groups.some((group) => wildcardMatch(pattern, group))) consider(true, { rule: "DenyGroups", entry: pattern, managed: false });177
}178
if ((values.allowgroups || []).length && !groups.some((group) => values.allowgroups.some((pattern) => wildcardMatch(pattern, group)))) {179
consider(true, { rule: "AllowGroups", entry: "not a member", managed: false });180
}181
const rootLogin = values.permitrootlogin === "without-password" ? "prohibit-password" : (values.permitrootlogin || "yes");182
const password = values.passwordauthentication === "yes" || (keyboardInteractive && (values.kbdinteractiveauthentication === "yes" || values.challengeresponseauthentication === "yes"));183
const pubkey = values.pubkeyauthentication !== "no";184
const methods = {185
password: password && (username !== "root" || rootLogin === "yes") && methodSatisfiable(values.authenticationmethods, "password"),186
// forced-commands-only allows keys only with command="..." options; reported as key login with restriction.187
publickey: pubkey && (username !== "root" || ["yes", "prohibit-password", "forced-commands-only"].includes(rootLogin)) && methodSatisfiable(values.authenticationmethods, "publickey")188
};189
if (username === "root" && rootLogin === "no") consider(true, { rule: "PermitRootLogin", entry: "no", managed: false });190
return {191
allowed,192
reasons,193
methods,194
methodAllowed: method ? (allowed === true ? methods[method] : allowed === false ? false : null) : null195
};196
}198
function listenerFamilies(effective) {199
const values = effective.values || {};200
const family = values.addressfamily || "any";201
const addresses = values.listenaddress || [];202
const has = (version) => addresses.some((entry) => {203
const host = entry.startsWith("[") ? entry.slice(1, entry.indexOf("]")) : entry.replace(/:\d+$/, "");204
return net.isIP(host) === version;205
});206
return {207
ipv4: family === "inet6" ? false : (addresses.length ? has(4) : true),208
ipv6: family === "inet" ? false : (addresses.length ? has(6) : true)209
};210
}212
// Checks that the regenerated drop-in really produced the requested effective settings.213
function verifyPolicyEffect(policy, username, effective, familyLabel, { keyboardInteractive = true, ipv4Pattern = IPV4_ALL, ipv6Pattern = IPV6_ALL } = {}) {214
const values = effective.values || {};215
const problems = [];216
const deny = values.denyusers || [];217
if (policy.access === "deny" && !deny.includes(username)) problems.push(`${familyLabel}: DenyUsers ${username}`);218
if (policy.ipv4 === "deny" && !deny.includes(`${username}@${ipv4Pattern}`)) problems.push(`${familyLabel}: DenyUsers ${username}@${ipv4Pattern}`);219
if (policy.ipv6 === "deny" && !deny.includes(`${username}@${ipv6Pattern}`)) problems.push(`${familyLabel}: DenyUsers ${username}@${ipv6Pattern}`);220
if (policy.password === "no") {221
if (values.passwordauthentication !== "no") problems.push(`${familyLabel}: PasswordAuthentication is ${values.passwordauthentication || "unknown"}`);222
const keyboard = values.kbdinteractiveauthentication ?? values.challengeresponseauthentication;223
if (keyboardInteractive && keyboard === "yes") problems.push(`${familyLabel}: KbdInteractiveAuthentication is yes`);224
}225
if (policy.password === "yes" && values.passwordauthentication !== "yes") problems.push(`${familyLabel}: PasswordAuthentication is ${values.passwordauthentication || "unknown"}`);226
if (policy.pubkey !== "inherit" && (values.pubkeyauthentication || "unknown") !== policy.pubkey) problems.push(`${familyLabel}: PubkeyAuthentication is ${values.pubkeyauthentication || "unknown"}`);227
if (policy.permitRootLogin && policy.permitRootLogin !== "inherit") {228
// sshd -T prints the historic alias "without-password" for prohibit-password.229
const effective = values.permitrootlogin === "without-password" ? "prohibit-password" : values.permitrootlogin;230
if (effective !== policy.permitRootLogin) problems.push(`${familyLabel}: PermitRootLogin is ${values.permitrootlogin || "unknown"}`);231
}232
return problems;233
}235
module.exports = {236
IPV4_ALL,237
IPV6_ALL,238
MANAGED_PATH,239
WINDOWS_IPV4_PATTERN,240
WINDOWS_IPV6_PATTERN,241
POLICY_DEFAULTS,242
POLICY_VALUES,243
PolicyError,244
USERNAME_PATTERN,245
evaluateAccess,246
generateManagedConfig,247
hostMatches,248
isDefaultPolicy,249
listenerFamilies,250
normalizePolicy,251
parseEffective,252
policyComment,253
parseManagedConfig,254
verifyPolicyEffect,255
wildcardMatch256
};SHA-256
f3196545856e2aec95693c2d34dc6233e386ae32a24fc1d74364a34b9e726ba9CH-J Proprietary Software License 1.14 · سورس CH-J Proprietary Software License 1.14 کے تحت فراہم کیا جاتا ہے۔ اس کی دستیابی لائسنس کی شرائط نہیں بدلتی اور نہ ہی اضافی حقوق دیتی ہے۔
آرکائیو کا SHA-256
cf4455b698f0f28e456a37b1834976a0194d8486acb7101052d59a838745cca3