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
38 changes: 38 additions & 0 deletions images/chromium-headful/client/src/app.vue
Original file line number Diff line number Diff line change
Expand Up @@ -209,6 +209,7 @@

shakeKbd = false
wasConnected = false
readOnlyOverride: boolean | null = null

get volume() {
const numberParam = parseFloat(new URL(location.href).searchParams.get('volume') || '1.0')
Expand All @@ -224,6 +225,10 @@
}

get isReadOnlyMode() {
if (this.readOnlyOverride !== null) {
return this.readOnlyOverride
}

const params = new URL(location.href).searchParams
const value = params.get('readOnly') || params.get('readonly') || params.get('ro')
return typeof value === 'string' && ['1', 'true', 'yes'].includes(value.toLowerCase())
Expand Down Expand Up @@ -326,11 +331,44 @@
}

if (this.isReadOnlyMode) {
this.applyReadOnlyMode(true, false)
}
}

mounted() {
window.addEventListener('message', this.onParentMessage)
}

beforeDestroy() {
window.removeEventListener('message', this.onParentMessage)
}

private onParentMessage(event: MessageEvent) {
if (event.source !== window.parent) return
if (this.parentOrigin !== '*' && event.origin !== this.parentOrigin) return

const data = event.data as { type?: string; readOnly?: unknown }
if (data?.type !== 'KERNEL_SET_READ_ONLY' || typeof data.readOnly !== 'boolean') return

this.applyReadOnlyMode(data.readOnly, true)
}

private applyReadOnlyMode(readOnly: boolean, releaseControl: boolean) {
this.readOnlyOverride = readOnly

if (readOnly) {
if (releaseControl) {
this.$accessor.remote.release()
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Parent lock loses local lock

Medium Severity

Parent-frame lock calls release and then setLocked(true), but the control/release handler runs remote.reset(), which clears locked. The local lock that is supposed to block input even if hosting is granted again does not persist, so a later control/locked can restore input while the embedder still believes the view is read-only.

Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit 8c850ad. Configure here.

// Disable implicit hosting so the user doesn't automatically gain control
this.$accessor.remote.setImplicitHosting(false)
// Lock the session locally to block any input even if hosting is later requested
this.$accessor.remote.setLocked(true)
return
}

this.$accessor.remote.setLocked(false)
this.$accessor.remote.setImplicitHosting(true)
}

// KERNEL: end custom resolution, frame rate, and readOnly control via query params
Expand Down
23 changes: 23 additions & 0 deletions images/chromium-headful/client/src/neko/base.ts
Original file line number Diff line number Diff line change
Expand Up @@ -407,6 +407,21 @@ export abstract class BaseClient extends EventEmitter<BaseEvents> {
this.emit('error', (event as ErrorEvent).error)
}

private postParentMessage(message: Record<string, unknown>) {
if (window.parent === window) {
return
}

let targetOrigin = '*'
try {
if (document.referrer) {
targetOrigin = new URL(document.referrer).origin
}
} catch (e) {}

window.parent.postMessage(message, targetOrigin)
}

private onConnected() {
if (this._timeout) {
clearTimeout(this._timeout)
Expand All @@ -424,6 +439,14 @@ export abstract class BaseClient extends EventEmitter<BaseEvents> {

private onTimeout() {
this.emit('debug', `connection timeout`)
this.postParentMessage({
type: 'KERNEL_CONNECTION_TIMEOUT',
reason: 'connection timeout',
iceConnectionState: this._peer?.iceConnectionState ?? this._state,
connectionState: this._peer?.connectionState,
signalingState: this._peer?.signalingState,
socketOpen: this.socketOpen,
})
if (this._timeout) {
clearTimeout(this._timeout)
this._timeout = undefined
Expand Down
Loading