From 939e0ace679fc20d46bcec7a0210f2325660f43d Mon Sep 17 00:00:00 2001 From: Andrew Berry Date: Thu, 8 Oct 2026 13:23:05 -0400 Subject: [PATCH 1/3] fix: disable KasmVNC basic authentication --- README.md | 2 +- config.playwright.yml | 3 +- tests/test.bats | 15 ++++----- tests/testdata/kasmvnc.spec.ts | 39 ++++++++++++++++++++++++ web-build/disabled.Dockerfile.playwright | 4 ++- 5 files changed, 53 insertions(+), 10 deletions(-) create mode 100644 tests/testdata/kasmvnc.spec.ts diff --git a/README.md b/README.md index 24a582d..9975810 100644 --- a/README.md +++ b/README.md @@ -78,7 +78,7 @@ The following services are exposed with this addon: | Service | URL | Notes | |-------------------------|-----------------------------------|--------------------------------------------------------------------------------------------| -| KasmVNC | https://\.ddev.site:8444 | Username is your local username. Password is `secret`. | +| KasmVNC | https://\.ddev.site:8444 | No login required. | | Playwright Test Reports | https://\.ddev.site:9324 | This port is changed from the default to not conflict with running Playwright on the host. | ## Viewing test reports diff --git a/config.playwright.yml b/config.playwright.yml index 39ea2bd..40dddd4 100644 --- a/config.playwright.yml +++ b/config.playwright.yml @@ -85,7 +85,8 @@ web_environment: # We add the sleep so this doesn't error out when not using playwright. web_extra_daemons: - name: "kasmvnc" - command: "kasmvncserver -fg || sleep infinity" + # Basic Auth is disabled for both HTTP and the desktop WebSocket. + command: "kasmvncserver -fg -disableBasicAuth || sleep infinity" directory: /var/www/html web_extra_exposed_ports: - name: playwright diff --git a/tests/test.bats b/tests/test.bats index c7315a7..399e73e 100644 --- a/tests/test.bats +++ b/tests/test.bats @@ -121,6 +121,7 @@ verify_run_playwright() { mkdir -p "${playwright_dir}/tests" cp "$DIR"/tests/testdata/phpinfo.spec.ts "${playwright_dir}/tests/phpinfo.spec.ts" + cp "$DIR"/tests/testdata/kasmvnc.spec.ts "${playwright_dir}/tests/kasmvnc.spec.ts" health_checks # Both paths expose the same tmpfs: the namespaced path is preferred, while @@ -129,12 +130,11 @@ verify_run_playwright() { assert_success ddev exec -- rm -f /tmp/ddev-playwright/compatibility-check - # Verify kasmvnc is listening. - curl -s https://"${PROJNAME}".ddev.site:8444/ - curl -s --user "$USER":secret https://"${PROJNAME}.ddev.site:8444/" - ddev logs - echo "#" curl -s --user "$USER":secret https://"${PROJNAME}.ddev.site:8444/" >&3 - curl -s --user "$USER":secret https://"${PROJNAME}.ddev.site:8444/" | grep -q KasmVNC + # The HTTP client must load without credentials or an authentication challenge. + run curl --fail --silent --show-error --dump-header - "https://${PROJNAME}.ddev.site:8444/" + assert_success + assert_output --partial "KasmVNC" + refute_output --partial "WWW-Authenticate:" # Verify that browsers have been downloaded. # @@ -163,7 +163,8 @@ verify_run_playwright() { # Verify we can run an example test. The reporters come from # playwright.config.ts (line + html); the html one leaves behind a report for # verify_show_report to serve. - ddev playwright test + # Each browser controls the same desktop, so run them one at a time. + ddev playwright test --workers=1 verify_show_report } diff --git a/tests/testdata/kasmvnc.spec.ts b/tests/testdata/kasmvnc.spec.ts new file mode 100644 index 0000000..9ad7079 --- /dev/null +++ b/tests/testdata/kasmvnc.spec.ts @@ -0,0 +1,39 @@ +import { test, expect } from '@playwright/test'; +import { randomUUID } from 'node:crypto'; +import { existsSync, rmSync } from 'node:fs'; + +// Runs in Chromium, Firefox and WebKit, with no httpCredentials configured. +// Loading the HTML alone misses failures in WebSocket auth or RFB negotiation. +test('KasmVNC connects to the desktop without credentials', async ({ page }) => { + test.setTimeout(60_000); + const url = new URL(process.env.DDEV_PRIMARY_URL!); + url.port = '8444'; + url.searchParams.set('autoconnect', '1'); + + const response = await page.goto(url.toString()); + expect(response?.status()).toBe(200); + expect(response?.headers()['www-authenticate']).toBeUndefined(); + await expect(page.locator('html')).toHaveClass(/noVNC_connected/, { timeout: 30_000 }); + + // A completed RFB connection must have received the desktop dimensions. + const canvas = page.locator('#noVNC_container canvas').first(); + await expect(canvas).toBeVisible(); + await expect.poll(() => canvas.evaluate((element: HTMLCanvasElement) => + element.width > 0 && element.height > 0 + )).toBe(true); + + // Exercise remote keyboard input through IceWM's command bar. The marker is + // created by the remote desktop, in the same container as this test runner. + const marker = `/tmp/kasmvnc-input-${randomUUID()}`; + try { + await canvas.click({ position: { x: 10, y: 10 } }); + const desktop = await canvas.screenshot(); + await page.keyboard.press('Control+Alt+Space'); + await expect.poll(async () => (await canvas.screenshot()).equals(desktop)).toBe(false); + await page.keyboard.type(`touch ${marker}`, { delay: 20 }); + await page.keyboard.press('Enter'); + await expect.poll(() => existsSync(marker), { timeout: 10_000 }).toBe(true); + } finally { + rmSync(marker, { force: true }); + } +}); diff --git a/web-build/disabled.Dockerfile.playwright b/web-build/disabled.Dockerfile.playwright index 8aa2781..6022c7e 100644 --- a/web-build/disabled.Dockerfile.playwright +++ b/web-build/disabled.Dockerfile.playwright @@ -85,7 +85,9 @@ COPY kasmvnc.yaml xstartup /home/$username/.vnc RUN chown $username:$username /home/$username/.vnc/* RUN sudo -u $username touch /home/$username/.vnc/.de-was-selected -RUN sudo -u $username /bin/bash -c 'echo -e "secret\nsecret\n" | kasmvncpasswd -wo -u $USER' +# KasmVNC 1.4.0's launcher requires an account even with -disableBasicAuth. +# This random credential only satisfies that startup check; clients do not use it. +RUN sudo -u $username /bin/bash -c 'password=$(openssl rand -hex 32); printf "%s\n%s\n" "$password" "$password" | kasmvncpasswd -wo -u "$USER"' # --------------------------------------------------------------------------- # End of VNC / desktop stack. From 7b236ce43487fffea9e23e6ddec523cfd5fd556d Mon Sep 17 00:00:00 2001 From: Andrew Berry Date: Thu, 8 Oct 2026 14:20:40 -0400 Subject: [PATCH 2/3] test: preserve parallel application coverage alongside KasmVNC checks --- tests/test.bats | 15 ++++++++++----- tests/testdata/kasmvnc.spec.ts | 5 +++-- 2 files changed, 13 insertions(+), 7 deletions(-) diff --git a/tests/test.bats b/tests/test.bats index 399e73e..8eee0ce 100644 --- a/tests/test.bats +++ b/tests/test.bats @@ -160,11 +160,16 @@ verify_run_playwright() { assert_invariant_layers_precede_browser_install - # Verify we can run an example test. The reporters come from - # playwright.config.ts (line + html); the html one leaves behind a report for - # verify_show_report to serve. - # Each browser controls the same desktop, so run them one at a time. - ddev playwright test --workers=1 + # All KasmVNC clients control the same X display. Concurrent clients would + # interleave keyboard input in IceWM's command bar, so serialize only this + # desktop-control check. The add-on does not restrict downstream workers. + ddev playwright test kasmvnc.spec.ts --workers=1 --reporter=line + + # Application tests have separate browser contexts and can run concurrently. + # Explicitly use three workers so CI exercises parallel application testing + # across Chromium, Firefox and WebKit, even when the fixture's CI default is + # one worker. Keep its line + html reporters for verify_show_report below. + ddev playwright test phpinfo.spec.ts --workers=3 verify_show_report } diff --git a/tests/testdata/kasmvnc.spec.ts b/tests/testdata/kasmvnc.spec.ts index 9ad7079..aeb42bc 100644 --- a/tests/testdata/kasmvnc.spec.ts +++ b/tests/testdata/kasmvnc.spec.ts @@ -4,8 +4,9 @@ import { existsSync, rmSync } from 'node:fs'; // Runs in Chromium, Firefox and WebKit, with no httpCredentials configured. // Loading the HTML alone misses failures in WebSocket auth or RFB negotiation. +// KasmVNC is already running (test.bats checks HTTP readiness), so connection +// setup uses the fixture's normal test and assertion timeouts. test('KasmVNC connects to the desktop without credentials', async ({ page }) => { - test.setTimeout(60_000); const url = new URL(process.env.DDEV_PRIMARY_URL!); url.port = '8444'; url.searchParams.set('autoconnect', '1'); @@ -13,7 +14,7 @@ test('KasmVNC connects to the desktop without credentials', async ({ page }) => const response = await page.goto(url.toString()); expect(response?.status()).toBe(200); expect(response?.headers()['www-authenticate']).toBeUndefined(); - await expect(page.locator('html')).toHaveClass(/noVNC_connected/, { timeout: 30_000 }); + await expect(page.locator('html')).toHaveClass(/noVNC_connected/); // A completed RFB connection must have received the desktop dimensions. const canvas = page.locator('#noVNC_container canvas').first(); From c4778ba82d537a031ea2f9f2935a5dd2fd8b5402 Mon Sep 17 00:00:00 2001 From: Andrew Berry Date: Thu, 8 Oct 2026 14:37:27 -0400 Subject: [PATCH 3/3] test: verify KasmVNC input with a focused browser target --- tests/test.bats | 2 +- tests/testdata/kasmvnc.spec.ts | 64 ++++++++++++++++++++-------------- 2 files changed, 38 insertions(+), 28 deletions(-) diff --git a/tests/test.bats b/tests/test.bats index 8eee0ce..3dd5f7e 100644 --- a/tests/test.bats +++ b/tests/test.bats @@ -161,7 +161,7 @@ verify_run_playwright() { assert_invariant_layers_precede_browser_install # All KasmVNC clients control the same X display. Concurrent clients would - # interleave keyboard input in IceWM's command bar, so serialize only this + # interleave keyboard input in the focused window, so serialize only this # desktop-control check. The add-on does not restrict downstream workers. ddev playwright test kasmvnc.spec.ts --workers=1 --reporter=line diff --git a/tests/testdata/kasmvnc.spec.ts b/tests/testdata/kasmvnc.spec.ts index aeb42bc..25591ac 100644 --- a/tests/testdata/kasmvnc.spec.ts +++ b/tests/testdata/kasmvnc.spec.ts @@ -1,40 +1,50 @@ -import { test, expect } from '@playwright/test'; +import { test, expect, chromium } from '@playwright/test'; import { randomUUID } from 'node:crypto'; -import { existsSync, rmSync } from 'node:fs'; // Runs in Chromium, Firefox and WebKit, with no httpCredentials configured. // Loading the HTML alone misses failures in WebSocket auth or RFB negotiation. // KasmVNC is already running (test.bats checks HTTP readiness), so connection // setup uses the fixture's normal test and assertion timeouts. test('KasmVNC connects to the desktop without credentials', async ({ page }) => { - const url = new URL(process.env.DDEV_PRIMARY_URL!); - url.port = '8444'; - url.searchParams.set('autoconnect', '1'); + // This headed browser receives input on KasmVNC's X display. Its DOM gives + // us a direct assertion of remote input without desktop screenshot timing. + const desktopBrowser = await chromium.launch({ headless: false, args: ['--kiosk'] }); + try { + const desktopPage = await desktopBrowser.newPage(); + await desktopPage.setContent(` + + + `); + await desktopPage.bringToFront(); + const input = desktopPage.getByRole('textbox'); + await input.focus(); - const response = await page.goto(url.toString()); - expect(response?.status()).toBe(200); - expect(response?.headers()['www-authenticate']).toBeUndefined(); - await expect(page.locator('html')).toHaveClass(/noVNC_connected/); + const url = new URL(process.env.DDEV_PRIMARY_URL!); + url.port = '8444'; + url.searchParams.set('autoconnect', '1'); - // A completed RFB connection must have received the desktop dimensions. - const canvas = page.locator('#noVNC_container canvas').first(); - await expect(canvas).toBeVisible(); - await expect.poll(() => canvas.evaluate((element: HTMLCanvasElement) => - element.width > 0 && element.height > 0 - )).toBe(true); + const response = await page.goto(url.toString()); + expect(response?.status()).toBe(200); + expect(response?.headers()['www-authenticate']).toBeUndefined(); + await expect(page.locator('html')).toHaveClass(/noVNC_connected/); - // Exercise remote keyboard input through IceWM's command bar. The marker is - // created by the remote desktop, in the same container as this test runner. - const marker = `/tmp/kasmvnc-input-${randomUUID()}`; - try { - await canvas.click({ position: { x: 10, y: 10 } }); - const desktop = await canvas.screenshot(); - await page.keyboard.press('Control+Alt+Space'); - await expect.poll(async () => (await canvas.screenshot()).equals(desktop)).toBe(false); - await page.keyboard.type(`touch ${marker}`, { delay: 20 }); - await page.keyboard.press('Enter'); - await expect.poll(() => existsSync(marker), { timeout: 10_000 }).toBe(true); + // A completed RFB connection must have received the desktop dimensions. + const canvas = page.locator('#noVNC_container canvas').first(); + await expect(canvas).toBeVisible(); + await expect.poll(() => canvas.evaluate((element: HTMLCanvasElement) => + element.width > 0 && element.height > 0 + )).toBe(true); + + // The kiosk window fills the desktop with the textarea. A remote click + // focuses it, then KasmVNC transports the keystrokes to that window. + await canvas.click(); + const text = `kasmvnc-${randomUUID()}`; + await page.keyboard.type(text, { delay: 20 }); + await expect(input).toHaveValue(text); } finally { - rmSync(marker, { force: true }); + await desktopBrowser.close(); } });