From 920b5fcb9a9f222fa14de94580424ecf07810c09 Mon Sep 17 00:00:00 2001 From: ahuigo <1781999+ahuigo@users.noreply.github.com> Date: Wed, 8 Jan 2025 11:38:00 +0800 Subject: [PATCH] feat(authenticate): Provide credentials for HTTP authentication(redo) --- .github/workflows/ci.yml | 4 ++++ README.md | 31 +++++++++++++++++++++++++++++++ examples/authenticate.ts | 24 ++++++++++++++++++++++++ src/page.ts | 34 ++++++++++++++++++++++++++++------ tests/authenticate_test.ts | 29 +++++++++++++++++++++++++++++ 5 files changed, 116 insertions(+), 6 deletions(-) create mode 100644 examples/authenticate.ts create mode 100644 tests/authenticate_test.ts diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 7518e15..1843a13 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -20,6 +20,10 @@ jobs: with: deno-version: v2.x + - name: Disable AppArmor + if: ${{ matrix.os == 'ubuntu-latest' }} + run: echo 0 | sudo tee /proc/sys/kernel/apparmor_restrict_unprivileged_userns + - name: check format if: matrix.os == 'ubuntu-latest' run: deno fmt --check diff --git a/README.md b/README.md index 5a55b8f..0503200 100644 --- a/README.md +++ b/README.md @@ -138,6 +138,16 @@ const browser = await launch(); const anotherBrowser = await launch({ wsEndpoint: browser.wsEndpoint() }); ``` +### Page authenticate + +[authenticate example code](https://github.com/lino-levan/astral/blob/main/examples/authenticate.ts): + + // Open a new page + const page = await browser.newPage("https://httpbin.org/basic-auth/user/passwd"); + + // Provide credentials for HTTP authentication. + await page.authenticate({ username: "user", password: "passwd" }); + ## BYOB - Bring Your Own Browser Essentially the process is as simple as running a chromium-like binary with the @@ -175,3 +185,24 @@ console.log(await page.evaluate(() => document.title)); // Close connection await browser.close(); ``` + +## FAQ + +### Launch FAQ + +#### "No usable sandbox!" with user namespace cloning enabled + +> Ubuntu 23.10+ (or possibly other Linux distros in the future) ship an AppArmor +> profile that applies to Chrome stable binaries installed at +> /opt/google/chrome/chrome (the default installation path). This policy is +> stored at /etc/apparmor.d/chrome. This AppArmor policy prevents Chrome for +> Testing binaries downloaded by Puppeteer from using user namespaces resulting +> in the No usable sandbox! error when trying to launch the browser. For +> workarounds, see +> https://chromium.googlesource.com/chromium/src/+/main/docs/security/apparmor-userns-restrictions.md + +The following shell can remove AppArmor restrictions on user namespaces so that +Puppeteer can launch Chrome without the "No usable sandbox!" error (Refer +[puppeteer#13196](https://github.com/puppeteer/puppeteer/pull/13196)): + + echo 0 | sudo tee /proc/sys/kernel/apparmor_restrict_unprivileged_userns diff --git a/examples/authenticate.ts b/examples/authenticate.ts new file mode 100644 index 0000000..1b2647d --- /dev/null +++ b/examples/authenticate.ts @@ -0,0 +1,24 @@ +/// + +// Import Astral +import { launch } from "../mod.ts"; + +// Launch the browser +const browser = await launch(); + +// Open a new page +const page = await browser.newPage(); + +// Provide credentials for HTTP authentication. +await page.authenticate({ username: "postman", password: "password" }); +const url = "https://postman-echo.com/basic-auth"; +await page.goto(url, { waitUntil: "networkidle2" }); + +// Get response +const content = await page.evaluate(() => { + return document.body.innerText; +}); +console.log(content); + +// Close the browser +await browser.close(); diff --git a/src/page.ts b/src/page.ts index a052074..63aea72 100644 --- a/src/page.ts +++ b/src/page.ts @@ -1,21 +1,21 @@ import { deadline } from "@std/async/deadline"; import { fromFileUrl } from "@std/path/from-file-url"; -import { Celestial } from "../bindings/celestial.ts"; import type { Fetch_requestPausedEvent, Network_Cookie, Runtime_consoleAPICalled, } from "../bindings/celestial.ts"; +import { Celestial } from "../bindings/celestial.ts"; import type { Browser } from "./browser.ts"; -import { ElementHandle } from "./element_handle.ts"; -import { convertToUint8Array, retryDeadline } from "./util.ts"; -import { Mouse } from "./mouse.ts"; -import { Keyboard } from "./keyboard.ts"; -import { Touchscreen } from "./touchscreen.ts"; import { Dialog } from "./dialog.ts"; +import { ElementHandle } from "./element_handle.ts"; import { FileChooser } from "./file_chooser.ts"; +import { Keyboard } from "./keyboard.ts"; import { Locator } from "./locator.ts"; +import { Mouse } from "./mouse.ts"; +import { Touchscreen } from "./touchscreen.ts"; +import { convertToUint8Array, retryDeadline } from "./util.ts"; /** The options for deleting a cookie */ export type DeleteCookieOptions = Omit< @@ -286,6 +286,28 @@ export class Page extends EventTarget { return this.#celestial; } + /** + * Provide credentials for HTTP authentication. + * + * @example + * ```ts + * await page.authenticate({ 'username': username, 'password': password }); + * ``` + */ + authenticate( + { username, password }: { username: string; password: string }, + ): Promise { + function base64encoded(s: string) { + const bytes = new TextEncoder().encode(s); + return btoa(String.fromCharCode(...bytes)); + } + + const auth = base64encoded(`${username}:${password}`); + return this.#celestial.Network.setExtraHTTPHeaders({ + headers: { "Authorization": `Basic ${auth}` }, + }); + } + /** * Runs `document.querySelector` within the page. If no element matches the selector, the return value resolves to `null`. * diff --git a/tests/authenticate_test.ts b/tests/authenticate_test.ts new file mode 100644 index 0000000..5bd6a2f --- /dev/null +++ b/tests/authenticate_test.ts @@ -0,0 +1,29 @@ +/// + +import { assertEquals, assertNotEquals } from "@std/assert"; + +import { launch } from "../mod.ts"; + +Deno.test("Testing authenticate", async (t) => { + // Open the webpage + const browser = await launch({ headless: true }); + const page = await browser.newPage(); + + // Provide credentials for HTTP authentication. + await page.authenticate({ username: "postman", password: "password" }); + const url = "https://postman-echo.com/basic-auth"; + await page.goto(url, { waitUntil: "networkidle2" }); + + // Get JSON response + const content = await page.evaluate(() => { + return document.body.innerText; + }); + + // Assert JSON response + assertNotEquals(content, ""); + const response = JSON.parse(content); + assertEquals(response.authenticated, true); + + // Close browser + await browser.close(); +});