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
6 changes: 4 additions & 2 deletions server/internal/session/session.go
Original file line number Diff line number Diff line change
Expand Up @@ -75,8 +75,10 @@ func (session *SessionCtx) IsHost() bool {

// only needed for legacy webrtc handler
func (session *SessionCtx) LegacyIsHost() bool {
implicitHosting := session.manager.Settings().ImplicitHosting
return !(!implicitHosting && !session.manager.isHost(session)) || (implicitHosting && !session.profile.CanHost)
if !session.profile.CanHost {
return false
}
return session.manager.Settings().ImplicitHosting || session.manager.isHost(session)
}

func (session *SessionCtx) SetAsHost() {
Expand Down
41 changes: 41 additions & 0 deletions server/internal/session/session_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
package session

import (
"testing"

"github.com/m1k1o/neko/server/internal/config"
"github.com/m1k1o/neko/server/pkg/types"
)

func TestLegacyIsHost(t *testing.T) {
tests := []struct {
name string
implicitHosting bool
canHost bool
setHost bool
want bool
}{
{name: "implicit hosting, can host", implicitHosting: true, canHost: true, want: true},
{name: "implicit hosting, cannot host", implicitHosting: true, canHost: false, want: false},
{name: "explicit hosting, host", canHost: true, setHost: true, want: true},
{name: "explicit hosting, not host", canHost: true, want: false},
{name: "explicit hosting, cannot host", canHost: false, want: false},
}

for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
manager := New(&config.Session{ImplicitHosting: tt.implicitHosting})
session, _, err := manager.Create("member", types.MemberProfile{CanHost: tt.canHost})
if err != nil {
t.Fatalf("create session: %v", err)
}
if tt.setHost {
session.SetAsHost()
}

if got := session.LegacyIsHost(); got != tt.want {
t.Errorf("LegacyIsHost() = %v, want %v", got, tt.want)
}
})
}
}
70 changes: 70 additions & 0 deletions server/internal/webrtc/legacyhandler_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
package webrtc

import (
"bytes"
"encoding/binary"
"testing"

"github.com/rs/zerolog"

"github.com/m1k1o/neko/server/internal/config"
"github.com/m1k1o/neko/server/internal/session"
"github.com/m1k1o/neko/server/pkg/types"
)

type recordingDesktop struct {
types.DesktopManager
calls []string
}

func (d *recordingDesktop) Move(x, y int) { d.calls = append(d.calls, "Move") }
func (d *recordingDesktop) KeyDown(code uint32) error {
d.calls = append(d.calls, "KeyDown")
return nil
}
func (d *recordingDesktop) ButtonDown(code uint32) error {
d.calls = append(d.calls, "ButtonDown")
return nil
}

func legacyPayload(t *testing.T, payload any) []byte {
t.Helper()
buffer := &bytes.Buffer{}
if err := binary.Write(buffer, binary.LittleEndian, payload); err != nil {
t.Fatalf("encode payload: %v", err)
}
return buffer.Bytes()
}

func TestHandleLegacyInput(t *testing.T) {
inputs := map[string]any{
"move": PayloadMove{PayloadHeader: PayloadHeader{Event: OP_MOVE, Length: 4}, X: 10, Y: 10},
"key down": PayloadKey{PayloadHeader: PayloadHeader{Event: OP_KEY_DOWN, Length: 8}, Key: 0x61},
"button down": PayloadKey{PayloadHeader: PayloadHeader{Event: OP_KEY_DOWN, Length: 8}, Key: 1},
}

for _, canHost := range []bool{false, true} {
for name, input := range inputs {
t.Run(name, func(t *testing.T) {
sessions := session.New(&config.Session{ImplicitHosting: true})
sess, _, err := sessions.Create("member", types.MemberProfile{CanHost: canHost})
if err != nil {
t.Fatalf("create session: %v", err)
}
desktop := &recordingDesktop{}
manager := &WebRTCManagerCtx{desktop: desktop}

if err := manager.handleLegacy(zerolog.Nop(), legacyPayload(t, input), sess); err != nil {
t.Fatalf("handleLegacy: %v", err)
}

if canHost && len(desktop.calls) != 1 {
t.Errorf("can host: desktop calls = %v, want one", desktop.calls)
}
if !canHost && len(desktop.calls) != 0 {
t.Errorf("cannot host: desktop calls = %v, want none", desktop.calls)
}
})
}
}
}
9 changes: 4 additions & 5 deletions server/internal/websocket/handler/clipboard.go
Original file line number Diff line number Diff line change
@@ -1,17 +1,16 @@
package handler

