Implicit control gain (#108)

* add client side implicit hosting.

* add server side implicit hosting.

* update changelog.

* allow clipboard & keybaord access.
This commit is contained in:
Miroslav Šedivý 2021-12-11 14:34:28 +01:00 committed by GitHub
parent f08ed0fc28
commit 7d1fa28d88
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
21 changed files with 125 additions and 38 deletions

View File

@ -18,20 +18,20 @@
<span @click="mute(child.data.member)" v-if="!child.data.member.muted">{{ $t('context.mute') }}</span> <span @click="mute(child.data.member)" v-if="!child.data.member.muted">{{ $t('context.mute') }}</span>
<span @click="unmute(child.data.member)" v-else>{{ $t('context.unmute') }}</span> <span @click="unmute(child.data.member)" v-else>{{ $t('context.unmute') }}</span>
</li> </li>
<li v-if="child.data.member.id === host"> <li v-if="child.data.member.id === host && !implicitHosting">
<span @click="adminRelease(child.data.member)">{{ $t('context.release') }}</span> <span @click="adminRelease(child.data.member)">{{ $t('context.release') }}</span>
</li> </li>
<li v-if="child.data.member.id === host"> <li v-if="child.data.member.id === host && !implicitHosting">
<span @click="adminControl(child.data.member)">{{ $t('context.take') }}</span> <span @click="adminControl(child.data.member)">{{ $t('context.take') }}</span>
</li> </li>
<li> <li>
<span v-if="child.data.member.id !== host" @click="adminGive(child.data.member)">{{ <span v-if="child.data.member.id !== host && !implicitHosting" @click="adminGive(child.data.member)">{{
$t('context.give') $t('context.give')
}}</span> }}</span>
</li> </li>
</template> </template>
<template v-else> <template v-else>
<li v-if="hosting"> <li v-if="hosting && !implicitHosting">
<span @click="give(child.data.member)">{{ $t('context.give') }}</span> <span @click="give(child.data.member)">{{ $t('context.give') }}</span>
</li> </li>
</template> </template>
@ -161,6 +161,10 @@
return this.$accessor.remote.id return this.$accessor.remote.id
} }
get implicitHosting() {
return this.$accessor.remote.implicitHosting
}
open(event: MouseEvent, data: any) { open(event: MouseEvent, data: any) {
this.context.open(event, data) this.context.open(event, data)
} }

View File

