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();
+});