import (
// "errors"
"errors"

"github.com/m1k1o/neko/server/pkg/types"
"github.com/m1k1o/neko/server/pkg/types/message"
)

func (h *MessageHandlerCtx) clipboardSet(session types.Session, payload *message.ClipboardData) error {
// Disabled: profile check
// if !session.Profile().CanAccessClipboard {
// return errors.New("cannot access clipboard")
// }
if !session.Profile().CanHost || !session.Profile().CanAccessClipboard {
return errors.New("cannot access clipboard")
}

// Disabled: is host check
// if !session.IsHost() {
Expand Down
7 changes: 3 additions & 4 deletions server/internal/websocket/handler/control.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,10 +17,9 @@ var (
)

func (h *MessageHandlerCtx) controlRelease(session types.Session) error {
// Disabled: profile check
// if !session.Profile().CanHost || session.PrivateModeEnabled() {
// return ErrIsNotAllowedToHost
// }
if !session.Profile().CanHost {
return ErrIsNotAllowedToHost
}

// Disabled: is host check
// if !session.IsHost() {
Expand Down
147 changes: 147 additions & 0 deletions server/internal/websocket/handler/control_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,147 @@
package handler

import (
"errors"
"testing"

"github.com/m1k1o/neko/server/internal/config"
"github.com/m1k1o/neko/server/internal/session"
"github.com/m1k1o/neko/server/pkg/types"
"github.com/m1k1o/neko/server/pkg/types/message"
)

// recordingDesktop records the desktop calls a handler makes. Methods a test
// does not expect are left to the embedded nil interface and panic if called.
type recordingDesktop struct {
types.DesktopManager
calls []string
}

func (d *recordingDesktop) Move(x, y int) { d.calls = append(d.calls, "Move") }
func (d *recordingDesktop) KeyPress(codes ...uint32) error {
d.calls = append(d.calls, "KeyPress")
return nil
}
func (d *recordingDesktop) ResetKeys() { d.calls = append(d.calls, "ResetKeys") }
func (d *recordingDesktop) SetKeyboardMap(types.KeyboardMap) error {
d.calls = append(d.calls, "SetKeyboardMap")
return nil
}
func (d *recordingDesktop) SetKeyboardModifiers(types.KeyboardModifiers) {
d.calls = append(d.calls, "SetKeyboardModifiers")
}
func (d *recordingDesktop) ClipboardSetText(types.ClipboardText) error {
d.calls = append(d.calls, "ClipboardSetText")
return nil
}

type recordingWebRTC struct {
types.WebRTCManager
}

func (recordingWebRTC) SetCursorPosition(x, y int) {}

var (
hostProfile = types.MemberProfile{CanLogin: true, CanConnect: true, CanWatch: true, CanHost: true, CanAccessClipboard: true}
viewProfile = types.MemberProfile{CanLogin: true, CanConnect: true, CanWatch: true}
)

func newTestHandler(t *testing.T, profile types.MemberProfile) (*MessageHandlerCtx, *recordingDesktop, types.Session) {
t.Helper()
sessions := session.New(&config.Session{ImplicitHosting: true})
sess, _, err := sessions.Create("member", profile)
if err != nil {
t.Fatalf("create session: %v", err)
}
desktop := &recordingDesktop{}
return New(sessions, desktop, nil, recordingWebRTC{}), desktop, sess
}

func TestNonHostSessionsCannotControl(t *testing.T) {
actions := map[string]func(h *MessageHandlerCtx, s types.Session) error{
"control request": func(h *MessageHandlerCtx, s types.Session) error { return h.controlRequest(s) },
"control release": func(h *MessageHandlerCtx, s types.Session) error { return h.controlRelease(s) },
"move": func(h *MessageHandlerCtx, s types.Session) error {
return h.controlMove(s, &message.ControlPos{X: 1, Y: 1})
},
"key press": func(h *MessageHandlerCtx, s types.Session) error {
return h.controlKeyPress(s, &message.ControlKey{Keysym: 0x61})
},
"clipboard set": func(h *MessageHandlerCtx, s types.Session) error {
return h.clipboardSet(s, &message.ClipboardData{Text: "text"})
},
"keyboard map": func(h *MessageHandlerCtx, s types.Session) error {
return h.keyboardMap(s, &message.KeyboardMap{KeyboardMap: types.KeyboardMap{Layout: "de"}})
},
"keyboard modifiers": func(h *MessageHandlerCtx, s types.Session) error {
capsLock := true
return h.keyboardModifiers(s, &message.KeyboardModifiers{KeyboardModifiers: types.KeyboardModifiers{CapsLock: &capsLock}})
},
}

for name, action := range actions {
t.Run(name, func(t *testing.T) {
h, desktop, sess := newTestHandler(t, viewProfile)
if err := action(h, sess); err == nil {
t.Fatal("expected the action to be refused")
}
if len(desktop.calls) != 0 {
t.Errorf("desktop calls = %v, want none", desktop.calls)
}
if sess.IsHost() {
t.Error("session became host")
}
})
}
}

func TestHostSessionsCanControl(t *testing.T) {
tests := []struct {
name string
action func(h *MessageHandlerCtx, s types.Session) error
want string
}{
{"move", func(h *MessageHandlerCtx, s types.Session) error {
return h.controlMove(s, &message.ControlPos{X: 1, Y: 1})
}, "Move"},
{"key press", func(h *MessageHandlerCtx, s types.Session) error {
return h.controlKeyPress(s, &message.ControlKey{Keysym: 0x61})
}, "KeyPress"},
{"clipboard set", func(h *MessageHandlerCtx, s types.Session) error {
return h.clipboardSet(s, &message.ClipboardData{Text: "text"})
}, "ClipboardSetText"},
{"keyboard map", func(h *MessageHandlerCtx, s types.Session) error {
return h.keyboardMap(s, &message.KeyboardMap{KeyboardMap: types.KeyboardMap{Layout: "de"}})
}, "SetKeyboardMap"},
{"keyboard modifiers", func(h *MessageHandlerCtx, s types.Session) error {
capsLock := true
return h.keyboardModifiers(s, &message.KeyboardModifiers{KeyboardModifiers: types.KeyboardModifiers{CapsLock: &capsLock}})
}, "SetKeyboardModifiers"},
{"control release", func(h *MessageHandlerCtx, s types.Session) error { return h.controlRelease(s) }, "ResetKeys"},
}

for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
h, desktop, sess := newTestHandler(t, hostProfile)
if err := tt.action(h, sess); err != nil {
t.Fatalf("action failed: %v", err)
}
if len(desktop.calls) == 0 || desktop.calls[len(desktop.calls)-1] != tt.want {
t.Errorf("desktop calls = %v, want last call %s", desktop.calls, tt.want)
}
})
}
}

