169 lines
9.6 KiB
JavaScript
169 lines
9.6 KiB
JavaScript
"use strict";
|
|
(() => {
|
|
// These are reviewable shell instructions only. The panel never executes them.
|
|
const views = new WeakMap();
|
|
const quote = value => `'${String(value).replaceAll("'", "'\"'\"'")}'`;
|
|
function absolutePath(value, directory) {
|
|
const raw = String(value || "");
|
|
if (/[\x00-\x1f\x7f]/.test(raw)) return null;
|
|
const path = raw.trim();
|
|
if (!path.startsWith("/") || path.length > 4096) return null;
|
|
if (path.split("/").some(part => part === "." || part === "..")) return null;
|
|
if (!directory && path.endsWith("/")) return null;
|
|
const normalized = path.replace(/\/{2,}/g, "/").replace(/\/$/, "");
|
|
return normalized && normalized !== "/" ? normalized : null;
|
|
}
|
|
function commands({ kind, paths = [], user = "rustproxy" } = {}) {
|
|
if (!["certificate", "web"].includes(kind) || !/^[A-Za-z_][A-Za-z0-9_.-]*\$?$/.test(user) || /[\x00-\x1f\x7f]/.test(user) || user.length > 64) return "";
|
|
const selected = paths.map(path => absolutePath(path, kind === "web"));
|
|
if (selected.length !== (kind === "web" ? 1 : 2) || selected.some(path => !path)) return "";
|
|
const lines = ["sudo sh <<'RUSTPROXY_ACL'", "set -eu", `rp_user=${quote(user)}`];
|
|
if (kind === "web") lines.push(`rp_root_path=${quote(selected[0])}`);
|
|
else lines.push(`rp_cert_path=${quote(selected[0])}`, `rp_key_path=${quote(selected[1])}`);
|
|
lines.push(
|
|
"command -v setfacl >/dev/null || { printf '%s\\n' 'Install the acl package (setfacl) first.' >&2; exit 1; }",
|
|
'id "$rp_user" >/dev/null',
|
|
"",
|
|
"rp_traverse() {",
|
|
' [ "$1" = / ] && return 0',
|
|
' rp_traverse "$(dirname -- "$1")" || return 1',
|
|
' rp_real_dir=$(readlink -e -- "$1") || return 1',
|
|
' if [ "$rp_real_dir" != "$1" ]; then rp_traverse "$rp_real_dir" || return 1; fi',
|
|
' if ! sudo -u "$rp_user" -- test -x "$1"; then',
|
|
' rp_access=1',
|
|
' if sudo -u "$rp_user" -- test -r "$1"; then rp_access=$((rp_access | 4)); fi',
|
|
' if sudo -u "$rp_user" -- test -w "$1"; then rp_access=$((rp_access | 2)); fi',
|
|
' rp_mode=$(stat -Lc %a -- "$1") || return 1',
|
|
' rp_mask=$(((0$rp_mode / 8 % 8) | rp_access))',
|
|
' setfacl -n -m "u:$rp_user:$rp_access,m::$rp_mask" -- "$1" || return 1',
|
|
" fi",
|
|
"}",
|
|
""
|
|
);
|
|
if (kind === "web") {
|
|
const grantWebTree = [
|
|
"set -eu",
|
|
"rp_user=$1; shift",
|
|
'for rp_path do',
|
|
' [ ! -L "$rp_path" ] || continue',
|
|
' rp_required=4',
|
|
' if [ -d "$rp_path" ]; then rp_required=5; elif [ ! -f "$rp_path" ]; then continue; fi',
|
|
' rp_mode=$(stat -Lc %a -- "$rp_path")',
|
|
' rp_old_mask=$((0$rp_mode / 8 % 8))',
|
|
' rp_mask=$((rp_old_mask | rp_required))',
|
|
' rp_entries="u:$rp_user:$rp_required,m::$rp_mask"',
|
|
' if [ "$rp_required" = 5 ]; then',
|
|
' rp_acl=$(getfacl -cp -- "$rp_path")',
|
|
' rp_default=$(printf "%s\\n" "$rp_acl" | awk -F: \'$1 == "default" && $2 == "mask" && $3 == "" {mask=substr($4,1,3)} $1 == "default" && $2 == "group" && $3 == "" {group=substr($4,1,3)} END {print (mask != "" ? mask : group)}\')',
|
|
' rp_default_mask=$rp_old_mask',
|
|
' if [ -n "$rp_default" ]; then',
|
|
' rp_default_mask=0',
|
|
' case "$rp_default" in r??) rp_default_mask=$((rp_default_mask | 4));; esac',
|
|
' case "$rp_default" in ?w?) rp_default_mask=$((rp_default_mask | 2));; esac',
|
|
' case "$rp_default" in ??x) rp_default_mask=$((rp_default_mask | 1));; esac',
|
|
' fi',
|
|
' rp_entries="$rp_entries,d:u:$rp_user:5,d:m::$((rp_default_mask | 5))"',
|
|
" fi",
|
|
' setfacl -n -m "$rp_entries" -- "$rp_path"',
|
|
"done"
|
|
].join("\n");
|
|
lines.push(
|
|
"command -v getfacl >/dev/null || { printf '%s\\n' 'Install the acl package (getfacl) first.' >&2; exit 1; }",
|
|
'rp_root=$(readlink -e -- "$rp_root_path")',
|
|
'[ "$rp_root" != / ] && [ -d "$rp_root" ] || { printf \'%s\\n\' \'Choose an existing website directory, not /.\' >&2; exit 1; }',
|
|
'rp_traverse "$(dirname -- "$rp_root_path")"',
|
|
'rp_traverse "$(dirname -- "$rp_root")"',
|
|
`find -P "$rp_root" \\( -type d -o -type f \\) -exec sh -c ${quote(grantWebTree)} rustproxy-web-acl "$rp_user" {} +`,
|
|
'sudo -u "$rp_user" -- test -r "$rp_root"',
|
|
'sudo -u "$rp_user" -- test -x "$rp_root"'
|
|
);
|
|
} else {
|
|
lines.push(
|
|
'rp_cert=$(readlink -e -- "$rp_cert_path")',
|
|
'rp_key=$(readlink -e -- "$rp_key_path")',
|
|
'[ -f "$rp_cert" ] && [ -f "$rp_key" ] || { printf \'%s\\n\' \'Certificate and key must be existing regular files.\' >&2; exit 1; }',
|
|
'chmod go-w -- "$rp_cert" "$rp_key"',
|
|
'chmod o-r -- "$rp_key"',
|
|
'rp_traverse "$(dirname -- "$rp_cert_path")"',
|
|
'rp_traverse "$(dirname -- "$rp_key_path")"',
|
|
'rp_traverse "$(dirname -- "$rp_cert")"',
|
|
'rp_traverse "$(dirname -- "$rp_key")"',
|
|
'setfacl -n -m "u:$rp_user:r--,m::r--" -- "$rp_cert" "$rp_key"',
|
|
'sudo -u "$rp_user" -- test -r "$rp_cert_path"',
|
|
'sudo -u "$rp_user" -- test -r "$rp_key_path"'
|
|
);
|
|
}
|
|
lines.push("RUSTPROXY_ACL");
|
|
return lines.join("\n");
|
|
}
|
|
function update(view) {
|
|
const { target, options, description, summary, help, accountLabel, account, output, empty, copy } = view;
|
|
target.hidden = options.enabled === false;
|
|
description.textContent = t(options.kind === "web" ? "pathPermissionsWebDescription" : "pathPermissionsCertificateDescription");
|
|
summary.textContent = t("pathPermissionsOptions");
|
|
help.textContent = t(options.kind === "web" ? "pathPermissionsWebHelp" : "pathPermissionsCertificateHelp");
|
|
accountLabel.textContent = t("pathPermissionsUser");
|
|
copy.textContent = t("pathPermissionsCopy");
|
|
output.setAttribute("aria-label", t("pathPermissionsTitle"));
|
|
output.textContent = target.hidden ? "" : commands({ ...options, user: account.value });
|
|
output.hidden = !output.textContent;
|
|
description.hidden = output.hidden;
|
|
copy.hidden = !output.textContent;
|
|
copy.disabled = !output.textContent;
|
|
empty.hidden = !!output.textContent;
|
|
empty.textContent = t(options.kind === "web" ? "pathPermissionsWebPrompt" : "pathPermissionsCertificatePrompt");
|
|
}
|
|
function render(target, options) {
|
|
if (!target) return;
|
|
let view = views.get(target);
|
|
if (!view) {
|
|
target.classList.add("path-permissions");
|
|
const description = el("p", undefined, "fine path-permissions-description");
|
|
const actions = el("div", undefined, "form-actions path-permissions-actions");
|
|
const copy = el("button", undefined, "quiet");
|
|
copy.type = "button";
|
|
const details = el("details", undefined, "path-permissions-options");
|
|
const summary = el("summary", undefined, "fine");
|
|
const help = el("p", undefined, "fine");
|
|
const label = el("label", undefined, "path-permissions-account"), accountLabel = el("span"), account = el("input");
|
|
account.type = "text"; account.defaultValue = "rustproxy"; account.value = "rustproxy"; account.maxLength = 64;
|
|
account.autocomplete = "off"; account.spellcheck = false;
|
|
label.append(accountLabel, account);
|
|
const output = el("pre", undefined, "metrics-example path-permissions-commands");
|
|
output.tabIndex = 0;
|
|
const empty = el("p", undefined, "fine");
|
|
details.append(summary, label, help);
|
|
actions.append(copy, details);
|
|
target.replaceChildren(output, description, empty, actions);
|
|
view = { target, options, description, summary, help, accountLabel, account, output, empty, copy };
|
|
views.set(target, view);
|
|
account.addEventListener("input", () => update(view));
|
|
const selectOutput = () => {
|
|
output.focus();
|
|
const selection = window.getSelection?.();
|
|
if (!selection) return;
|
|
const range = document.createRange();
|
|
range.selectNodeContents(output);
|
|
selection.removeAllRanges(); selection.addRange(range);
|
|
};
|
|
copy.addEventListener("click", async () => {
|
|
try {
|
|
if (!output.textContent) return;
|
|
if (navigator.clipboard?.writeText) await navigator.clipboard.writeText(output.textContent);
|
|
else {
|
|
selectOutput();
|
|
if (!document.execCommand("copy")) throw new Error("clipboard unavailable");
|
|
}
|
|
toast(t("pathPermissionsCopied"));
|
|
} catch {
|
|
selectOutput();
|
|
toast(t("pathPermissionsCopyFailed"), true);
|
|
}
|
|
});
|
|
}
|
|
view.options = options;
|
|
update(view);
|
|
}
|
|
window.rustProxyPermissions = { commands, render };
|
|
})();
|