From 2a68c8a2871688c282efe02a80d31b67b947839d Mon Sep 17 00:00:00 2001 From: Jacob Gillespie Date: Sat, 10 Feb 2024 11:50:36 +0000 Subject: [PATCH] Implement support for GitHub Actions backscroll --- src/durable-objects/Watcher.ts | 4 +- src/utils/github.ts | 91 +++++++++++++++++++++++++++++++++- 2 files changed, 92 insertions(+), 3 deletions(-) diff --git a/src/durable-objects/Watcher.ts b/src/durable-objects/Watcher.ts index 56baf73..df07942 100644 --- a/src/durable-objects/Watcher.ts +++ b/src/durable-objects/Watcher.ts @@ -88,7 +88,7 @@ export class Watcher implements DurableObject { const lines = message.arguments.flatMap((arg) => arg.lines) for (const code of this.challenges.keys()) { if (lines.some((line) => line.includes(code))) { - console.log(`Challenge ${code} validated`) + console.log(`Challenge ${code} validated with websocket`) this.challenges.set(code, true) this.stopWatcher() return @@ -121,7 +121,7 @@ async function startWebsocketWatcher( Accept: 'application/json', Cookie: `user_session=${session}`, 'User-Agent': - 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/115.0.0.0 Safari/537.36', + 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/121.0.0.0 Safari/537.36', }, }) const body = await res.json<{data?: {authenticated_url: string}}>() diff --git a/src/utils/github.ts b/src/utils/github.ts index 1a58cfc..565c37b 100644 --- a/src/utils/github.ts +++ b/src/utils/github.ts @@ -13,6 +13,9 @@ export async function validateClaim( claimData: ClaimData, challengeCode: string, ): Promise { + const session = await env.KEYS.get('github-session') + if (!session) throw new Error('no github session') + const {data: run} = await request('GET /repos/{owner}/{repo}/actions/runs/{run_id}', { owner: claimData.owner, repo: claimData.repo, @@ -40,6 +43,21 @@ export async function validateClaim( const headSHA = job.head_sha if (!jobID || !headSHA) continue + const jobURL = `https://github.com/${claimData.owner}/${claimData.repo}/actions/runs/${claimData.runID}/jobs/${jobID}` + + promises.push( + validateChallengeCodeWithBackscroll({ + session, + org: claimData.owner, + repo: claimData.repo, + runID: claimData.runID, + jobID: jobID, + code: challengeCode, + }).then((validated) => { + return {jobID, validated} + }), + ) + promises.push( validateChallengeCode( env, @@ -95,7 +113,7 @@ export async function validateClaim( return validatedClaims } -export async function validateChallengeCode(env: Env['Bindings'], url: string, code: string): Promise { +async function validateChallengeCode(env: Env['Bindings'], url: string, code: string): Promise { const stub = env.WATCHER.get(env.WATCHER.idFromName(url)) const res = await stub.fetch('http://watcher/validate', { method: 'POST', @@ -105,3 +123,74 @@ export async function validateChallengeCode(env: Env['Bindings'], url: string, c const data = await res.json<{validated: boolean}>() return data.validated } + +interface BackscrollArgs { + session: string + org: string + repo: string + runID: number + jobID: number + code: string +} + +async function validateChallengeCodeWithBackscroll(args: BackscrollArgs): Promise { + console.log('Validating challenge with backscroll', args) + + try { + const {session, org, repo, runID, jobID, code} = args + const backscrollRegex = new RegExp(`/${org}/${repo}/actions/runs/${runID}/jobs/(\\w+)/steps/[\\w-]+/backscroll`) + const pageRes = await fetch(`https://github.com/${org}/${repo}/actions/runs/${runID}/job/${jobID}`, { + headers: { + Accept: 'text/html,*/*', + Cookie: `user_session=${session}`, + 'User-Agent': + 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/121.0.0.0 Safari/537.36', + }, + }) + const pageText = await pageRes.text() + const match = pageText.match(backscrollRegex) + if (!match) { + console.log('No backscroll job ID found', args) + return false + } + const backscrollJobID = match[1] + + const jobURL = `https://github.com/${org}/${repo}/actions/runs/${runID}/jobs/${backscrollJobID}` + const stepsRes = await fetch(`${jobURL}/steps`, { + headers: { + Accept: 'application/json', + Cookie: `user_session=${session}`, + 'User-Agent': + 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/121.0.0.0 Safari/537.36', + }, + }) + const body = await stepsRes.json<{id: string; status: string}[]>() + const runningSteps = body.filter((step) => step.status === 'in_progress') + + for (const step of runningSteps) { + try { + console.log('Fetching backscroll for', step) + const backscrollRes = await fetch(`${jobURL}/steps/${step.id}/backscroll`, { + headers: { + Accept: 'application/json', + Cookie: `user_session=${session}`, + 'User-Agent': + 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/121.0.0.0 Safari/537.36', + }, + }) + const body = await backscrollRes.json<{lines: {line: string}[]}>() + + if (body.lines.some((line) => line.line.includes(code))) { + console.log(`Challenge ${code} validated with backscroll`, args) + return true + } + } catch (e) { + console.log('Error checking backscroll for step', step, e) + } + } + } catch (e) { + console.log('Error fething backscroll', e, args) + } + + return false +}