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
40 changes: 38 additions & 2 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,42 @@
}

if (this.isReadOnlyMode) {
// Disable implicit hosting so the user doesn't automatically gain control
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()
}
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
80 changes: 80 additions & 0 deletions images/chromium-headful/client/src/neko/base.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,14 @@ export interface BaseEvents {
error: (error: Error) => void
}

type IceCandidateSummary = {
candidateType?: string
protocol?: string
addressFamily?: 'ipv4' | 'ipv6' | 'unknown'
}

type IceTransportPolicy = 'all' | 'relay'

export abstract class BaseClient extends EventEmitter<BaseEvents> {
protected _ws?: WebSocket
protected _ws_heartbeat?: number
Expand All @@ -28,6 +36,9 @@ export abstract class BaseClient extends EventEmitter<BaseEvents> {
protected _state: RTCIceConnectionState = 'disconnected'
protected _id = ''
protected _candidates: RTCIceCandidate[] = []
private _localIceCandidates: IceCandidateSummary[] = []
private _remoteIceCandidates: IceCandidateSummary[] = []
private _iceTransportPolicy: IceTransportPolicy = 'all'

get id() {
return this._id
Expand Down Expand Up @@ -134,6 +145,9 @@ export abstract class BaseClient extends EventEmitter<BaseEvents> {
this._state = 'disconnected'
this._displayname = undefined
this._id = ''
this._localIceCandidates = []
this._remoteIceCandidates = []
this._iceTransportPolicy = 'all'
}

public sendData(event: 'wheel', data: { x: number; y: number; controlKey?: boolean }): void
Expand Down Expand Up @@ -217,9 +231,12 @@ export abstract class BaseClient extends EventEmitter<BaseEvents> {
}

if (lite !== true) {
this._iceTransportPolicy = this.iceTransportPolicy()
this._peer = new RTCPeerConnection({
iceServers: servers,
iceTransportPolicy: this._iceTransportPolicy,
})
this.emit('debug', `created peer with ICE transport policy: ${this._iceTransportPolicy}`)
} else {
this._peer = new RTCPeerConnection()
}
Expand Down Expand Up @@ -272,6 +289,10 @@ export abstract class BaseClient extends EventEmitter<BaseEvents> {
}

const init = event.candidate.toJSON()
const summary = this.summarizeIceCandidate(init.candidate)
if (summary) {
this._localIceCandidates.push(summary)
}
this.emit('debug', `sending local ICE candidate`, init)

this._ws!.send(
Expand Down Expand Up @@ -372,6 +393,10 @@ export abstract class BaseClient extends EventEmitter<BaseEvents> {
if (event === EVENT.SIGNAL.CANDIDATE) {
const { data } = payload as SignalCandidatePayload
const candidate: RTCIceCandidate = JSON.parse(data)
const summary = this.summarizeIceCandidate(candidate.candidate)
if (summary) {
this._remoteIceCandidates.push(summary)
}
if (this._peer) {
this._peer.addIceCandidate(candidate)
} else {
Expand All @@ -389,6 +414,35 @@ export abstract class BaseClient extends EventEmitter<BaseEvents> {
}
}

private iceTransportPolicy(): IceTransportPolicy {
const attempt = Number(new URL(location.href).searchParams.get('kernelLiveViewAttempt') || '0')
return Number.isFinite(attempt) && attempt > 0 ? 'relay' : 'all'
}

private summarizeIceCandidate(candidate: string | undefined): IceCandidateSummary | undefined {
if (!candidate) {
return undefined
}

const parts = candidate.trim().split(/\s+/)
const address = parts[4]
const typeIndex = parts.indexOf('typ')

return {
protocol: parts[2]?.toLowerCase(),
candidateType: typeIndex >= 0 ? parts[typeIndex + 1] : undefined,
addressFamily: this.addressFamily(address),
}
}

private addressFamily(address: string | undefined): 'ipv4' | 'ipv6' | 'unknown' {
if (!address) {
return 'unknown'
}

return address.includes(':') ? 'ipv6' : 'ipv4'
}

private onData(e: MessageEvent) {
this[EVENT.DATA](e.data)
}
Expand All @@ -407,6 +461,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 {}

window.parent.postMessage(message, targetOrigin)
}

private onConnected() {
if (this._timeout) {
clearTimeout(this._timeout)
Expand All @@ -424,6 +493,17 @@ 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,
iceTransportPolicy: this._iceTransportPolicy,
localCandidates: this._localIceCandidates.slice(-10),
remoteCandidates: this._remoteIceCandidates.slice(-10),
})
if (this._timeout) {
clearTimeout(this._timeout)
this._timeout = undefined
Expand Down
2 changes: 1 addition & 1 deletion server/go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -127,4 +127,4 @@ require (
modernc.org/sqlite v1.23.1 // indirect
)

replace github.com/m1k1o/neko/server => github.com/kernel/neko/server v0.0.0-20260213021128-abe9ac59a634
replace github.com/m1k1o/neko/server => github.com/kernel/neko/server v0.0.0-20260902162719-16605715b141
2 changes: 2 additions & 0 deletions server/go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -255,6 +255,8 @@ github.com/kernel/hypeman-go v0.20.0 h1:9kEMjtlko5oYSETwn9v829rJBv5GpcmoYjBjhjuw
github.com/kernel/hypeman-go v0.20.0/go.mod h1:guRrhyP9QW/ebUS1UcZ0uZLLJeGAAhDNzSi68U4M9hI=
github.com/kernel/neko/server v0.0.0-20260213021128-abe9ac59a634 h1:Pn8Zag7TMXnMPdjz136NTjpGwI7rgx++BNzsH2b4w3I=
github.com/kernel/neko/server v0.0.0-20260213021128-abe9ac59a634/go.mod h1:0+zactiySvtKwfe5JFjyNrSuQLA+EEPZl5bcfcZf1RM=
github.com/kernel/neko/server v0.0.0-20260902162719-16605715b141 h1:v+5hfR6E9qEH8xQwHYaFfcoEmWs0wJhEqGmKQ0MvPws=
github.com/kernel/neko/server v0.0.0-20260902162719-16605715b141/go.mod h1:06r88Ixwd/djXAxJTC7aLtsJ8pStQelzGoWDf0+H26E=
github.com/klauspost/compress v1.18.3 h1:9PJRvfbmTabkOX8moIpXPbMMbYN60bWImDDU7L+/6zw=
github.com/klauspost/compress v1.18.3/go.mod h1:R0h/fSBs8DE4ENlcrlib3PsXS61voFxhIs2DeRhCvJ4=
github.com/klauspost/cpuid/v2 v2.2.7 h1:ZWSB3igEs+d0qvnxR/ZBzXVmxkgt8DdzP6m9pfuVLDM=
Expand Down
Loading