mirror of
https://github.com/openclaw/openclaw.git
synced 2026-08-02 21:01:34 +00:00
* fix(imessage): detect remote hosts when HOME is blank * fix(imessage): resolve blank HOME in SSH wrappers (#111715) Resolve explicitly blank or whitespace HOME values from the operating-system account, preserve configured and unset home contracts, and share the canonical resolver with the iMessage monitor and recovery cursor. Add unmocked subprocess coverage for account-home resolution and working-directory tilde shadows. Co-authored-by: LZY3538 <liu.zhenye@xydigit.com> --------- Co-authored-by: Peter Steinberger <steipete@gmail.com>
149 lines
4.4 KiB
TypeScript
149 lines
4.4 KiB
TypeScript
// Imessage plugin module classifies CLI and Messages database locality.
|
|
import { constants, accessSync, readFileSync, realpathSync } from "node:fs";
|
|
import os from "node:os";
|
|
import path from "node:path";
|
|
|
|
const localCliPathCache = new Map<string, boolean>();
|
|
const MACH_O_MAGICS = new Set([
|
|
"feedface",
|
|
"feedfacf",
|
|
"cefaedfe",
|
|
"cffaedfe",
|
|
"cafebabe",
|
|
"cafebabf",
|
|
"bebafeca",
|
|
"bfbafeca",
|
|
]);
|
|
|
|
export function resolveIMessageHomeDir(): string | undefined {
|
|
const configuredHome = process.env.HOME;
|
|
const home = configuredHome?.trim();
|
|
if (home) {
|
|
return home;
|
|
}
|
|
try {
|
|
// On POSIX, os.homedir() echoes a defined blank HOME instead of querying the account.
|
|
const systemHome = configuredHome === undefined ? os.homedir() : os.userInfo().homedir;
|
|
return systemHome.trim() || undefined;
|
|
} catch {
|
|
return undefined;
|
|
}
|
|
}
|
|
|
|
export function expandIMessageUserPath(value: string): string {
|
|
if (!value.startsWith("~")) {
|
|
return value;
|
|
}
|
|
const home = resolveIMessageHomeDir();
|
|
return home ? value.replace(/^~(?=$|[\\/])/, home) : value;
|
|
}
|
|
|
|
function resolveIMessageExecutable(cliPath: string): string | undefined {
|
|
const expanded = expandIMessageUserPath(cliPath);
|
|
if (expanded.includes(path.sep)) {
|
|
return expanded;
|
|
}
|
|
for (const directory of (process.env.PATH ?? "").split(path.delimiter)) {
|
|
if (!directory) {
|
|
continue;
|
|
}
|
|
const candidate = path.join(directory, expanded);
|
|
try {
|
|
accessSync(candidate, constants.X_OK);
|
|
return candidate;
|
|
} catch {
|
|
// Continue through PATH until an executable candidate is found.
|
|
}
|
|
}
|
|
return undefined;
|
|
}
|
|
|
|
function isMachOExecutable(filePath: string): boolean {
|
|
try {
|
|
return MACH_O_MAGICS.has(readFileSync(realpathSync(filePath)).subarray(0, 4).toString("hex"));
|
|
} catch {
|
|
return false;
|
|
}
|
|
}
|
|
|
|
function isProvenLocalIMessageCliPath(params: { cliPath: string; remoteHost?: string }): boolean {
|
|
if (params.remoteHost?.trim()) {
|
|
return false;
|
|
}
|
|
const cliPath = params.cliPath.trim();
|
|
const cacheKey = `${cliPath}\0${process.env.PATH ?? ""}`;
|
|
const cached = localCliPathCache.get(cacheKey);
|
|
if (cached !== undefined) {
|
|
return cached;
|
|
}
|
|
const executable = resolveIMessageExecutable(cliPath);
|
|
let local = executable ? isMachOExecutable(executable) : false;
|
|
if (executable && !local) {
|
|
try {
|
|
const wrapper = readFileSync(realpathSync(executable), "utf8");
|
|
const match = wrapper.match(
|
|
/^#![^\r\n]+\r?\n\s*exec\s+(?:"([^"]+)"|'([^']+)'|(\S+))\s+"\$@"\s*$/u,
|
|
);
|
|
const target = match?.[1] ?? match?.[2] ?? match?.[3];
|
|
local = Boolean(target && path.isAbsolute(target) && isMachOExecutable(target));
|
|
} catch {
|
|
local = false;
|
|
}
|
|
}
|
|
// CLI installation is process-stable channel metadata; avoid repeated file reads.
|
|
localCliPathCache.set(cacheKey, local);
|
|
return local;
|
|
}
|
|
|
|
function isLikelyLocalIMessageCliPath(params: { cliPath: string; remoteHost?: string }): boolean {
|
|
if (params.remoteHost?.trim()) {
|
|
return false;
|
|
}
|
|
const cliPath = params.cliPath.trim();
|
|
if (cliPath === "imsg") {
|
|
return true;
|
|
}
|
|
if (path.basename(cliPath) !== "imsg") {
|
|
return false;
|
|
}
|
|
try {
|
|
return !/\bssh\b[\s\S]*\bimsg\b/u.test(readFileSync(expandIMessageUserPath(cliPath), "utf8"));
|
|
} catch {
|
|
return true;
|
|
}
|
|
}
|
|
|
|
function defaultMessagesDbPath(): string | undefined {
|
|
const home = resolveIMessageHomeDir();
|
|
return home ? path.join(home, "Library", "Messages", "chat.db") : undefined;
|
|
}
|
|
|
|
export function resolveIMessageChatDbLookupPath(params: {
|
|
cliPath: string;
|
|
dbPath?: string;
|
|
remoteHost?: string;
|
|
}): string | undefined {
|
|
const configured = params.dbPath?.trim();
|
|
if (configured) {
|
|
return configured;
|
|
}
|
|
// Receipt recovery is best effort and preserves the shipped wrapper heuristic.
|
|
if (!isLikelyLocalIMessageCliPath({ cliPath: params.cliPath, remoteHost: params.remoteHost })) {
|
|
return undefined;
|
|
}
|
|
return defaultMessagesDbPath();
|
|
}
|
|
|
|
export function resolveLocalIMessageChatDbPath(params: {
|
|
cliPath: string;
|
|
dbPath?: string;
|
|
remoteHost?: string;
|
|
}): string | undefined {
|
|
// Authorization may use chat.db only after positively attesting a local imsg executable.
|
|
if (!isProvenLocalIMessageCliPath({ cliPath: params.cliPath, remoteHost: params.remoteHost })) {
|
|
return undefined;
|
|
}
|
|
const configured = params.dbPath?.trim();
|
|
return configured ? expandIMessageUserPath(configured) : defaultMessagesDbPath();
|
|
}
|