diff --git a/packages/vjudge/src/providers/codeforces.ts b/packages/vjudge/src/providers/codeforces.ts index d95abb33e..941da8db0 100644 --- a/packages/vjudge/src/providers/codeforces.ts +++ b/packages/vjudge/src/providers/codeforces.ts @@ -82,6 +82,16 @@ export default class CodeforcesProvider extends BasicFetcher implements IBasicPr return _tta; } + getCsrfTokenOnDocument(document: Document) { + const meta = document.querySelector("meta[name='X-Csrf-Token']")?.getAttribute('content'); + if (meta?.length === 32) return meta; + const span = document.querySelector('span.csrf-token')?.getAttribute('data-csrf'); + if (span?.length === 32) return span; + const input = document.querySelector('input[name="csrf_token"]')?.getAttribute('value'); + if (input?.length === 32) return input; + return ''; + } + async getCsrfToken(url: string) { const { document, html, headers } = await this.html(url); if (document.body.children.length < 2 && html.length < 512) { @@ -89,13 +99,7 @@ export default class CodeforcesProvider extends BasicFetcher implements IBasicPr } const ftaa = this.getCookie('70a7c28f3de') || 'n/a'; const bfaa = this.getCookie('raa') || this.getCookie('bfaa') || 'n/a'; - return [ - ( - document.querySelector('meta[name="X-Csrf-Token"]') - || document.querySelector('input[name="csrf_token"]') - )?.getAttribute('content'), - ftaa, bfaa, headers, - ]; + return [this.getCsrfTokenOnDocument(document), ftaa, bfaa, headers]; } get loggedIn() { @@ -270,11 +274,15 @@ export default class CodeforcesProvider extends BasicFetcher implements IBasicPr }); } - async readLatestSubmission(contestId = '') { - const { document } = await this.html(contestId ? `/gym/${contestId}/my` : '/problemset/status?my=on'); - this.csrf = document.querySelector('meta[name="X-Csrf-Token"]').getAttribute('content'); - const submission = document.querySelector('[data-submission-id]').getAttribute('data-submission-id'); - return submission; + async readLatestSubmission(contestId = '', allowEmpty = false) { + for (let i = 1; i <= 3; i++) { + await sleep(1000); + const { document } = await this.html(contestId ? `/gym/${contestId}/my` : '/problemset/status?my=on'); + this.csrf = this.getCsrfTokenOnDocument(document); + const id = document.querySelector('[data-submission-id]')?.getAttribute('data-submission-id'); + if (id || allowEmpty) return id; + } + return null; } async submitProblem(id: string, lang: string, code: string, info, next, end) { @@ -283,37 +291,38 @@ export default class CodeforcesProvider extends BasicFetcher implements IBasicPr const endpoint = type === 'GYM' ? `/gym/${contestId}/submit` : `/problemset/submit/${contestId}/${problemId}`; - const latestSubmission = await this.readLatestSubmission(type === 'GYM' ? contestId : ''); - const [csrf, ftaa, bfaa] = await this.getCsrfToken(endpoint); - // TODO check submit time to ensure submission - const { text: submit, redirects } = await this.post(`${endpoint}?csrf_token=${csrf}`).send({ - csrf_token: csrf, - action: 'submitSolutionFormSubmitted', - programTypeId, - source: code, - tabSize: 4, - sourceFile: '', - ftaa, - bfaa, - _tta: this.tta(this.getCookie('39ce7')), - contestId, - submittedProblemIndex: problemId, - }); - const { window: { document: statusDocument } } = new JSDOM(submit); - const message = Array.from(statusDocument.querySelectorAll('.error')) - .map((i) => i.textContent).join('').replace(/ /g, ' ').trim(); - if (message) { - end({ status: STATUS.STATUS_SYSTEM_ERROR, message }); - return null; - } - const submission = await this.readLatestSubmission(type === 'GYM' ? contestId : ''); - if (submission === latestSubmission || redirects.length === 0 || !redirects.toString().includes('my')) { - // Submit failed so the request is not redirected + try { + const latestSubmission = await this.readLatestSubmission(type === 'GYM' ? contestId : '', true); + const [csrf, ftaa, bfaa] = await this.getCsrfToken(endpoint); + // TODO check submit time to ensure submission + const { text: submit, redirects } = await this.post(`${endpoint}?csrf_token=${csrf}`).send({ + csrf_token: csrf, + action: 'submitSolutionFormSubmitted', + programTypeId, + source: code, + tabSize: 4, + sourceFile: '', + ftaa, + bfaa, + _tta: this.tta(this.getCookie('39ce7')), + contestId, + submittedProblemIndex: problemId, + }); + const { window: { document: statusDocument } } = new JSDOM(submit); + const message = Array.from(statusDocument.querySelectorAll('.error')) + .map((i) => i.textContent).join('').replace(/ /g, ' ').trim(); + if (message) throw new Error(message); + const submission = await this.readLatestSubmission(type === 'GYM' ? contestId : ''); + if (!submission) throw new Error('Failed to get submission id.'); + if (submission === latestSubmission) throw new Error('Submission page is not updated.'); + if (redirects.length === 0 || !redirects.toString().includes('my')) throw new Error('No redirect to submission page.'); + return type !== 'GYM' ? submission : `${contestId}#${submission}`; + } catch (e) { + next({ message: e.message }); // eslint-disable-next-line max-len - end({ status: STATUS.STATUS_SYSTEM_ERROR, message: 'Submit failed. Check service status or use better network to avoid rejection by server protection.' }); + end({ status: STATUS.STATUS_SYSTEM_ERROR, message: 'Submit to remote failed. Check service status or use better network to avoid rejection by server protection.' }); return null; } - return type !== 'GYM' ? submission : `${contestId}#${submission}`; } async waitForSubmission(id: string, next, end) {