Publicerad källa
Ladda ner källa ZIP
CH-J Server Manager
Bläddra i kataloger och filer för en specifik programutgåva.
CH-J / Server Manager
368 filer
core-20261017T201717Z-057348c4a70f
Tillagd: Datum inte noterat · Byggd:
1
"use strict";3
const { WINDOWS_IPV4_PATTERN, WINDOWS_IPV6_PATTERN, isDefaultPolicy, normalizePolicy, policyComment } = require("./sshPolicy");5
// Windows OpenSSH has no sshd_config.d by default, so CH-J keeps two marked sections inside6
// %ProgramData%\ssh\sshd_config: global DenyUsers lines placed before the first existing Match7
// block (never after one), and per-user Match blocks appended at the end, so existing Match8
// blocks such as "Match Group administrators" keep their priority. Only directives documented9
// for Windows OpenSSH are written (no KbdInteractiveAuthentication). Address families use the10
// patterns user@*.*.*.* and user@*:* because Windows OpenSSH turns "/" in DenyUsers into "\\".11
const GLOBAL_BEGIN = "# BEGIN CH-J chj.users global (managed by CH-J Server Manager, do not edit)";12
const GLOBAL_END = "# END CH-J chj.users global";13
const MATCH_BEGIN = "# BEGIN CH-J chj.users match (managed by CH-J Server Manager, do not edit)";14
const MATCH_END = "# END CH-J chj.users match";15
const SSH_NAME_PATTERN = /^[a-z0-9_][a-z0-9._-]{0,19}$/;16
const CONFIG_PATH = "C:\\ProgramData\\ssh\\sshd_config";18
class WindowsPolicyError extends Error {19
constructor(code, message) {20
super(message);21
this.code = code;22
}23
}25
function sshName(name) {26
const value = String(name || "").toLowerCase();27
return SSH_NAME_PATTERN.test(value) ? value : null;28
}30
function splitLines(text) {31
const eol = /\r\n/.test(text) ? "\r\n" : "\n";32
const lines = text.split(/\r?\n/);33
const trailing = lines.length && lines[lines.length - 1] === "";34
if (trailing) lines.pop();35
return { eol, lines };36
}38
// Locates managed sections and the structure CH-J depends on. Never throws on foreign content.39
function analyzeConfig(text) {40
const { eol, lines } = splitLines(String(text || ""));41
const marks = { globalBegin: [], globalEnd: [], matchBegin: [], matchEnd: [] };42
lines.forEach((line, index) => {43
if (line === GLOBAL_BEGIN) marks.globalBegin.push(index);44
else if (line === GLOBAL_END) marks.globalEnd.push(index);45
else if (line === MATCH_BEGIN) marks.matchBegin.push(index);46
else if (line === MATCH_END) marks.matchEnd.push(index);47
});48
const problems = [];49
for (const [begin, end] of [["globalBegin", "globalEnd"], ["matchBegin", "matchEnd"]]) {50
if (marks[begin].length > 1 || marks[end].length > 1 || marks[begin].length !== marks[end].length || (marks[begin][0] > marks[end][0])) problems.push("managed markers are damaged");51
}52
const inManaged = (index) => (marks.globalBegin.length && index >= marks.globalBegin[0] && index <= marks.globalEnd[0])53
|| (marks.matchBegin.length && index >= marks.matchBegin[0] && index <= marks.matchEnd[0]);54
let firstMatch = -1;55
let include = false;56
lines.forEach((line, index) => {57
if (inManaged(index)) return;58
const keyword = line.trim().split(/\s+/)[0]?.toLowerCase();59
if (keyword === "match" && firstMatch < 0) firstMatch = index;60
if (keyword === "include") include = true;61
});62
if (marks.globalBegin.length && firstMatch >= 0 && marks.globalBegin[0] > firstMatch) problems.push("the CH-J global section is located after a Match block");63
if (include) problems.push("Include directives are not supported by the CH-J Windows editor");64
return { eol, lines, marks, firstMatch, include, problems, supported: problems.length === 0 };65
}67
function section(lines, begin, end) {68
return begin.length ? lines.slice(begin[0], end[0] + 1) : [];69
}71
function renderSections(policies) {72
const entries = [...policies.entries()].filter(([, policy]) => !isDefaultPolicy(policy)).sort(([left], [right]) => left.localeCompare(right));73
if (!entries.length) return { global: [], match: [] };74
const global = [GLOBAL_BEGIN];75
for (const [name, policy] of entries) global.push(policyComment(name, policy));76
for (const [name, policy] of entries) {77
if (policy.access === "deny") global.push(`DenyUsers ${name}`);78
if (policy.ipv4 === "deny") global.push(`DenyUsers ${name}@${WINDOWS_IPV4_PATTERN}`);79
if (policy.ipv6 === "deny") global.push(`DenyUsers ${name}@${WINDOWS_IPV6_PATTERN}`);80
}81
global.push(GLOBAL_END);82
const matchEntries = entries.filter(([, policy]) => policy.password !== "inherit" || policy.pubkey !== "inherit");83
const match = [];84
if (matchEntries.length) {85
match.push(MATCH_BEGIN);86
for (const [name, policy] of matchEntries) {87
match.push(`Match User ${name}`);88
if (policy.password !== "inherit") match.push(`\tPasswordAuthentication ${policy.password}`);89
if (policy.pubkey !== "inherit") match.push(`\tPubkeyAuthentication ${policy.pubkey}`);90
}91
match.push(MATCH_END);92
}93
return { global, match };94
}96
function parseManagedPolicies(text) {97
const analysis = analyzeConfig(text);98
const policies = new Map();99
for (const line of section(analysis.lines, analysis.marks.globalBegin, analysis.marks.globalEnd)) {100
const match = line.match(/^# chj-policy ([a-z0-9_][a-z0-9._-]{0,19}) (.+)$/);101
if (!match) continue;102
try { policies.set(match[1], normalizePolicy(Object.fromEntries(match[2].split(" ").map((pair) => pair.split("="))))); }103
catch { return { analysis, policies: new Map(), consistent: false }; }104
}105
const expected = renderSections(policies);106
const consistent = analysis.problems.length === 0107
&& JSON.stringify(section(analysis.lines, analysis.marks.globalBegin, analysis.marks.globalEnd)) === JSON.stringify(expected.global)108
&& JSON.stringify(section(analysis.lines, analysis.marks.matchBegin, analysis.marks.matchEnd)) === JSON.stringify(expected.match);109
return { analysis, policies, consistent };110
}112
// Rebuilds sshd_config with the managed sections replaced; every foreign line keeps its text and order.113
function renderConfig(text, policies) {114
const { analysis, consistent } = parseManagedPolicies(text);115
if (!analysis.supported) throw new WindowsPolicyError("USERS_SSH_LAYOUT_UNSUPPORTED", `sshd_config cannot be edited safely: ${analysis.problems.join("; ")}.`);116
if (!consistent) throw new WindowsPolicyError("USERS_SSH_MANAGED_FILE_MODIFIED", "The CH-J sections in sshd_config were edited manually; CH-J will not overwrite them.");117
const { lines, marks, eol } = analysis;118
const managed = new Set();119
for (const [begin, end] of [[marks.globalBegin, marks.globalEnd], [marks.matchBegin, marks.matchEnd]]) {120
if (begin.length) for (let index = begin[0]; index <= end[0]; index += 1) managed.add(index);121
}122
const foreign = [];123
let firstMatch = -1;124
lines.forEach((line, index) => {125
if (managed.has(index)) return;126
if (firstMatch < 0 && line.trim().split(/\s+/)[0]?.toLowerCase() === "match") firstMatch = foreign.length;127
foreign.push(line);128
});129
const { global, match } = renderSections(policies);130
let output = firstMatch < 0 ? [...foreign, ...global] : [...foreign.slice(0, firstMatch), ...global, ...foreign.slice(firstMatch)];131
output = [...output, ...match];132
return `${output.join(eol)}${eol}`;133
}135
module.exports = {136
CONFIG_PATH,137
GLOBAL_BEGIN,138
MATCH_BEGIN,139
SSH_NAME_PATTERN,140
WindowsPolicyError,141
analyzeConfig,142
parseManagedPolicies,143
renderConfig,144
sshName145
};SHA-256
b3b8de8cd49fbecd267a8de8a6794d1284eaa825b3779596d7e2e2cc029b7706CH-J Proprietary Software License 1.14 · Källan finns under CH-J Proprietary Software License 1.14. Dess tillgänglighet ändrar inte licensvillkoren eller ger ytterligare rättigheter.
Arkiv SHA-256
cf4455b698f0f28e456a37b1834976a0194d8486acb7101052d59a838745cca3