Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion images/chromium-headful/Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -162,7 +162,7 @@ RUN --mount=type=cache,target=/tmp/cache/ffmpeg,sharing=locked,id=$CACHEIDPREFIX
rm -rf /tmp/ffmpeg*
EOT

FROM ghcr.io/kernel/neko/base:3.0.8-v1.6.3 AS neko
FROM ghcr.io/kernel/neko/base:3.0.8-v1.6.4-rc.1 AS neko
FROM node:22-bullseye-slim AS node-22
FROM docker.io/library/docker:29.8.2-dind AS docker
ARG TARGETARCH
Expand Down
5 changes: 5 additions & 0 deletions images/chromium-headful/client/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,11 @@ For older images without the capability, change the query parameter and reload.
If an advertised capability fails to acknowledge a request, fall back to a reload
with the desired query mode. The dashboard uses a one-second acknowledgement timeout.

Read-only links stay read-only. When the live-view proxy serves a read-only link it
sets `window.KERNEL_LIVE_VIEW_READ_ONLY = true`; the viewer then ignores the query
parameter and answers unlock requests with `readOnly: true`. The server refuses that
viewer's input regardless.

Read-only mode locks local input and disables implicit hosting. Unlocking restores
the server's configured implicit-hosting setting. Mode changes persist across
transport reconnects; a full page reload uses the URL's initial mode again.
10 changes: 6 additions & 4 deletions images/chromium-headful/client/src/app.vue
Original file line number Diff line number Diff line change
Expand Up @@ -180,7 +180,7 @@
import { Vue, Component, Ref, Watch } from 'vue-property-decorator'

import Connect from '~/components/connect.vue'
import { isReadOnlyMessage } from '~/utils/read-only'
import { isReadOnlyMessage, resolveReadOnly } from '~/utils/read-only'
import Disconnected from '~/components/disconnected.vue'
import Video from '~/components/video.vue'
import Menu from '~/components/menu.vue'
Expand Down Expand Up @@ -234,7 +234,8 @@
created() {
const params = new URL(location.href).searchParams
const value = params.get('readOnly') || params.get('readonly') || params.get('ro')
this.$accessor.remote.setReadOnly(typeof value === 'string' && ['1', 'true', 'yes'].includes(value.toLowerCase()))
const requested = typeof value === 'string' && ['1', 'true', 'yes'].includes(value.toLowerCase())
this.$accessor.remote.setReadOnly(resolveReadOnly(requested, window.KERNEL_LIVE_VIEW_READ_ONLY))
window.addEventListener('message', this.onParentMessage)
}

Expand All @@ -245,11 +246,12 @@
onParentMessage(event: MessageEvent) {
if (window.parent === window || !isReadOnlyMessage(event, window.parent, this.parentOrigin)) return

if (event.data.readOnly && !this.isReadOnlyMode) {
const readOnly = resolveReadOnly(event.data.readOnly, window.KERNEL_LIVE_VIEW_READ_ONLY)
if (readOnly && !this.isReadOnlyMode) {
if (this.video) this.video.releaseInput()
this.$accessor.remote.release()
}
this.$accessor.remote.setReadOnly(event.data.readOnly)
this.$accessor.remote.setReadOnly(readOnly)
window.parent.postMessage(
{ type: 'KERNEL_READ_ONLY_CHANGED', readOnly: this.isReadOnlyMode, requestId: event.data.requestId },
this.parentOrigin,
Expand Down
13 changes: 13 additions & 0 deletions images/chromium-headful/client/src/utils/read-only.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,10 @@
declare global {
interface Window {
// Injected by the live-view proxy for read-only links.
KERNEL_LIVE_VIEW_READ_ONLY?: unknown
}
}

export function isReadOnlyMessage(event: MessageEvent, parent: Window, parentOrigin: string): boolean {
return (
event.source === parent &&
Expand All @@ -11,3 +18,9 @@ export function isReadOnlyMessage(event: MessageEvent, parent: Window, parentOri
(event.data.requestId === undefined || typeof event.data.requestId === 'string')
)
}

// A read-only link stays read-only: the server refuses its input either way, so
// the page neither unlocks nor offers controls it cannot use.
export function resolveReadOnly(requested: boolean, forced: unknown): boolean {
return forced === true || requested
}
16 changes: 15 additions & 1 deletion images/chromium-headful/client/tests/read-only.test.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { describe, expect, mock, test } from 'bun:test'
import { isReadOnlyMessage } from '../src/utils/read-only'
import { isReadOnlyMessage, resolveReadOnly } from '../src/utils/read-only'

const accessor = { connected: true, remote: { setKeyboardModifierState: mock() } }
mock.module('~/store', () => ({ accessor }))
Expand Down Expand Up @@ -52,6 +52,20 @@ describe('read-only parent messages', () => {
})
})

describe('read-only links', () => {
test('keep read-only regardless of the requested mode', () => {
expect(resolveReadOnly(false, true)).toBe(true)
expect(resolveReadOnly(true, true)).toBe(true)
})

test('follow the requested mode when the proxy does not force it', () => {
for (const forced of [undefined, false, 'true', 1]) {
expect(resolveReadOnly(false, forced)).toBe(false)
expect(resolveReadOnly(true, forced)).toBe(true)
}
})
})

describe('read-only input state', () => {
test('locks and unlocks without resetting the current controller', () => {
const remote = state()
Expand Down
11 changes: 6 additions & 5 deletions images/chromium-headful/neko.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -22,17 +22,18 @@ member:
can_access_clipboard: true
sends_inactive_cursor: true
can_see_inactive_cursors: true
user_password: "neko"
# View-only member, used by the live-view proxy for read-only links.
user_password: "live-view-read-only"
user_profile:
name: "" # if empty, the login username is used
is_admin: false
can_login: true
can_connect: true
can_watch: true
can_host: true
can_share_media: true
can_access_clipboard: true
sends_inactive_cursor: true
can_host: false
can_share_media: false
can_access_clipboard: false
sends_inactive_cursor: false
can_see_inactive_cursors: false

session:
Expand Down
Loading