@ -1,6 +1,6 @@
<template> <template>
<ul> <ul>
<li v-if="seesControl"> <li v-if="!implicitHosting && (!controlLocked || hosting)">
<i <i
:class="[ :class="[
!disabeld && shakeKbd ? 'shake' : '', !disabeld && shakeKbd ? 'shake' : '',
@ -20,7 +20,19 @@
@click.stop.prevent="toggleControl" @click.stop.prevent="toggleControl"
/> />
</li> </li>
<li v-if="seesControl"> <li class="no-pointer" v-if="implicitHosting">
<i
:class="[controlLocked ? 'disabled' : '', 'fas', 'fa-mouse-pointer']"
v-tooltip="{
content: controlLocked ? $t('controls.hasnot') : $t('controls.has'),
placement: 'top',
offset: 5,
boundariesElement: 'body',
delay: { show: 300, hide: 100 },
}"
/>
</li>
<li v-if="implicitHosting || (!implicitHosting && (!controlLocked || hosting))">
<label <label
class="switch" class="switch"
v-tooltip="{ v-tooltip="{
@ -31,7 +43,7 @@
delay: { show: 300, hide: 100 }, delay: { show: 300, hide: 100 },
}" }"
> >
<input type="checkbox" v-model="locked" :disabled="!hosting" /> <input type="checkbox" v-model="locked" :disabled="!hosting || (implicitHosting && controlLocked)" />
<span /> <span />
</label> </label>
</li> </li>
@ -105,6 +117,10 @@
font-size: 24px; font-size: 24px;
cursor: pointer; cursor: pointer;
&.no-pointer {
cursor: default;
}
i { i {
padding: 0 5px; padding: 0 5px;
@ -242,12 +258,8 @@
export default class extends Vue { export default class extends Vue {
@Prop(Boolean) readonly shakeKbd!: boolean @Prop(Boolean) readonly shakeKbd!: boolean
get severLocked(): boolean { get controlLocked() {
return 'control' in this.$accessor.locked && this.$accessor.locked['control'] return 'control' in this.$accessor.locked && this.$accessor.locked['control'] && !this.$accessor.user.admin
}
get seesControl(): boolean {
return !this.severLocked || this.$accessor.user.admin || this.hosting
} }
get disabeld() { get disabeld() {
@ -258,6 +270,10 @@
return this.$accessor.remote.hosting return this.$accessor.remote.hosting
} }
get implicitHosting() {
return this.$accessor.remote.implicitHosting
}
get volume() { get volume() {
return this.$accessor.video.volume return this.$accessor.video.volume
} }

View File

@ -240,6 +240,10 @@
return this.$accessor.remote.hosting return this.$accessor.remote.hosting
} }
get implicitHosting() {
return this.$accessor.remote.implicitHosting
}
get hosted() { get hosted() {
return this.$accessor.remote.hosted return this.$accessor.remote.hosted
} }
@ -272,8 +276,13 @@
return this.$accessor.settings.autoplay return this.$accessor.settings.autoplay
} }
// server-side lock
get controlLocked() {
return 'control' in this.$accessor.locked && this.$accessor.locked['control'] && !this.$accessor.user.admin
}
get locked() { get locked() {
return this.$accessor.remote.locked return this.$accessor.remote.locked || (this.controlLocked && (!this.hosting || this.implicitHosting))
} }
get scroll() { get scroll() {

View File

@ -49,6 +49,8 @@ export const controls = {
request: 'Request Controls', request: 'Request Controls',
lock: 'Lock Controls', lock: 'Lock Controls',
unlock: 'Unlock Controls', unlock: 'Unlock Controls',
has: 'You have control',
hasnot: 'You do not have control',
} }
export const locks = { export const locks = {

View File

@ -51,6 +51,9 @@ export const controls = {
request: 'Controles solicitados', request: 'Controles solicitados',
lock: 'Controles bloqueados', lock: 'Controles bloqueados',
unlock: 'Controles desbloqueados', unlock: 'Controles desbloqueados',
// TODO
//has: 'You have control',
//hasnot: 'You do not have control',
} }
export const locks = { export const locks = {

View File

@ -46,6 +46,16 @@ export const context = {
}, },
} }
export const controls = {
release: 'Relacher le contrôle',
request: 'Demander le contrôle',
lock: 'Vérouiller le contrôle',
unlock: 'Débloquer le contrôle',
// TODO
// has: 'You have control',
// hasnot: 'You do not have control',
}
export const locks = { export const locks = {
// TODO // TODO
//control: { //control: {
@ -57,20 +67,13 @@ export const locks = {
// notif_unlocked: 'unlocked controls for users', // notif_unlocked: 'unlocked controls for users',
//}, //},
login: { login: {
release: 'Relacher le contrôle',
request: 'Demander le contrôle',
lock: 'Vérouiller le contrôle',
unlock: 'Débloquer le contrôle',
notif_locked: 'a vérouillé la salle',
notif_unlocked: 'a dévérouillé la salle',
},
}
export const room = {
lock: 'Vérouiller la salle (pour les utilisateurs)', lock: 'Vérouiller la salle (pour les utilisateurs)',
unlock: 'Dévérouiller la salle (pour les utilisateurs)', unlock: 'Dévérouiller la salle (pour les utilisateurs)',
locked: 'Salle vérouillée (pour les utilisateurs)', locked: 'Salle vérouillée (pour les utilisateurs)',
unlocked: 'Salle dévérouillée (pour les utilisateurs)', unlocked: 'Salle dévérouillée (pour les utilisateurs)',
notif_locked: 'a vérouillé la salle',
notif_unlocked: 'a dévérouillé la salle',
},
} }
export const setting = { export const setting = {

View File

@ -51,6 +51,9 @@ export const controls = {
request: 'Forespør kontroll', request: 'Forespør kontroll',
lock: 'Lås kontrollen', lock: 'Lås kontrollen',
unlock: 'Lås opp kontrollen', unlock: 'Lås opp kontrollen',
// TODO
//has: 'You have control',
//hasnot: 'You do not have control',
} }
export const locks = { export const locks = {

View File

@ -51,6 +51,9 @@ export const controls = {
request: 'Požiadať o ovládanie', request: 'Požiadať o ovládanie',
lock: 'Zamknúť ovládanie', lock: 'Zamknúť ovládanie',
unlock: 'Odomknúť ovládanie', unlock: 'Odomknúť ovládanie',
// TODO
//has: 'You have control',
//hasnot: 'You do not have control',
} }
export const locks = { export const locks = {

View File

@ -51,6 +51,9 @@ export const controls = {
request: 'Fråga om kontroll', request: 'Fråga om kontroll',
lock: 'Lås kontrollen', lock: 'Lås kontrollen',
unlock: 'Lås upp kontrollen', unlock: 'Lås upp kontrollen',
// TODO
//has: 'You have control',
//hasnot: 'You do not have control',
} }
export const locks = { export const locks = {

View File

@ -18,13 +18,13 @@ export const state = () => ({
export const getters = getterTree(state, { export const getters = getterTree(state, {
hosting: (state, getters, root) => { hosting: (state, getters, root) => {
return root.user.id === state.id return root.user.id === state.id || state.implicitHosting
}, },
hosted: (state, getters, root) => { hosted: (state, getters, root) => {
return state.id !== '' return state.id !== '' || state.implicitHosting
}, },
host: (state, getters, root) => { host: (state, getters, root) => {
return root.user.members[state.id] || null return root.user.members[state.id] || (state.implicitHosting && root.user.id) || null
}, },
}) })
@ -57,8 +57,6 @@ export const mutations = mutationTree(state, {
state.id = '' state.id = ''
state.clipboard = '' state.clipboard = ''
state.locked = false state.locked = false
state.implicitHosting = false
state.keyboardModifierState = -1
}, },
}) })

View File

@ -5,6 +5,7 @@
### New Features ### New Features
- Added `m1k1o/neko:microsoft-edge` tag. - Added `m1k1o/neko:microsoft-edge` tag.
- Fixed clipboard sync in chromium based browsers. - Fixed clipboard sync in chromium based browsers.
- Added support for implicit control (using `NEKO_IMPLICITCONTROL=1`). That means, users do not need to request control prior usage.
### Misc ### Misc
- Automatic WebRTC SDP negotiation using onnegotiationneeded handlers. This allows adding/removing track on demand in a session. - Automatic WebRTC SDP negotiation using onnegotiationneeded handlers. This allows adding/removing track on demand in a session.

View File

@ -29,6 +29,8 @@ type SessionManager struct {
remote types.RemoteManager remote types.RemoteManager
members map[string]*Session members map[string]*Session
emmiter events.EventEmmiter emmiter events.EventEmmiter
// TODO: Handle locks in sessions as flags.
controlLocked bool
} }
func (manager *SessionManager) New(id string, admin bool, socket types.WebSocket) types.Session { func (manager *SessionManager) New(id string, admin bool, socket types.WebSocket) types.Session {
@ -104,6 +106,16 @@ func (manager *SessionManager) Get(id string) (types.Session, bool) {
return session, ok return session, ok
} }
// TODO: Handle locks in sessions as flags.
func (manager *SessionManager) SetControlLocked(locked bool) {
manager.controlLocked = locked
}
func (manager *SessionManager) CanControl(id string) bool {
session, ok := manager.Get(id)
return ok && (!manager.controlLocked || session.Admin())
}
func (manager *SessionManager) Admins() []*types.Member { func (manager *SessionManager) Admins() []*types.Member {
manager.mu.Lock() manager.mu.Lock()
defer manager.mu.Unlock() defer manager.mu.Unlock()

View File

@ -22,6 +22,8 @@ type WebRTC struct {
NAT1To1IPs []string NAT1To1IPs []string
TCPMUX int TCPMUX int
UDPMUX int UDPMUX int
ImplicitControl bool
} }
func (WebRTC) Init(cmd *cobra.Command) error { func (WebRTC) Init(cmd *cobra.Command) error {
@ -65,6 +67,12 @@ func (WebRTC) Init(cmd *cobra.Command) error {
return err return err
} }
// TODO: Should be moved to session config.
cmd.PersistentFlags().Bool("implicitcontrol", false, "if enabled members can gain control implicitly")
if err := viper.BindPFlag("implicitcontrol", cmd.PersistentFlags().Lookup("implicitcontrol")); err != nil {
return err
}
return nil return nil
} }
@ -120,4 +128,7 @@ func (s *WebRTC) Set() {
s.EphemeralMin = min s.EphemeralMin = min
s.EphemeralMax = max s.EphemeralMax = max
} }
// TODO: Should be moved to session config.
s.ImplicitControl = viper.GetBool("implicitcontrol")
} }

View File

@ -38,6 +38,8 @@ type SessionManager interface {
ClearHost() ClearHost()
Has(id string) bool Has(id string) bool
Get(id string) (Session, bool) Get(id string) (Session, bool)
SetControlLocked(locked bool)
CanControl(id string) bool
Members() []*Member Members() []*Member
Admins() []*Member Admins() []*Member
Destroy(id string) Destroy(id string)

View File

@ -13,6 +13,7 @@ type WebRTCManager interface {
CreatePeer(id string, session Session) (Peer, error) CreatePeer(id string, session Session) (Peer, error)
ICELite() bool ICELite() bool
ICEServers() []webrtc.ICEServer ICEServers() []webrtc.ICEServer
ImplicitControl() bool
} }
type Peer interface { type Peer interface {

View File

@ -39,7 +39,7 @@ type PayloadKey struct {
} }
func (manager *WebRTCManager) handle(id string, msg webrtc.DataChannelMessage) error { func (manager *WebRTCManager) handle(id string, msg webrtc.DataChannelMessage) error {
if !manager.sessions.IsHost(id) { if (!manager.config.ImplicitControl && !manager.sessions.IsHost(id)) || (manager.config.ImplicitControl && !manager.sessions.CanControl(id)) {
return nil return nil
} }

View File

@ -301,6 +301,10 @@ func (manager *WebRTCManager) ICEServers() []webrtc.ICEServer {
return manager.config.ICEServers return manager.config.ICEServers
} }
func (manager *WebRTCManager) ImplicitControl() bool {
return manager.config.ImplicitControl
}
func (manager *WebRTCManager) createTrack(codecName string) (*webrtc.TrackLocalStaticSample, webrtc.RTPCodecParameters, error) { func (manager *WebRTCManager) createTrack(codecName string) (*webrtc.TrackLocalStaticSample, webrtc.RTPCodecParameters, error) {
var codec webrtc.RTPCodecParameters var codec webrtc.RTPCodecParameters

View File

@ -25,6 +25,11 @@ func (h *MessageHandler) adminLock(id string, session types.Session, payload *me
return nil return nil
} }
// TODO: Handle locks in sessions as flags.
if payload.Resource == "control" {
h.sessions.SetControlLocked(true)
}
h.locked[payload.Resource] = id h.locked[payload.Resource] = id
if err := h.sessions.Broadcast( if err := h.sessions.Broadcast(
@ -52,6 +57,11 @@ func (h *MessageHandler) adminUnlock(id string, session types.Session, payload *
return nil return nil
} }
// TODO: Handle locks in sessions as flags.
if payload.Resource == "control" {
h.sessions.SetControlLocked(false)
}
delete(h.locked, payload.Resource) delete(h.locked, payload.Resource)
if err := h.sessions.Broadcast( if err := h.sessions.Broadcast(

View File

@ -125,9 +125,9 @@ func (h *MessageHandler) controlGive(id string, session types.Session, payload *
} }
func (h *MessageHandler) controlClipboard(id string, session types.Session, payload *message.Clipboard) error { func (h *MessageHandler) controlClipboard(id string, session types.Session, payload *message.Clipboard) error {
// check if session is host // check if session can access clipboard
if !h.sessions.IsHost(id) { if (!h.webrtc.ImplicitControl() && !h.sessions.IsHost(id)) || (h.webrtc.ImplicitControl() && !h.sessions.CanControl(id)) {
h.logger.Debug().Str("id", id).Msg("is not the host") h.logger.Debug().Str("id", id).Msg("cannot access clipboard")
return nil return nil
} }
@ -136,9 +136,9 @@ func (h *MessageHandler) controlClipboard(id string, session types.Session, payl
} }
func (h *MessageHandler) controlKeyboard(id string, session types.Session, payload *message.Keyboard) error { func (h *MessageHandler) controlKeyboard(id string, session types.Session, payload *message.Keyboard) error {
// check if session is host // check if session can control keyboard
if !h.sessions.IsHost(id) { if (!h.webrtc.ImplicitControl() && !h.sessions.IsHost(id)) || (h.webrtc.ImplicitControl() && !h.sessions.CanControl(id)) {
h.logger.Debug().Str("id", id).Msg("is not the host") h.logger.Debug().Str("id", id).Msg("cannot control keyboard")
return nil return nil
} }

View File

@ -15,7 +15,7 @@ func (h *MessageHandler) SessionCreated(id string, session types.Session) error
// send initialization information // send initialization information
if err := session.Send(message.SystemInit{ if err := session.Send(message.SystemInit{
Event: event.SYSTEM_INIT, Event: event.SYSTEM_INIT,
ImplicitHosting: true, ImplicitHosting: h.webrtc.ImplicitControl(),
Locks: h.locked, Locks: h.locked,
}); err != nil { }); err != nil {
h.logger.Warn().Str("id", id).Err(err).Msgf("sending event %s has failed", event.SYSTEM_INIT) h.logger.Warn().Str("id", id).Err(err).Msgf("sending event %s has failed", event.SYSTEM_INIT)

View File

@ -105,6 +105,7 @@ func (ws *WebSocketHandler) Start() {
sess, ok := ws.handler.locked["control"] sess, ok := ws.handler.locked["control"]
if ok && ws.conf.ControlProtection && sess == CONTROL_PROTECTION_SESSION && len(ws.sessions.Admins()) > 0 { if ok && ws.conf.ControlProtection && sess == CONTROL_PROTECTION_SESSION && len(ws.sessions.Admins()) > 0 {
delete(ws.handler.locked, "control") delete(ws.handler.locked, "control")
ws.sessions.SetControlLocked(false) // TODO: Handle locks in sessions as flags.
ws.logger.Info().Msgf("control unlocked on behalf of control protection") ws.logger.Info().Msgf("control unlocked on behalf of control protection")
if err := ws.sessions.Broadcast( if err := ws.sessions.Broadcast(
@ -140,6 +141,7 @@ func (ws *WebSocketHandler) Start() {
_, ok := ws.handler.locked["control"] _, ok := ws.handler.locked["control"]
if !ok && ws.conf.ControlProtection && adminCount == 0 { if !ok && ws.conf.ControlProtection && adminCount == 0 {
ws.handler.locked["control"] = CONTROL_PROTECTION_SESSION ws.handler.locked["control"] = CONTROL_PROTECTION_SESSION
ws.sessions.SetControlLocked(true) // TODO: Handle locks in sessions as flags.
ws.logger.Info().Msgf("control locked and released on behalf of control protection") ws.logger.Info().Msgf("control locked and released on behalf of control protection")
ws.handler.adminRelease(id, session) ws.handler.adminRelease(id, session)