feat: add support for deferring Step 9 callback timeout during auth security verification
This commit is contained in:
@@ -12042,6 +12042,29 @@ function throwIfStep8SettledOrStopped(isSettled = false) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function isStep9AuthCallbackWaitPageUrl(rawUrl) {
|
||||||
|
if (!rawUrl) return false;
|
||||||
|
try {
|
||||||
|
const parsed = new URL(rawUrl);
|
||||||
|
const hostname = String(parsed.hostname || '').toLowerCase();
|
||||||
|
if (!['auth.openai.com', 'auth0.openai.com', 'accounts.openai.com'].includes(hostname)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
const pathname = String(parsed.pathname || '');
|
||||||
|
return /\/api\/oauth\/oauth2\/auth(?:[/?#]|$)/i.test(pathname)
|
||||||
|
|| /\/oauth\/oauth2\/auth(?:[/?#]|$)/i.test(pathname);
|
||||||
|
} catch {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function shouldDeferStep9CallbackTimeout(details = {}) {
|
||||||
|
const tabId = details?.tabId;
|
||||||
|
if (!Number.isInteger(tabId)) return false;
|
||||||
|
const tab = await chrome.tabs.get(tabId).catch(() => null);
|
||||||
|
return isStep9AuthCallbackWaitPageUrl(tab?.url || '');
|
||||||
|
}
|
||||||
|
|
||||||
async function ensureStep8SignupPageReady(tabId, options = {}) {
|
async function ensureStep8SignupPageReady(tabId, options = {}) {
|
||||||
const visibleStep = Math.floor(Number(options.visibleStep || options.logStep || options.step) || 0);
|
const visibleStep = Math.floor(Number(options.visibleStep || options.logStep || options.step) || 0);
|
||||||
await ensureContentScriptReadyOnTab('signup-page', tabId, {
|
await ensureContentScriptReadyOnTab('signup-page', tabId, {
|
||||||
@@ -12496,6 +12519,7 @@ const step9Executor = self.MultiPageBackgroundStep9?.createStep9Executor({
|
|||||||
setStep8TabUpdatedListener,
|
setStep8TabUpdatedListener,
|
||||||
setWebNavCommittedListener,
|
setWebNavCommittedListener,
|
||||||
setWebNavListener,
|
setWebNavListener,
|
||||||
|
shouldDeferStep9CallbackTimeout,
|
||||||
sleepWithStop,
|
sleepWithStop,
|
||||||
STEP8_CLICK_RETRY_DELAY_MS,
|
STEP8_CLICK_RETRY_DELAY_MS,
|
||||||
STEP8_MAX_ROUNDS,
|
STEP8_MAX_ROUNDS,
|
||||||
|
|||||||
@@ -33,6 +33,7 @@
|
|||||||
setWebNavCommittedListener,
|
setWebNavCommittedListener,
|
||||||
setStep8PendingReject,
|
setStep8PendingReject,
|
||||||
setStep8TabUpdatedListener,
|
setStep8TabUpdatedListener,
|
||||||
|
shouldDeferStep9CallbackTimeout,
|
||||||
} = deps;
|
} = deps;
|
||||||
|
|
||||||
const LOCALHOST_CALLBACK_LOCAL_TIMEOUT_MS = 240000;
|
const LOCALHOST_CALLBACK_LOCAL_TIMEOUT_MS = 240000;
|
||||||
@@ -96,6 +97,7 @@
|
|||||||
let signupTabId = null;
|
let signupTabId = null;
|
||||||
const callbackWaitStartedAt = Date.now();
|
const callbackWaitStartedAt = Date.now();
|
||||||
let timeoutCheckTimer = null;
|
let timeoutCheckTimer = null;
|
||||||
|
let timeoutDeferredLogged = false;
|
||||||
|
|
||||||
const cleanupListener = () => {
|
const cleanupListener = () => {
|
||||||
if (timeoutCheckTimer) {
|
if (timeoutCheckTimer) {
|
||||||
@@ -128,11 +130,46 @@
|
|||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const isCallbackTimeoutDeferred = async (elapsedMs) => {
|
||||||
|
if (typeof shouldDeferStep9CallbackTimeout !== 'function') {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
const deferred = await shouldDeferStep9CallbackTimeout({
|
||||||
|
tabId: signupTabId,
|
||||||
|
visibleStep,
|
||||||
|
elapsedMs,
|
||||||
|
oauthUrl: activeState?.oauthUrl || '',
|
||||||
|
});
|
||||||
|
if (deferred && !timeoutDeferredLogged) {
|
||||||
|
timeoutDeferredLogged = true;
|
||||||
|
await addStepLog(
|
||||||
|
visibleStep,
|
||||||
|
'检测到认证页仍在安全验证/授权跳转中,暂停本地回调超时判定,继续等待 localhost 回调...',
|
||||||
|
'info'
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return Boolean(deferred);
|
||||||
|
} catch (error) {
|
||||||
|
await addStepLog(
|
||||||
|
visibleStep,
|
||||||
|
`复核认证页跳转状态失败(${error?.message || error}),继续按原超时规则等待回调。`,
|
||||||
|
'warn'
|
||||||
|
);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
const checkCallbackTimeout = async () => {
|
const checkCallbackTimeout = async () => {
|
||||||
if (resolved) {
|
if (resolved) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const elapsedMs = Date.now() - callbackWaitStartedAt;
|
const elapsedMs = Date.now() - callbackWaitStartedAt;
|
||||||
|
if (await isCallbackTimeoutDeferred(elapsedMs)) {
|
||||||
|
timeoutCheckTimer = setTimeout(checkCallbackTimeout, CALLBACK_TIMEOUT_CHECK_INTERVAL_MS);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
if (elapsedMs >= LOCALHOST_CALLBACK_LOCAL_TIMEOUT_MS) {
|
if (elapsedMs >= LOCALHOST_CALLBACK_LOCAL_TIMEOUT_MS) {
|
||||||
rejectStep9(new Error(`${Math.round(LOCALHOST_CALLBACK_LOCAL_TIMEOUT_MS / 1000)} 秒内未捕获到 localhost 回调跳转,步骤 ${visibleStep} 的点击可能被拦截了。`));
|
rejectStep9(new Error(`${Math.round(LOCALHOST_CALLBACK_LOCAL_TIMEOUT_MS / 1000)} 秒内未捕获到 localhost 回调跳转,步骤 ${visibleStep} 的点击可能被拦截了。`));
|
||||||
return;
|
return;
|
||||||
|
|||||||
@@ -182,3 +182,175 @@ return {
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('step9 defers callback timeout while auth security verification is still redirecting', async () => {
|
||||||
|
const api = new Function('step9ModuleSource', `
|
||||||
|
const self = {};
|
||||||
|
let webNavListener = null;
|
||||||
|
let webNavCommittedListener = null;
|
||||||
|
let step8TabUpdatedListener = null;
|
||||||
|
let cleanupCalls = 0;
|
||||||
|
let deferCalls = 0;
|
||||||
|
let completePayload = null;
|
||||||
|
const callbackUrl = 'http://localhost:1455/auth/callback?code=abc&state=xyz';
|
||||||
|
|
||||||
|
const chrome = {
|
||||||
|
webNavigation: {
|
||||||
|
onBeforeNavigate: {
|
||||||
|
addListener(listener) {
|
||||||
|
webNavListener = listener;
|
||||||
|
setTimeout(() => {
|
||||||
|
if (typeof webNavListener === 'function') {
|
||||||
|
webNavListener({ tabId: 123, url: callbackUrl });
|
||||||
|
}
|
||||||
|
}, 25);
|
||||||
|
},
|
||||||
|
removeListener() {},
|
||||||
|
},
|
||||||
|
onCommitted: {
|
||||||
|
addListener(listener) {
|
||||||
|
webNavCommittedListener = listener;
|
||||||
|
},
|
||||||
|
removeListener() {},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
tabs: {
|
||||||
|
onUpdated: {
|
||||||
|
addListener(listener) {
|
||||||
|
step8TabUpdatedListener = listener;
|
||||||
|
},
|
||||||
|
removeListener() {},
|
||||||
|
},
|
||||||
|
async update() {},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
async function addLog() {}
|
||||||
|
function cleanupStep8NavigationListeners() {
|
||||||
|
cleanupCalls += 1;
|
||||||
|
webNavListener = null;
|
||||||
|
webNavCommittedListener = null;
|
||||||
|
step8TabUpdatedListener = null;
|
||||||
|
}
|
||||||
|
function throwIfStep8SettledOrStopped(resolved) {
|
||||||
|
if (resolved) throw new Error('already resolved');
|
||||||
|
}
|
||||||
|
async function getTabId() { return 123; }
|
||||||
|
async function isTabAlive() { return true; }
|
||||||
|
async function ensureStep8SignupPageReady() {}
|
||||||
|
async function getOAuthFlowStepTimeoutMs(defaultTimeoutMs, options = {}) {
|
||||||
|
if (options.actionLabel === 'OAuth localhost 回调') {
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
return defaultTimeoutMs;
|
||||||
|
}
|
||||||
|
async function shouldDeferStep9CallbackTimeout(details = {}) {
|
||||||
|
deferCalls += 1;
|
||||||
|
return Number(details.tabId) === 123;
|
||||||
|
}
|
||||||
|
async function waitForStep8Ready() {
|
||||||
|
return {
|
||||||
|
consentReady: true,
|
||||||
|
url: 'https://auth.openai.com/sign-in-with-chatgpt/codex/consent',
|
||||||
|
};
|
||||||
|
}
|
||||||
|
async function triggerStep8ContentStrategy() {}
|
||||||
|
async function waitForStep8ClickEffect() {
|
||||||
|
return {
|
||||||
|
progressed: true,
|
||||||
|
reason: 'url_changed',
|
||||||
|
url: 'https://auth.openai.com/api/oauth/oauth2/auth?client_id=app_test',
|
||||||
|
};
|
||||||
|
}
|
||||||
|
function getStep8CallbackUrlFromNavigation(details, signupTabId) {
|
||||||
|
return Number(details?.tabId) === Number(signupTabId) ? details.url : '';
|
||||||
|
}
|
||||||
|
function getStep8CallbackUrlFromTabUpdate() { return ''; }
|
||||||
|
function getStep8EffectLabel() { return 'URL 已变化'; }
|
||||||
|
async function prepareStep8DebuggerClick() { return { rect: { centerX: 10, centerY: 10 } }; }
|
||||||
|
async function clickWithDebugger() {}
|
||||||
|
async function reloadStep8ConsentPage() {}
|
||||||
|
async function reuseOrCreateTab() { return 123; }
|
||||||
|
async function sleepWithStop() {}
|
||||||
|
function setWebNavListener(listener) { webNavListener = listener; }
|
||||||
|
function getWebNavListener() { return webNavListener; }
|
||||||
|
function setWebNavCommittedListener(listener) { webNavCommittedListener = listener; }
|
||||||
|
function getWebNavCommittedListener() { return webNavCommittedListener; }
|
||||||
|
function setStep8TabUpdatedListener(listener) { step8TabUpdatedListener = listener; }
|
||||||
|
function getStep8TabUpdatedListener() { return step8TabUpdatedListener; }
|
||||||
|
function setStep8PendingReject() {}
|
||||||
|
async function completeStepFromBackground(step, payload) {
|
||||||
|
completePayload = { step, payload };
|
||||||
|
}
|
||||||
|
|
||||||
|
const STEP8_CLICK_RETRY_DELAY_MS = 1;
|
||||||
|
const STEP8_READY_WAIT_TIMEOUT_MS = 5;
|
||||||
|
const STEP8_MAX_ROUNDS = 1;
|
||||||
|
const STEP8_STRATEGIES = [
|
||||||
|
{ mode: 'content', strategy: 'requestSubmit', label: 'form.requestSubmit' },
|
||||||
|
];
|
||||||
|
|
||||||
|
${step9ModuleSource}
|
||||||
|
|
||||||
|
const executor = self.MultiPageBackgroundStep9.createStep9Executor({
|
||||||
|
addLog,
|
||||||
|
chrome,
|
||||||
|
cleanupStep8NavigationListeners,
|
||||||
|
clickWithDebugger,
|
||||||
|
completeStepFromBackground,
|
||||||
|
ensureStep8SignupPageReady,
|
||||||
|
getOAuthFlowStepTimeoutMs,
|
||||||
|
getStep8CallbackUrlFromNavigation,
|
||||||
|
getStep8CallbackUrlFromTabUpdate,
|
||||||
|
getStep8EffectLabel,
|
||||||
|
getTabId,
|
||||||
|
getWebNavCommittedListener,
|
||||||
|
getWebNavListener,
|
||||||
|
getStep8TabUpdatedListener,
|
||||||
|
isTabAlive,
|
||||||
|
prepareStep8DebuggerClick,
|
||||||
|
reloadStep8ConsentPage,
|
||||||
|
reuseOrCreateTab,
|
||||||
|
setStep8PendingReject,
|
||||||
|
setStep8TabUpdatedListener,
|
||||||
|
setWebNavCommittedListener,
|
||||||
|
setWebNavListener,
|
||||||
|
shouldDeferStep9CallbackTimeout,
|
||||||
|
sleepWithStop,
|
||||||
|
STEP8_CLICK_RETRY_DELAY_MS,
|
||||||
|
STEP8_MAX_ROUNDS,
|
||||||
|
STEP8_READY_WAIT_TIMEOUT_MS,
|
||||||
|
STEP8_STRATEGIES,
|
||||||
|
throwIfStep8SettledOrStopped,
|
||||||
|
triggerStep8ContentStrategy,
|
||||||
|
waitForStep8ClickEffect,
|
||||||
|
waitForStep8Ready,
|
||||||
|
});
|
||||||
|
|
||||||
|
return {
|
||||||
|
executeStep9: executor.executeStep9,
|
||||||
|
snapshot() {
|
||||||
|
return {
|
||||||
|
cleanupCalls,
|
||||||
|
deferCalls,
|
||||||
|
completePayload,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
};
|
||||||
|
`)(step9ModuleSource);
|
||||||
|
|
||||||
|
await api.executeStep9({
|
||||||
|
oauthUrl: 'https://auth.openai.com/original-oauth',
|
||||||
|
visibleStep: 12,
|
||||||
|
});
|
||||||
|
|
||||||
|
const snapshot = api.snapshot();
|
||||||
|
assert.equal(snapshot.deferCalls >= 1, true);
|
||||||
|
assert.equal(snapshot.cleanupCalls >= 1, true);
|
||||||
|
assert.deepEqual(snapshot.completePayload, {
|
||||||
|
step: 12,
|
||||||
|
payload: {
|
||||||
|
localhostUrl: 'http://localhost:1455/auth/callback?code=abc&state=xyz',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|||||||
Reference in New Issue
Block a user