pegasus-autosign/getToken.ts

229 lines
7 KiB
TypeScript
Raw Normal View History

2025-09-29 08:25:39 +02:00
// 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 };