func TestControlRequestSetsHostForHostProfile(t *testing.T) {
h, _, sess := newTestHandler(t, hostProfile)
if err := h.controlRequest(sess); err != nil {
t.Fatalf("control request: %v", err)
}
if !sess.IsHost() {
t.Error("session is not host")
}
if err := h.controlRequest(sess); !errors.Is(err, ErrIsAlreadyTheHost) {
t.Errorf("second request err = %v, want %v", err, ErrIsAlreadyTheHost)
}
}
10 changes: 9 additions & 1 deletion server/internal/websocket/handler/keyboard.go
Original file line number Diff line number Diff line change
@@ -1,13 +1,17 @@
package handler

import (
// "errors"
"errors"

"github.com/m1k1o/neko/server/pkg/types"
"github.com/m1k1o/neko/server/pkg/types/message"
)

func (h *MessageHandlerCtx) keyboardMap(session types.Session, payload *message.KeyboardMap) error {
if !session.Profile().CanHost {
return errors.New("is not allowed to host")
}

// Disabled: is host check
// if !session.IsHost() {
// return errors.New("is not the host")
Expand All @@ -17,6 +21,10 @@ func (h *MessageHandlerCtx) keyboardMap(session types.Session, payload *message.
}

func (h *MessageHandlerCtx) keyboardModifiers(session types.Session, payload *message.KeyboardModifiers) error {
if !session.Profile().CanHost {
return errors.New("is not allowed to host")
}

// Disabled: is host check
// if !session.IsHost() {
// return errors.New("is not the host")
Expand Down
Loading