mirror of
https://github.com/xinnan-tech/xiaozhi-esp32-server.git
synced 2026-07-21 22:53:56 +08:00
187 lines
6.5 KiB
JavaScript
187 lines
6.5 KiB
JavaScript
export const SNAPSHOT_SECRET_REDACTED = "__SNAPSHOT_SECRET_REDACTED__";
|
|
|
|
export function hasValidCurrentStateToken(value) {
|
|
return typeof value === "string" && value.trim().length > 0;
|
|
}
|
|
|
|
export function redactSnapshotDisplayValue(value, redactedLabel, parentKey = "") {
|
|
if (value === SNAPSHOT_SECRET_REDACTED || isSensitiveSnapshotKey(parentKey)) {
|
|
return redactedLabel;
|
|
}
|
|
if (Array.isArray(value)) {
|
|
return value.map((item) => redactSnapshotDisplayValue(item, redactedLabel, parentKey));
|
|
}
|
|
if (isPlainObject(value)) {
|
|
const sensitiveEntry = Object.keys(value).some((key) => {
|
|
const normalizedKey = key.toLowerCase();
|
|
return (normalizedKey === "key" || normalizedKey === "name")
|
|
&& typeof value[key] === "string"
|
|
&& isSensitiveSnapshotKey(value[key]);
|
|
});
|
|
return Object.keys(value).reduce((result, key) => {
|
|
const semanticKey = resolveUrlSemanticKey(parentKey, key);
|
|
if (sensitiveEntry && key.toLowerCase() === "value") {
|
|
result[key] = redactedLabel;
|
|
} else {
|
|
result[key] = redactSnapshotDisplayValue(value[key], redactedLabel, semanticKey);
|
|
}
|
|
return result;
|
|
}, {});
|
|
}
|
|
if (typeof value === "string" && isSnapshotUrlValue(parentKey, value)) {
|
|
return redactSnapshotUrl(value, redactedLabel, parentKey);
|
|
}
|
|
return value;
|
|
}
|
|
|
|
export function normalizeSnapshotOrderedValue(value) {
|
|
if (Array.isArray(value)) {
|
|
return value.map((item) => normalizeSnapshotOrderedValue(item));
|
|
}
|
|
if (isPlainObject(value)) {
|
|
return Object.keys(value).sort().reduce((result, key) => {
|
|
result[key] = normalizeSnapshotOrderedValue(value[key]);
|
|
return result;
|
|
}, {});
|
|
}
|
|
return value === undefined ? null : value;
|
|
}
|
|
|
|
export function isSensitiveSnapshotKey(key) {
|
|
const normalized = String(key || "").toLowerCase().replace(/[^a-z0-9]/g, "");
|
|
return normalized === "authorization"
|
|
|| normalized.includes("authorization")
|
|
|| normalized.includes("authentication")
|
|
|| normalized === "auth"
|
|
|| normalized.endsWith("auth")
|
|
|| normalized === "cookie"
|
|
|| normalized === "cookie2"
|
|
|| normalized === "setcookie"
|
|
|| normalized === "setcookie2"
|
|
|| normalized.endsWith("cookie")
|
|
|| normalized === "session"
|
|
|| normalized.endsWith("session")
|
|
|| normalized.includes("sessionid")
|
|
|| normalized.includes("sessionkey")
|
|
|| normalized.includes("sessiontoken")
|
|
|| normalized.includes("sessioncookie")
|
|
|| normalized.endsWith("sessid")
|
|
|| normalized === "token"
|
|
|| normalized.endsWith("token")
|
|
|| normalized.includes("apikey")
|
|
|| normalized.includes("appkey")
|
|
|| normalized.includes("accesskey")
|
|
|| normalized.includes("subscriptionkey")
|
|
|| normalized.includes("privatekey")
|
|
|| normalized.includes("password")
|
|
|| normalized.includes("passwd")
|
|
|| normalized.includes("secret")
|
|
|| normalized.includes("credential");
|
|
}
|
|
|
|
function isSnapshotUrlValue(parentKey, value) {
|
|
const normalizedKey = String(parentKey || "").toLowerCase().replace(/[^a-z0-9]/g, "");
|
|
return normalizedKey.includes("url")
|
|
|| normalizedKey.endsWith("uri")
|
|
|| normalizedKey.includes("endpoint")
|
|
|| normalizedKey.includes("webhook")
|
|
|| /^(?:[a-z][a-z0-9+.-]*:)*\/\//i.test(value);
|
|
}
|
|
|
|
function resolveUrlSemanticKey(parentKey, childKey) {
|
|
if (isWebhookSemanticKey(childKey)) {
|
|
return childKey;
|
|
}
|
|
if (isWebhookSemanticKey(parentKey)) {
|
|
return childKey ? `${parentKey}.${childKey}` : parentKey;
|
|
}
|
|
return childKey;
|
|
}
|
|
|
|
function isWebhookSemanticKey(value) {
|
|
const normalized = String(value || "").toLowerCase().replace(/[^a-z0-9]/g, "");
|
|
return normalized.includes("webhook")
|
|
|| normalized === "hook"
|
|
|| normalized === "hooks"
|
|
|| normalized.endsWith("hook")
|
|
|| normalized.endsWith("hooks");
|
|
}
|
|
|
|
function redactSnapshotUrl(value, redactedLabel, parentKey) {
|
|
const withoutCredentials = value.replace(
|
|
/^((?:[a-z][a-z0-9+.-]*:)*\/\/)([^/?#\s]*@)/i,
|
|
(match, prefix) => `${prefix}${redactedLabel}@`
|
|
);
|
|
const suffixIndex = withoutCredentials.search(/[?#]/);
|
|
const base = suffixIndex < 0 ? withoutCredentials : withoutCredentials.slice(0, suffixIndex);
|
|
const redactedBase = redactSnapshotCapabilityPath(base, redactedLabel, parentKey);
|
|
return suffixIndex < 0
|
|
? redactedBase
|
|
: `${redactedBase}${withoutCredentials[suffixIndex]}${redactedLabel}`;
|
|
}
|
|
|
|
function redactSnapshotCapabilityPath(value, redactedLabel, parentKey) {
|
|
const absoluteUrl = value.match(/^((?:[a-z][a-z0-9+.-]*:)*\/\/)([^/?#]*)([^?#]*)$/i);
|
|
if (!absoluteUrl) {
|
|
return redactGenericCapabilityPath(value, redactedLabel, parentKey);
|
|
}
|
|
|
|
const [, scheme, authority, path = ""] = absoluteUrl;
|
|
const hostWithPort = authority.slice(authority.lastIndexOf("@") + 1);
|
|
const host = hostWithPort.replace(/:\d+$/, "").toLowerCase();
|
|
let redactedPath = path;
|
|
let providerMatched = false;
|
|
|
|
if (host === "hooks.slack.com" || host === "hooks.slack-gov.com") {
|
|
redactedPath = redactedPath.replace(
|
|
/^(\/services\/[^/]+\/[^/]+\/)([^/]+)(.*)$/i,
|
|
(match, prefix, secret, suffix) => `${prefix}${redactedLabel}${suffix}`
|
|
);
|
|
providerMatched = redactedPath !== path;
|
|
} else if (
|
|
host === "discord.com"
|
|
|| host.endsWith(".discord.com")
|
|
|| host === "discordapp.com"
|
|
|| host.endsWith(".discordapp.com")
|
|
) {
|
|
redactedPath = redactedPath.replace(
|
|
/^(\/api(?:\/v\d+)?\/webhooks\/[^/]+\/)([^/]+)(.*)$/i,
|
|
(match, prefix, secret, suffix) => `${prefix}${redactedLabel}${suffix}`
|
|
);
|
|
providerMatched = redactedPath !== path;
|
|
} else if (host === "api.telegram.org") {
|
|
redactedPath = redactedPath.replace(
|
|
/^((?:\/file)?\/bot[^/:]+:)([^/]+)(.*)$/i,
|
|
(match, prefix, secret, suffix) => `${prefix}${redactedLabel}${suffix}`
|
|
);
|
|
providerMatched = redactedPath !== path;
|
|
}
|
|
|
|
if (!providerMatched) {
|
|
redactedPath = redactGenericCapabilityPath(redactedPath, redactedLabel, parentKey);
|
|
}
|
|
|
|
return `${scheme}${authority}${redactedPath}`;
|
|
}
|
|
|
|
function redactGenericCapabilityPath(path, redactedLabel, parentKey) {
|
|
const markerRedacted = path.replace(
|
|
/(^|\/)(webhooks?|hooks?)(\/)(.+)$/i,
|
|
(match, boundary, marker, separator) => `${boundary}${marker}${separator}${redactedLabel}`
|
|
);
|
|
if (markerRedacted !== path) {
|
|
return markerRedacted;
|
|
}
|
|
if (!isWebhookSemanticKey(parentKey)) {
|
|
return path;
|
|
}
|
|
return path.replace(
|
|
/^(.*\/)([^/]+)(\/?)$/,
|
|
(match, prefix, secret, suffix) => `${prefix}${redactedLabel}${suffix}`
|
|
);
|
|
}
|
|
|
|
function isPlainObject(value) {
|
|
return value !== null && typeof value === "object" && !Array.isArray(value);
|
|
}
|