mirror of
https://github.com/hansjone/dsh-im-ops.git
synced 2026-10-09 03:03:24 +08:00
fix(client): recover loopback RPC 403 via localhost
This commit is contained in:
parent
9e39333ff6
commit
d999cb511f
6 changed files with 425 additions and 18 deletions
75
plugin-src/client/loopback-recovery.js
Normal file
75
plugin-src/client/loopback-recovery.js
Normal file
|
|
@ -0,0 +1,75 @@
|
|||
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);
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue