mirror of
https://github.com/hansjone/dsh-im-ops.git
synced 2026-10-09 03:03:24 +08:00
75 lines
2.4 KiB
JavaScript
75 lines
2.4 KiB
JavaScript
const TRANSPORT_FORBIDDEN = /^transport failure for \/[A-Za-z0-9._~-]+\/[A-Za-z0-9_$./~-]+: HTTP 403$/;
|
|
|
|
export const LOOPBACK_RECOVERY_ERROR_CODE = 'loopback-recovery-required';
|
|
export const LOOPBACK_RECOVERY_ERROR_MESSAGE =
|
|
'当前地址与浏览器的本机请求校验不兼容。请使用上方按钮改用 localhost 重新打开。';
|
|
|
|
function isIpv4Loopback(hostname) {
|
|
const parts = hostname.split('.');
|
|
return parts.length === 4
|
|
&& parts[0] === '127'
|
|
&& parts.every((part) => /^\d{1,3}$/.test(part) && Number(part) <= 255);
|
|
}
|
|
|
|
/**
|
|
* Return a safe localhost navigation target for the known loopback transport failure.
|
|
*/
|
|
export function createLoopbackRecovery(error, location) {
|
|
if (!TRANSPORT_FORBIDDEN.test(error?.message ?? '')) return null;
|
|
if (typeof location?.href !== 'string') return null;
|
|
|
|
try {
|
|
const current = new URL(location.href);
|
|
if (current.protocol !== 'http:' || !isIpv4Loopback(current.hostname)) return null;
|
|
current.hostname = 'localhost';
|
|
return Object.freeze({
|
|
url: current.href,
|
|
origin: current.origin,
|
|
});
|
|
} catch {
|
|
return null;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Decorate one IM RPC caller with a narrowly scoped localhost recovery signal.
|
|
*/
|
|
export function createLoopbackAwareRpcCall(rpcCall, {
|
|
location,
|
|
onRecovery,
|
|
} = {}) {
|
|
if (typeof rpcCall !== 'function') throw new TypeError('rpcCall must be a function');
|
|
return async (...args) => {
|
|
try {
|
|
return await rpcCall(...args);
|
|
} catch (error) {
|
|
const recovery = createLoopbackRecovery(error, location);
|
|
if (!recovery) throw error;
|
|
onRecovery?.(recovery);
|
|
const presented = new Error(LOOPBACK_RECOVERY_ERROR_MESSAGE);
|
|
presented.code = LOOPBACK_RECOVERY_ERROR_CODE;
|
|
presented.cause = error;
|
|
presented.recoveryUrl = recovery.url;
|
|
throw presented;
|
|
}
|
|
};
|
|
}
|
|
|
|
/**
|
|
* Apply the same recovery behavior to every RPC caller in the combined settings page.
|
|
*/
|
|
export function createLoopbackAwareRpcCalls(rpcCalls, options) {
|
|
return Object.freeze(Object.fromEntries(
|
|
Object.entries(rpcCalls).map(([name, rpcCall]) => [
|
|
name,
|
|
typeof rpcCall === 'function'
|
|
? createLoopbackAwareRpcCall(rpcCall, options)
|
|
: rpcCall,
|
|
]),
|
|
));
|
|
}
|
|
|
|
/** Navigate without leaving the known-broken loopback address in browser history. */
|
|
export function replacePageLocation(url, location = globalThis.location) {
|
|
location?.replace?.(url);
|
|
}
|