pegasus-autosign/getToken.ts

228 lines
7 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

// getPegasusPHPSession.ts
import { secrets, sleep } from "bun";
import puppeteer, { Browser, Page } from "puppeteer";
import auth from "./auth.json";
// ——— Credentials from Bun secrets (unchanged) ———
const email = auth.email || (await secrets.get({ service: "estia", name: "email" })) || "";
const password = auth.password || (await secrets.get({ service: "estia", name: "password" })) || "";
// ——— Options ———
export type GetSessionOpts = {
headless?: true | false | "new";
debugScreenshots?: boolean;
timeoutMs?: number;
};
// ——— Utility: screenshots only when debugging ———
async function shot(page: Page, name: string, enabled: boolean) {
if (!enabled) return;
try {
await page.screenshot({ path: `./${name}.png`, fullPage: true });
} catch {}
}
// ——— Utility: wait for either navigation or one of the selectors ———
async function waitForNavOrSelector(
page: Page,
selectors: string[],
timeout = 30_000
): Promise<"navigated" | { selector: string }> {
const selPromise = (async () => {
const start = Date.now();
while (Date.now() - start < timeout) {
for (const sel of selectors) {
const el = await page.$(sel);
if (el) return { selector: sel as string };
}
await page.waitForTimeout(150);
}
throw new Error("timeout waiting for selectors");
})();
const navPromise = page
.waitForNavigation({ waitUntil: "domcontentloaded", timeout })
.then(() => "navigated" as const);
try {
return await Promise.race([selPromise, navPromise]);
} catch {
// If both rejected (rare), try a small grace wait
await page.waitForTimeout(300);
return "navigated";
}
}
// ——— Utility: resilient click that retries across navigations ———
async function clickResilient(page: Page, selector: string, { timeout = 10_000 }: { timeout?: number } = {}) {
const end = Date.now() + timeout;
while (Date.now() < end) {
try {
await page.waitForSelector(selector, { visible: true, timeout: 1000 });
await page.click(selector, { delay: 20 });
return;
} catch (err: any) {
const msg = String(err?.message || "");
// These are fine — well retry until timeout
if (
msg.includes("Execution context was destroyed") ||
msg.includes("Inspected target navigated or closed") ||
msg.includes("Cannot find context with specified id")
) {
await page.waitForTimeout(150);
continue;
}
// If element isn't there yet, keep looping
if (msg.includes("waiting for selector")) {
await page.waitForTimeout(150);
continue;
}
// Unknown error — bubble up
throw err;
}
}
throw new Error(`Timeout clicking ${selector}`);
}
// ——— Utility: wait until any page returns to the Pegasus host ———
async function pickPegasusPage(browser: Browser, timeout = 30_000): Promise<Page> {
const end = Date.now() + timeout;
while (Date.now() < end) {
const pages = await browser.pages();
for (const p of pages) {
const url = p.url();
if (url.includes("learning.estia.fr")) return p;
}
await new Promise((r) => setTimeout(r, 300));
}
throw new Error("Timeout waiting to return to learning.estia.fr");
}
// ——— Main ———
export async function getPegasusPHPSession({
headless = "new",
debugScreenshots = false,
timeoutMs = 60_000,
}: GetSessionOpts = {}): Promise<string> {
let browser: Browser | undefined;
try {
browser = await puppeteer.launch({
headless,
args: ["--no-sandbox", "--disable-setuid-sandbox"],
});
const page = await browser.newPage();
page.setDefaultTimeout(timeoutMs);
// 1) Open Pegasus
await page.goto("https://learning.estia.fr/pegasus/index.php", {
waitUntil: "networkidle2",
});
await shot(page, "01_pegasus_home", debugScreenshots);
// 2) Click Microsoft login (allow same-tab or new-tab)
const msBtn = "a.authlink[href*='o365Auth.php']";
await page.waitForSelector(msBtn, { visible: true });
await page.click(msBtn);
// 2.1) If a new AAD tab opens, switch to it
let authPage: Page = page;
const newTarget = await browser
.waitForTarget((t) => /login\.microsoftonline\.com/i.test(t.url()), {
timeout: 10_000,
})
.catch(() => undefined);
if (newTarget) {
const p = await newTarget.page();
if (p) authPage = p;
} else {
// Same tab flow
await page.waitForNavigation({ waitUntil: "domcontentloaded" }).catch(() => {});
}
await shot(authPage, "02_after_click_ms", debugScreenshots);
// 3) Email
await authPage.waitForSelector('input[name="loginfmt"]', { visible: true });
await authPage.type('input[name="loginfmt"]', email, { delay: 20 });
await authPage.keyboard.press("Enter");
// (Optional) tenant account picker
await authPage
.waitForSelector("#i0118, div[role='button'][data-telemetryid='UserTile']", { timeout: 5000 })
.catch(() => {});
const tile = await authPage.$("div[role='button'][data-telemetryid='UserTile']");
if (tile) {
await clickResilient(authPage, "div[role='button'][data-telemetryid='UserTile']", {
timeout: 5_000,
});
}
await sleep(2000); // Wait a bit for the transition
// 4) Password
await authPage.waitForSelector("#i0118", { visible: true });
await authPage.type("#i0118", password, { delay: 20 });
await authPage.keyboard.press("Enter");
await shot(authPage, "03_after_password", debugScreenshots);
await sleep(2000); // Wait a bit for the transition
// 5) KMSI (Stay signed in) — prefer "No", else "Yes".
// We *dont* run big evaluate() here; we click selectors resiliently.
const kmsiCandidates = ["#idBtn_Back", "#idSIButton9"];
const kmsiOutcome = await waitForNavOrSelector(authPage, kmsiCandidates, 20_000).catch(
() => "navigated" as const
);
if (kmsiOutcome !== "navigated") {
const toClick =
kmsiOutcome.selector === "#idBtn_Back"
? "#idBtn_Back"
: (await authPage.$("#idBtn_Back"))
? "#idBtn_Back"
: "#idSIButton9";
try {
await clickResilient(authPage, toClick, { timeout: 8_000 });
} catch {
// If we miss it due to instant nav, thats okay
}
}
await shot(authPage, "04_after_kmsi", debugScreenshots);
// 6) Back to Pegasus (could happen in any tab)
let finalPage: Page;
try {
finalPage = await pickPegasusPage(browser, 30_000);
} catch {
// As a fallback, let the current authPage settle
await authPage.waitForNavigation({ waitUntil: "domcontentloaded", timeout: 10_000 }).catch(() => {});
finalPage = authPage;
}
// Network settle
try {
await finalPage.waitForNetworkIdle({ idleTime: 1000, timeout: 30_000 });
} catch {}
await shot(finalPage, "05_back_on_pegasus", debugScreenshots);
// 7) Cookies → PHPSESSID
const cookies = await finalPage.cookies("https://learning.estia.fr");
const php = cookies.find((c) => c.name.toLowerCase() === "phpsessid");
if (!php?.value) {
throw new Error("PHPSESSID cookie not found. Are you logged in?");
}
return php.value;
} finally {
if (browser) {
try {
await browser.close();
} catch {}
}
}
}
const token = await getPegasusPHPSession({ headless: true, debugScreenshots: false });
export { token };