From 3191dbb058922f4adcb46fd09c743791d8f8c12d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Miroslav=20=C5=A0ediv=C3=BD?= Date: Fri, 16 Oct 2026 20:23:56 +0200 Subject: [PATCH] Remove legacy mode. - Deleted the legacy WebSocket client implementation (`wstoclient.go`) and its test file (`wstoclient_test.go`). - Removed legacy handling from the HTTP manager, including legacy proxy setup and routing. - Cleaned up session management by removing legacy host checks and related functions. - Eliminated the legacy WebRTC handler and its associated logic. - Updated session interface to remove legacy methods. - Removed legacy API documentation and configuration options from migration guide and help files. --- client/README.md | 6 +- client/e2e.mjs | 2 +- server/cmd/root.go | 12 +- server/cmd/serve.go | 53 +- server/internal/api/session.go | 1 - server/internal/config/capture.go | 61 +- server/internal/config/desktop.go | 10 +- server/internal/config/member.go | 10 +- server/internal/config/root.go | 22 - server/internal/config/server.go | 22 +- server/internal/config/session.go | 16 +- server/internal/config/webrtc.go | 24 +- server/internal/http/legacy/event/events.go | 68 -- server/internal/http/legacy/handler.go | 463 ----------- .../internal/http/legacy/message/messages.go | 159 ---- server/internal/http/legacy/session.go | 232 ------ server/internal/http/legacy/session_test.go | 63 -- server/internal/http/legacy/types/types.go | 39 - server/internal/http/legacy/wstobackend.go | 386 --------- server/internal/http/legacy/wstoclient.go | 778 ------------------ .../internal/http/legacy/wstoclient_test.go | 20 - server/internal/http/manager.go | 26 - server/internal/session/session.go | 27 +- server/internal/webrtc/legacyhandler.go | 140 ---- server/internal/webrtc/manager.go | 18 - server/pkg/types/session.go | 1 - webpage/docs/migration-from-v2/README.md | 107 +-- webpage/docs/migration-from-v2/help.json | 8 - webpage/docs/migration-from-v2/help.txt | 2 - 29 files changed, 97 insertions(+), 2679 deletions(-) delete mode 100644 server/internal/http/legacy/event/events.go delete mode 100644 server/internal/http/legacy/handler.go delete mode 100644 server/internal/http/legacy/message/messages.go delete mode 100644 server/internal/http/legacy/session.go delete mode 100644 server/internal/http/legacy/session_test.go delete mode 100644 server/internal/http/legacy/types/types.go delete mode 100644 server/internal/http/legacy/wstobackend.go delete mode 100644 server/internal/http/legacy/wstoclient.go delete mode 100644 server/internal/http/legacy/wstoclient_test.go delete mode 100644 server/internal/webrtc/legacyhandler.go diff --git a/client/README.md b/client/README.md index ac3fe701..1b01fb71 100644 --- a/client/README.md +++ b/client/README.md @@ -1,7 +1,7 @@ # neko client The web client for [neko](https://github.com/m1k1o/neko): React 19 + TypeScript, built with Vite. -It speaks only the v3 API (`/api/ws` + REST), so it works with `NEKO_LEGACY=false`. +It speaks only the v3 API (`/api/ws` + REST). ``` client/ @@ -248,10 +248,10 @@ NEKO_URL=http://localhost:3000 npm run dev `e2e.mjs` drives two (or more) headless Chromium users through login, video, chat, control, files, admin actions, reconnects and URL parameters. It needs a disposable server (it takes control, types -into the desktop and kicks users) with the legacy API off and the multiuser provider: +into the desktop and kicks users) with the multiuser provider: ```sh -docker run -d -p 8080:8080 -p 52000:52000/udp -e NEKO_LEGACY=false \ +docker run -d -p 8080:8080 -p 52000:52000/udp \ -e NEKO_MEMBER_PROVIDER=multiuser -e NEKO_MEMBER_MULTIUSER_USER_PASSWORD=neko \ -e NEKO_MEMBER_MULTIUSER_ADMIN_PASSWORD=admin -e NEKO_FILETRANSFER_ENABLED=true \ -e NEKO_WEBRTC_UDPMUX=52000 \ diff --git a/client/e2e.mjs b/client/e2e.mjs index 40885e52..29cd8cac 100644 --- a/client/e2e.mjs +++ b/client/e2e.mjs @@ -1,5 +1,5 @@ import { chromium } from 'playwright' -// Two-user smoke test against a running v3 server (NEKO_LEGACY=false, multiuser provider): +// Two-user smoke test against a running v3 server with the multiuser provider: // NEKO_URL=https://host/ NEKO_ADMIN_PASSWORD=admin NEKO_USER_PASSWORD=neko npm run test:e2e // Screenshots land in ./e2e-out. Needs a KDE/X desktop that opens KRunner on Alt+F2 for the typing step. import fs from 'node:fs' diff --git a/server/cmd/root.go b/server/cmd/root.go index 49a49c2c..b4dae0fd 100644 --- a/server/cmd/root.go +++ b/server/cmd/root.go @@ -81,10 +81,7 @@ func init() { // set root config values rootConfig.Set() - // legacy if explicitly enabled or if unspecified and legacy config is found - if viper.GetBool("legacy") || !viper.IsSet("legacy") { - rootConfig.SetV2() - } + rootConfig.SetV2() ////// // logs @@ -166,11 +163,8 @@ func init() { log.Panic().Err(err).Msg("unable to run root command") } - // legacy if explicitly enabled or if unspecified and legacy config is found - if viper.GetBool("legacy") || !viper.IsSet("legacy") { - if err := rootConfig.InitV2(root); err != nil { - log.Panic().Err(err).Msg("unable to run root command") - } + if err := rootConfig.InitV2(root); err != nil { + log.Panic().Err(err).Msg("unable to run root command") } root.SetVersionTemplate(neko.Version.Details()) diff --git a/server/cmd/serve.go b/server/cmd/serve.go index bd604586..c09134a6 100644 --- a/server/cmd/serve.go +++ b/server/cmd/serve.go @@ -7,7 +7,6 @@ import ( "github.com/rs/zerolog" "github.com/rs/zerolog/log" "github.com/spf13/cobra" - "github.com/spf13/viper" "github.com/m1k1o/neko/server/internal/api" "github.com/m1k1o/neko/server/internal/capture" @@ -88,26 +87,23 @@ func (c *serve) Init(cmd *cobra.Command) error { return err } - // legacy if explicitly enabled or if unspecified and legacy config is found - if viper.GetBool("legacy") || !viper.IsSet("legacy") { - if err := c.configs.Desktop.InitV2(cmd); err != nil { - return err - } - if err := c.configs.Capture.InitV2(cmd); err != nil { - return err - } - if err := c.configs.WebRTC.InitV2(cmd); err != nil { - return err - } - if err := c.configs.Member.InitV2(cmd); err != nil { - return err - } - if err := c.configs.Session.InitV2(cmd); err != nil { - return err - } - if err := c.configs.Server.InitV2(cmd); err != nil { - return err - } + if err := c.configs.Desktop.InitV2(cmd); err != nil { + return err + } + if err := c.configs.Capture.InitV2(cmd); err != nil { + return err + } + if err := c.configs.WebRTC.InitV2(cmd); err != nil { + return err + } + if err := c.configs.Member.InitV2(cmd); err != nil { + return err + } + if err := c.configs.Session.InitV2(cmd); err != nil { + return err + } + if err := c.configs.Server.InitV2(cmd); err != nil { + return err } return nil @@ -124,15 +120,12 @@ func (c *serve) PreRun(cmd *cobra.Command, args []string) { c.configs.Plugins.Set() c.configs.Server.Set() - // legacy if explicitly enabled or if unspecified and legacy config is found - if viper.GetBool("legacy") || !viper.IsSet("legacy") { - c.configs.Desktop.SetV2() - c.configs.Capture.SetV2() - c.configs.WebRTC.SetV2() - c.configs.Member.SetV2() - c.configs.Session.SetV2() - c.configs.Server.SetV2() - } + c.configs.Desktop.SetV2() + c.configs.Capture.SetV2() + c.configs.WebRTC.SetV2() + c.configs.Member.SetV2() + c.configs.Session.SetV2() + c.configs.Server.SetV2() } func (c *serve) Start(cmd *cobra.Command) { diff --git a/server/internal/api/session.go b/server/internal/api/session.go index f054688e..68680c04 100644 --- a/server/internal/api/session.go +++ b/server/internal/api/session.go @@ -86,7 +86,6 @@ func (api *ApiManagerCtx) Whoami(w http.ResponseWriter, r *http.Request) error { }) } -// TODO: Remove when legacy mode is removed as all sessions must be synced with their providers. func (api *ApiManagerCtx) UpdateProfile(w http.ResponseWriter, r *http.Request) error { session, _ := auth.GetSession(r) diff --git a/server/internal/config/capture.go b/server/internal/config/capture.go index 261d85b4..d6e81070 100644 --- a/server/internal/config/capture.go +++ b/server/internal/config/capture.go @@ -378,14 +378,6 @@ func (s *Capture) Set() { }, } s.VideoIDs = []string{"main"} - - if viper.GetBool("legacy") { - legacyPipeline := s.VideoPipelines["main"] - // Hacky way to enable pointer for legacy pipeline. - legacyPipeline.GstPipeline = strings.Replace(legacyPipeline.GstPipeline, "show-pointer=false", "show-pointer=true", 1) - s.VideoPipelines["legacy"] = legacyPipeline - // we do not add legacy to VideoIDs so that its ignored by bandwidth estimator - } } else { log.Warn().Msgf("no video pipelines specified, using default") @@ -414,13 +406,6 @@ func (s *Capture) Set() { }, } s.VideoIDs = []string{"main"} - - if viper.GetBool("legacy") { - legacyPipeline := s.VideoPipelines["main"] - legacyPipeline.ShowPointer = true - s.VideoPipelines["legacy"] = legacyPipeline - // we do not add legacy to VideoIDs so that its ignored by bandwidth estimator - } } } else if videoPipeline != "" { log.Warn().Msg("you are setting both single video pipeline and multiple video pipelines, ignoring single video pipeline") @@ -485,7 +470,7 @@ func (s *Capture) Set() { } func (s *Capture) SetV2() { - enableLegacy := false + deprecatedV2ConfigUsed := false var ok bool @@ -496,7 +481,7 @@ func (s *Capture) SetV2() { if display := viper.GetString("display"); display != "" { s.Display = display log.Warn().Msg("you are using v2 configuration 'NEKO_DISPLAY' which is deprecated, please use 'NEKO_CAPTURE_VIDEO_DISPLAY' and/or 'NEKO_DESKTOP_DISPLAY' instead, also consider using 'DISPLAY' env variable if both should be the same") - enableLegacy = true + deprecatedV2ConfigUsed = true } modifiedVideoCodec := false @@ -507,29 +492,29 @@ func (s *Capture) SetV2() { s.VideoCodec = codec.VP8() } log.Warn().Msg("you are using v2 configuration 'NEKO_VIDEO_CODEC' which is deprecated, please use 'NEKO_CAPTURE_VIDEO_CODEC' instead") - enableLegacy = true + deprecatedV2ConfigUsed = true modifiedVideoCodec = true } if viper.GetBool("vp8") { s.VideoCodec = codec.VP8() log.Warn().Msg("you are using deprecated config setting 'NEKO_VP8=true', use 'NEKO_CAPTURE_VIDEO_CODEC=vp8' instead") - enableLegacy = true + deprecatedV2ConfigUsed = true modifiedVideoCodec = true } else if viper.GetBool("vp9") { s.VideoCodec = codec.VP9() log.Warn().Msg("you are using deprecated config setting 'NEKO_VP9=true', use 'NEKO_CAPTURE_VIDEO_CODEC=vp9' instead") - enableLegacy = true + deprecatedV2ConfigUsed = true modifiedVideoCodec = true } else if viper.GetBool("h264") { s.VideoCodec = codec.H264() log.Warn().Msg("you are using deprecated config setting 'NEKO_H264=true', use 'NEKO_CAPTURE_VIDEO_CODEC=h264' instead") - enableLegacy = true + deprecatedV2ConfigUsed = true modifiedVideoCodec = true } else if viper.GetBool("av1") { s.VideoCodec = codec.AV1() log.Warn().Msg("you are using deprecated config setting 'NEKO_AV1=true', use 'NEKO_CAPTURE_VIDEO_CODEC=av1' instead") - enableLegacy = true + deprecatedV2ConfigUsed = true modifiedVideoCodec = true } @@ -566,11 +551,7 @@ func (s *Capture) SetV2() { // Hacky way to disable pointer. GstPipeline: strings.Replace(pipeline, "show-pointer=true", "show-pointer=false", 1), }, - "legacy": { - GstPipeline: pipeline, - }, } - // we do not add legacy to VideoIDs so that its ignored by bandwidth estimator s.VideoIDs = []string{"main"} } } @@ -580,7 +561,7 @@ func (s *Capture) SetV2() { } // TODO: add deprecated warning and proper alternative for HW enc, bitrate and max fps - enableLegacy = true + deprecatedV2ConfigUsed = true } // @@ -590,7 +571,7 @@ func (s *Capture) SetV2() { if audioDevice := viper.GetString("device"); audioDevice != "" { s.AudioDevice = audioDevice log.Warn().Msg("you are using v2 configuration 'NEKO_DEVICE' which is deprecated, please use 'NEKO_CAPTURE_AUDIO_DEVICE' instead") - enableLegacy = true + deprecatedV2ConfigUsed = true } modifiedAudioCodec := false @@ -601,29 +582,29 @@ func (s *Capture) SetV2() { s.AudioCodec = codec.Opus() } log.Warn().Msg("you are using v2 configuration 'NEKO_AUDIO_CODEC' which is deprecated, please use 'NEKO_CAPTURE_AUDIO_CODEC' instead") - enableLegacy = true + deprecatedV2ConfigUsed = true modifiedAudioCodec = true } if viper.GetBool("opus") { s.AudioCodec = codec.Opus() log.Warn().Msg("you are using deprecated config setting 'NEKO_OPUS=true', use 'NEKO_CAPTURE_AUDIO_CODEC=opus' instead") - enableLegacy = true + deprecatedV2ConfigUsed = true modifiedAudioCodec = true } else if viper.GetBool("g722") { s.AudioCodec = codec.G722() log.Warn().Msg("you are using deprecated config setting 'NEKO_G722=true', use 'NEKO_CAPTURE_AUDIO_CODEC=g722' instead") - enableLegacy = true + deprecatedV2ConfigUsed = true modifiedAudioCodec = true } else if viper.GetBool("pcmu") { s.AudioCodec = codec.PCMU() log.Warn().Msg("you are using deprecated config setting 'NEKO_PCMU=true', use 'NEKO_CAPTURE_AUDIO_CODEC=pcmu' instead") - enableLegacy = true + deprecatedV2ConfigUsed = true modifiedAudioCodec = true } else if viper.GetBool("pcma") { s.AudioCodec = codec.PCMA() log.Warn().Msg("you are using deprecated config setting 'NEKO_PCMA=true', use 'NEKO_CAPTURE_AUDIO_CODEC=pcma' instead") - enableLegacy = true + deprecatedV2ConfigUsed = true modifiedAudioCodec = true } @@ -644,7 +625,7 @@ func (s *Capture) SetV2() { } // TODO: add deprecated warning and proper alternative for audio bitrate - enableLegacy = true + deprecatedV2ConfigUsed = true } // @@ -654,22 +635,20 @@ func (s *Capture) SetV2() { if viper.IsSet("broadcast_pipeline") { s.BroadcastPipeline = viper.GetString("broadcast_pipeline") log.Warn().Msg("you are using v2 configuration 'NEKO_BROADCAST_PIPELINE' which is deprecated, please use 'NEKO_CAPTURE_BROADCAST_PIPELINE' instead") - enableLegacy = true + deprecatedV2ConfigUsed = true } if viper.IsSet("broadcast_url") { s.BroadcastUrl = viper.GetString("broadcast_url") log.Warn().Msg("you are using v2 configuration 'NEKO_BROADCAST_URL' which is deprecated, please use 'NEKO_CAPTURE_BROADCAST_URL' instead") - enableLegacy = true + deprecatedV2ConfigUsed = true } if viper.IsSet("broadcast_autostart") { s.BroadcastAutostart = viper.GetBool("broadcast_autostart") log.Warn().Msg("you are using v2 configuration 'NEKO_BROADCAST_AUTOSTART' which is deprecated, please use 'NEKO_CAPTURE_BROADCAST_AUTOSTART' instead") - enableLegacy = true + deprecatedV2ConfigUsed = true } - // set legacy flag if any V2 configuration was used - if !viper.IsSet("legacy") && enableLegacy { - log.Warn().Msg("legacy configuration is enabled because at least one V2 configuration was used, please migrate to V3 configuration, visit https://neko.m1k1o.net/docs/v3/migration-from-v2 for more details") - viper.Set("legacy", true) + if deprecatedV2ConfigUsed { + log.Warn().Msg("deprecated V2 configuration is in use; migrate to V3 configuration, see https://neko.m1k1o.net/docs/v3/migration-from-v2") } } diff --git a/server/internal/config/desktop.go b/server/internal/config/desktop.go index 8331035e..efc16c27 100644 --- a/server/internal/config/desktop.go +++ b/server/internal/config/desktop.go @@ -110,7 +110,7 @@ func (s *Desktop) Set() { } func (s *Desktop) SetV2() { - enableLegacy := false + deprecatedV2ConfigUsed := false if viper.IsSet("screen") { r := regexp.MustCompile(`([0-9]{1,4})x([0-9]{1,4})@([0-9]{1,3})`) @@ -128,12 +128,10 @@ func (s *Desktop) SetV2() { } } log.Warn().Msg("you are using v2 configuration 'NEKO_SCREEN' which is deprecated, please use 'NEKO_DESKTOP_SCREEN' instead") - enableLegacy = true + deprecatedV2ConfigUsed = true } - // set legacy flag if any V2 configuration was used - if !viper.IsSet("legacy") && enableLegacy { - log.Warn().Msg("legacy configuration is enabled because at least one V2 configuration was used, please migrate to V3 configuration, visit https://neko.m1k1o.net/docs/v3/migration-from-v2 for more details") - viper.Set("legacy", true) + if deprecatedV2ConfigUsed { + log.Warn().Msg("deprecated V2 configuration is in use; migrate to V3 configuration, see https://neko.m1k1o.net/docs/v3/migration-from-v2") } } diff --git a/server/internal/config/member.go b/server/internal/config/member.go index 29f62b30..bec8b3fd 100644 --- a/server/internal/config/member.go +++ b/server/internal/config/member.go @@ -266,7 +266,7 @@ func (s *Member) Set() { } func (s *Member) SetV2() { - enableLegacy := false + deprecatedV2ConfigUsed := false if viper.IsSet("password") || viper.IsSet("password_admin") { s.Provider = "multiuser" @@ -281,12 +281,10 @@ func (s *Member) SetV2() { s.Multiuser.AdminPassword = "admin" } log.Warn().Msg("you are using v2 configuration 'NEKO_PASSWORD' and 'NEKO_PASSWORD_ADMIN' which are deprecated, please use 'NEKO_MEMBER_MULTIUSER_USER_PASSWORD' and 'NEKO_MEMBER_MULTIUSER_ADMIN_PASSWORD' with 'NEKO_MEMBER_PROVIDER=multiuser' instead") - enableLegacy = true + deprecatedV2ConfigUsed = true } - // set legacy flag if any V2 configuration was used - if !viper.IsSet("legacy") && enableLegacy { - log.Warn().Msg("legacy configuration is enabled because at least one V2 configuration was used, please migrate to V3 configuration, visit https://neko.m1k1o.net/docs/v3/migration-from-v2 for more details") - viper.Set("legacy", true) + if deprecatedV2ConfigUsed { + log.Warn().Msg("deprecated V2 configuration is in use; migrate to V3 configuration, see https://neko.m1k1o.net/docs/v3/migration-from-v2") } } diff --git a/server/internal/config/root.go b/server/internal/config/root.go index c8902087..978e5130 100644 --- a/server/internal/config/root.go +++ b/server/internal/config/root.go @@ -13,7 +13,6 @@ import ( type Root struct { Config string - Legacy bool LogLevel zerolog.Level LogTime string @@ -34,14 +33,6 @@ func (Root) Init(cmd *cobra.Command) error { return err } - // whether legacy configs and api should be enabled. - // - if not specified, it will be automatically enabled if at least one legacy config entry is found. - // - if it is specified, it will be enabled/disabled regardless of the presence of legacy config entries. - cmd.PersistentFlags().Bool("legacy", true, "enable legacy mode") - if err := viper.BindPFlag("legacy", cmd.PersistentFlags().Lookup("legacy")); err != nil { - return err - } - cmd.PersistentFlags().String("log.level", "info", "set log level (trace, debug, info, warn, error, fatal, panic, disabled)") if err := viper.BindPFlag("log.level", cmd.PersistentFlags().Lookup("log.level")); err != nil { return err @@ -81,10 +72,6 @@ func (Root) InitV2(cmd *cobra.Command) error { func (s *Root) Set() { s.Config = viper.GetString("config") - s.Legacy = viper.GetBool("legacy") - if s.Legacy { - log.Info().Msg("legacy configuration is enabled") - } logLevel := viper.GetString("log.level") level, err := zerolog.ParseLevel(logLevel) @@ -121,8 +108,6 @@ func (s *Root) Set() { } func (s *Root) SetV2() { - enableLegacy := false - if viper.IsSet("logs") { if viper.GetBool("logs") { logs := filepath.Join(".", "logs") @@ -134,12 +119,5 @@ func (s *Root) SetV2() { s.LogDir = "" } log.Warn().Msg("you are using v2 configuration 'NEKO_LOGS' which is deprecated, please use 'NEKO_LOG_DIR=/path/to/logs' instead") - enableLegacy = true - } - - // set legacy flag if any V2 configuration was used - if !viper.IsSet("legacy") && enableLegacy { - log.Warn().Msg("legacy configuration is enabled because at least one V2 configuration was used, please migrate to V3 configuration, visit https://neko.m1k1o.net/docs/v3/migration-from-v2 for more details") - viper.Set("legacy", true) } } diff --git a/server/internal/config/server.go b/server/internal/config/server.go index c094fbc7..b4124cff 100644 --- a/server/internal/config/server.go +++ b/server/internal/config/server.go @@ -127,37 +127,37 @@ func (s *Server) Set() { } func (s *Server) SetV2() { - enableLegacy := false + deprecatedV2ConfigUsed := false if viper.IsSet("cert") { s.Cert = viper.GetString("cert") log.Warn().Msg("you are using v2 configuration 'NEKO_CERT' which is deprecated, please use 'NEKO_SERVER_CERT' instead") - enableLegacy = true + deprecatedV2ConfigUsed = true } if viper.IsSet("key") { s.Key = viper.GetString("key") log.Warn().Msg("you are using v2 configuration 'NEKO_KEY' which is deprecated, please use 'NEKO_SERVER_KEY' instead") - enableLegacy = true + deprecatedV2ConfigUsed = true } if viper.IsSet("bind") { s.Bind = viper.GetString("bind") log.Warn().Msg("you are using v2 configuration 'NEKO_BIND' which is deprecated, please use 'NEKO_SERVER_BIND' instead") - enableLegacy = true + deprecatedV2ConfigUsed = true } if viper.IsSet("proxy") { s.Proxy = viper.GetBool("proxy") log.Warn().Msg("you are using v2 configuration 'NEKO_PROXY' which is deprecated, please use 'NEKO_SERVER_PROXY' instead") - enableLegacy = true + deprecatedV2ConfigUsed = true } if viper.IsSet("static") { s.Static = viper.GetString("static") log.Warn().Msg("you are using v2 configuration 'NEKO_STATIC' which is deprecated, please use 'NEKO_SERVER_STATIC' instead") - enableLegacy = true + deprecatedV2ConfigUsed = true } if viper.IsSet("path_prefix") { s.PathPrefix = path.Join("/", path.Clean(viper.GetString("path_prefix"))) log.Warn().Msg("you are using v2 configuration 'NEKO_PATH_PREFIX' which is deprecated, please use 'NEKO_SERVER_PATH_PREFIX' instead") - enableLegacy = true + deprecatedV2ConfigUsed = true } if viper.IsSet("cors") { s.CORS = viper.GetStringSlice("cors") @@ -166,13 +166,11 @@ func (s *Server) SetV2() { log.Warn().Msg("CORS is enabled for all origins, this is not recommended for production environments") } log.Warn().Msg("you are using v2 configuration 'NEKO_CORS' which is deprecated, please use 'NEKO_SERVER_CORS' instead") - enableLegacy = true + deprecatedV2ConfigUsed = true } - // set legacy flag if any V2 configuration was used - if !viper.IsSet("legacy") && enableLegacy { - log.Warn().Msg("legacy configuration is enabled because at least one V2 configuration was used, please migrate to V3 configuration, visit https://neko.m1k1o.net/docs/v3/migration-from-v2 for more details") - viper.Set("legacy", true) + if deprecatedV2ConfigUsed { + log.Warn().Msg("deprecated V2 configuration is in use; migrate to V3 configuration, see https://neko.m1k1o.net/docs/v3/migration-from-v2") } } diff --git a/server/internal/config/session.go b/server/internal/config/session.go index b5e90520..140d8c54 100644 --- a/server/internal/config/session.go +++ b/server/internal/config/session.go @@ -171,7 +171,7 @@ func (s *Session) Set() { } func (s *Session) SetV2() { - enableLegacy := false + deprecatedV2ConfigUsed := false if viper.IsSet("locks") { locks := viper.GetStringSlice("locks") @@ -185,28 +185,26 @@ func (s *Session) SetV2() { } } log.Warn().Msg("you are using v2 configuration 'NEKO_LOCKS' which is deprecated, please use 'NEKO_SESSION_LOCKED_CONTROLS' and 'NEKO_SESSION_LOCKED_LOGINS' instead") - enableLegacy = true + deprecatedV2ConfigUsed = true } if viper.IsSet("implicit_control") { s.ImplicitHosting = viper.GetBool("implicit_control") log.Warn().Msg("you are using v2 configuration 'NEKO_IMPLICIT_CONTROL' which is deprecated, please use 'NEKO_SESSION_IMPLICIT_HOSTING' instead") - enableLegacy = true + deprecatedV2ConfigUsed = true } if viper.IsSet("control_protection") { s.ControlProtection = viper.GetBool("control_protection") log.Warn().Msg("you are using v2 configuration 'NEKO_CONTROL_PROTECTION' which is deprecated, please use 'NEKO_SESSION_CONTROL_PROTECTION' instead") - enableLegacy = true + deprecatedV2ConfigUsed = true } if viper.IsSet("heartbeat_interval") { s.HeartbeatInterval = viper.GetInt("heartbeat_interval") log.Warn().Msg("you are using v2 configuration 'NEKO_HEARTBEAT_INTERVAL' which is deprecated, please use 'NEKO_SESSION_HEARTBEAT_INTERVAL' instead") - enableLegacy = true + deprecatedV2ConfigUsed = true } - // set legacy flag if any V2 configuration was used - if !viper.IsSet("legacy") && enableLegacy { - log.Warn().Msg("legacy configuration is enabled because at least one V2 configuration was used, please migrate to V3 configuration, visit https://neko.m1k1o.net/docs/v3/migration-from-v2 for more details") - viper.Set("legacy", true) + if deprecatedV2ConfigUsed { + log.Warn().Msg("deprecated V2 configuration is in use; migrate to V3 configuration, see https://neko.m1k1o.net/docs/v3/migration-from-v2") } } diff --git a/server/internal/config/webrtc.go b/server/internal/config/webrtc.go index bc202d70..35dcc393 100644 --- a/server/internal/config/webrtc.go +++ b/server/internal/config/webrtc.go @@ -318,27 +318,27 @@ func (s *WebRTC) Set() { } func (s *WebRTC) SetV2() { - enableLegacy := false + deprecatedV2ConfigUsed := false if viper.IsSet("nat1to1") { s.NAT1To1IPs = viper.GetStringSlice("nat1to1") log.Warn().Msg("you are using v2 configuration 'NEKO_NAT1TO1' which is deprecated, please use 'NEKO_WEBRTC_NAT1TO1' instead") - enableLegacy = true + deprecatedV2ConfigUsed = true } if viper.IsSet("tcpmux") { s.TCPMux = viper.GetInt("tcpmux") log.Warn().Msg("you are using v2 configuration 'NEKO_TCPMUX' which is deprecated, please use 'NEKO_WEBRTC_TCPMUX' instead") - enableLegacy = true + deprecatedV2ConfigUsed = true } if viper.IsSet("udpmux") { s.UDPMux = viper.GetInt("udpmux") log.Warn().Msg("you are using v2 configuration 'NEKO_UDPMUX' which is deprecated, please use 'NEKO_WEBRTC_UDPMUX' instead") - enableLegacy = true + deprecatedV2ConfigUsed = true } if viper.IsSet("icelite") { s.ICELite = viper.GetBool("icelite") log.Warn().Msg("you are using v2 configuration 'NEKO_ICELITE' which is deprecated, please use 'NEKO_WEBRTC_ICELITE' instead") - enableLegacy = true + deprecatedV2ConfigUsed = true } if viper.IsSet("iceservers") { @@ -353,7 +353,7 @@ func (s *WebRTC) SetV2() { s.ICEServersFrontend = iceServers s.ICEServersBackend = iceServers log.Warn().Msg("you are using v2 configuration 'NEKO_ICESERVERS' which is deprecated, please use 'NEKO_WEBRTC_ICESERVERS_FRONTEND' and/or 'NEKO_WEBRTC_ICESERVERS_BACKEND' instead") - enableLegacy = true + deprecatedV2ConfigUsed = true } if viper.IsSet("iceserver") { @@ -363,7 +363,7 @@ func (s *WebRTC) SetV2() { s.ICEServersBackend = append(s.ICEServersBackend, types.ICEServer{URLs: iceServerSlice}) } log.Warn().Msg("you are using v2 configuration 'NEKO_ICESERVER' which is deprecated, please use 'NEKO_WEBRTC_ICESERVERS_FRONTEND' and/or 'NEKO_WEBRTC_ICESERVERS_BACKEND' instead") - enableLegacy = true + deprecatedV2ConfigUsed = true } if viper.IsSet("ipfetch") { @@ -376,7 +376,7 @@ func (s *WebRTC) SetV2() { s.NAT1To1IPs = append(s.NAT1To1IPs, ip) } log.Warn().Msg("you are using v2 configuration 'NEKO_IPFETCH' which is deprecated, please use 'NEKO_WEBRTC_IP_RETRIEVAL_URL' instead") - enableLegacy = true + deprecatedV2ConfigUsed = true } if viper.IsSet("epr") { @@ -404,12 +404,10 @@ func (s *WebRTC) SetV2() { s.EphemeralMax = max } log.Warn().Msg("you are using v2 configuration 'NEKO_EPR' which is deprecated, please use 'NEKO_WEBRTC_EPR' instead") - enableLegacy = true + deprecatedV2ConfigUsed = true } - // set legacy flag if any V2 configuration was used - if !viper.IsSet("legacy") && enableLegacy { - log.Warn().Msg("legacy configuration is enabled because at least one V2 configuration was used, please migrate to V3 configuration, visit https://neko.m1k1o.net/docs/v3/migration-from-v2 for more details") - viper.Set("legacy", true) + if deprecatedV2ConfigUsed { + log.Warn().Msg("deprecated V2 configuration is in use; migrate to V3 configuration, see https://neko.m1k1o.net/docs/v3/migration-from-v2") } } diff --git a/server/internal/http/legacy/event/events.go b/server/internal/http/legacy/event/events.go deleted file mode 100644 index 3cc0b3ae..00000000 --- a/server/internal/http/legacy/event/events.go +++ /dev/null @@ -1,68 +0,0 @@ -package event - -const ( - SYSTEM_INIT = "system/init" - SYSTEM_DISCONNECT = "system/disconnect" - SYSTEM_ERROR = "system/error" -) - -const ( - CLIENT_HEARTBEAT = "client/heartbeat" -) - -const ( - SIGNAL_OFFER = "signal/offer" - SIGNAL_ANSWER = "signal/answer" - SIGNAL_PROVIDE = "signal/provide" - SIGNAL_CANDIDATE = "signal/candidate" -) - -const ( - MEMBER_LIST = "member/list" - MEMBER_CONNECTED = "member/connected" - MEMBER_DISCONNECTED = "member/disconnected" -) - -const ( - CONTROL_LOCKED = "control/locked" - CONTROL_RELEASE = "control/release" - CONTROL_REQUEST = "control/request" - CONTROL_REQUESTING = "control/requesting" - CONTROL_GIVE = "control/give" - CONTROL_CLIPBOARD = "control/clipboard" - CONTROL_KEYBOARD = "control/keyboard" -) - -const ( - CHAT_MESSAGE = "chat/message" - CHAT_EMOTE = "chat/emote" -) - -const ( - FILETRANSFER_LIST = "filetransfer/list" - FILETRANSFER_REFRESH = "filetransfer/refresh" -) - -const ( - SCREEN_CONFIGURATIONS = "screen/configurations" - SCREEN_RESOLUTION = "screen/resolution" - SCREEN_SET = "screen/set" -) - -const ( - BROADCAST_STATUS = "broadcast/status" - BROADCAST_CREATE = "broadcast/create" - BROADCAST_DESTROY = "broadcast/destroy" -) - -const ( - ADMIN_BAN = "admin/ban" - ADMIN_KICK = "admin/kick" - ADMIN_LOCK = "admin/lock" - ADMIN_MUTE = "admin/mute" - ADMIN_UNLOCK = "admin/unlock" - ADMIN_UNMUTE = "admin/unmute" - ADMIN_CONTROL = "admin/control" - ADMIN_RELEASE = "admin/release" - ADMIN_GIVE = "admin/give" -) diff --git a/server/internal/http/legacy/handler.go b/server/internal/http/legacy/handler.go deleted file mode 100644 index d33ba086..00000000 --- a/server/internal/http/legacy/handler.go +++ /dev/null @@ -1,463 +0,0 @@ -package legacy - -import ( - "encoding/json" - "errors" - "fmt" - "io" - "net" - "net/http" - "net/url" - "path" - "strings" - "time" - - "github.com/m1k1o/neko/server/internal/api" - oldEvent "github.com/m1k1o/neko/server/internal/http/legacy/event" - oldMessage "github.com/m1k1o/neko/server/internal/http/legacy/message" - oldTypes "github.com/m1k1o/neko/server/internal/http/legacy/types" - - "github.com/m1k1o/neko/server/pkg/types" - "github.com/m1k1o/neko/server/pkg/types/event" - "github.com/m1k1o/neko/server/pkg/types/message" - "github.com/m1k1o/neko/server/pkg/utils" - - "github.com/gorilla/websocket" - "github.com/rs/zerolog" - "github.com/rs/zerolog/log" -) - -var ( - // DefaultUpgrader specifies the parameters for upgrading an HTTP - // connection to a WebSocket connection. - DefaultUpgrader = &websocket.Upgrader{ - ReadBufferSize: 1024, - WriteBufferSize: 1024, - CheckOrigin: func(r *http.Request) bool { - return true - }, - } -) - -type LegacyHandler struct { - logger zerolog.Logger - serverAddr string - pathPrefix string - bannedIPs map[string]struct{} - sessionIPs map[string]string - wsDialer *websocket.Dialer -} - -func New(serverAddr, pathPrefix string) *LegacyHandler { - // Init - - return &LegacyHandler{ - logger: log.With().Str("module", "legacy").Logger(), - serverAddr: serverAddr, - pathPrefix: pathPrefix, - bannedIPs: make(map[string]struct{}), - sessionIPs: make(map[string]string), - wsDialer: &websocket.Dialer{ - Proxy: nil, // disable proxy for local requests - HandshakeTimeout: 45 * time.Second, - }, - } -} - -func (h *LegacyHandler) Route(r types.Router) { - r.Get("/ws", func(w http.ResponseWriter, r *http.Request) error { - s := h.newSession(r) - - // create a new websocket connection - connClient, err := DefaultUpgrader.Upgrade(w, r, nil) - if err != nil { - return utils.HttpError(http.StatusInternalServerError). - WithInternalErr(err). - Msg("couldn't upgrade connection to websocket") - } - defer connClient.Close() - s.connClient = connClient - - if h.isBanned(r) { - s.toClient(&oldMessage.SystemMessage{ - Event: oldEvent.SYSTEM_DISCONNECT, - Title: "banned ip", - Message: "you are banned", - }) - } - - // create a new session - username := r.URL.Query().Get("username") - password := r.URL.Query().Get("password") - err = s.create(username, password) - if err != nil { - h.logger.Error().Err(err).Msg("couldn't create a new session") - - s.toClient(&oldMessage.SystemMessage{ - Event: oldEvent.SYSTEM_DISCONNECT, - Title: "couldn't create a new session", - Message: err.Error(), - }) - - // we can't return HTTP error here because the connection is already upgraded - return nil - } - defer s.destroy() - - // dial to the remote backend - backendURL := "ws://" + h.serverAddr + path.Join(s.pathPrefix, "/api/ws") - backendHeaders := make(http.Header) - if s.token != "" { - backendURL += "?token=" + url.QueryEscape(s.token) - } else { - backendHeaders.Set("Cookie", s.cookie) - } - connBackend, _, err := h.wsDialer.Dial(backendURL, backendHeaders) - if err != nil { - h.logger.Error().Err(err).Msg("couldn't dial to the remote backend") - - s.toClient(&oldMessage.SystemMessage{ - Event: oldEvent.SYSTEM_DISCONNECT, - Title: "couldn't dial to the remote backend", - Message: err.Error(), - }) - - // we can't return HTTP error here because the connection is already upgraded - return nil - } - defer connBackend.Close() - s.connBackend = connBackend - - // request signal - videoAuto := true - if err = s.toBackend(event.SIGNAL_REQUEST, message.SignalRequest{ - Video: types.PeerVideoRequest{ - Auto: &videoAuto, - }, - }); err != nil { - h.logger.Error().Err(err).Msg("couldn't request signal") - s.toClient(&oldMessage.SystemMessage{ - Event: oldEvent.SYSTEM_DISCONNECT, - Title: "couldn't request signal", - Message: err.Error(), - }) - - // we can't return HTTP error here because the connection is already upgraded - return nil - } - - // copy messages between the client and the backend - errClient := make(chan error, 1) - errBackend := make(chan error, 1) - replicateWebsocketConn := func(dst, src *websocket.Conn, errc chan error, rewriteTextMessage func([]byte) error) { - for { - msgType, msg, err := src.ReadMessage() - if err != nil { - m := websocket.FormatCloseMessage(websocket.CloseNormalClosure, fmt.Sprintf("%v", err)) - if e, ok := err.(*websocket.CloseError); ok { - if e.Code != websocket.CloseNoStatusReceived { - m = websocket.FormatCloseMessage(e.Code, e.Text) - } - } - errc <- fmt.Errorf("src read message error: %w", err) - dst.WriteMessage(websocket.CloseMessage, m) - break - } - - // handle text messages - if msgType == websocket.TextMessage { - err = rewriteTextMessage(msg) - - if err == nil { - continue - } - - if errors.Is(err, ErrBackendRespone) { - h.logger.Error().Err(err).Msg("backend response error") - - s.toClient(&oldMessage.SystemMessage{ - Event: oldEvent.SYSTEM_ERROR, - Title: "backend response error", - Message: strings.ReplaceAll(err.Error(), ErrBackendRespone.Error()+": ", ""), - }) - continue - } - - if errors.Is(err, ErrWebsocketSend) { - errc <- fmt.Errorf("dst write message error: %w", err) - break - } - - h.logger.Error().Err(err).Msg("couldn't rewrite text message") - continue - } - - // forward ping pong messages - if msgType == websocket.PingMessage || - msgType == websocket.PongMessage { - err = dst.WriteMessage(msgType, msg) - if err != nil { - errc <- err - break - } - continue - } - } - } - - // backend -> client - go replicateWebsocketConn(connClient, connBackend, errClient, s.wsToClient) - - // client -> backend - go replicateWebsocketConn(connBackend, connClient, errBackend, s.wsToBackend) - - var message string - select { - case err = <-errClient: - message = "websocketproxy: Error when copying from backend to client" - case err = <-errBackend: - message = "websocketproxy: Error when copying from client to backend" - } - - if e, ok := err.(*websocket.CloseError); !ok || e.Code == websocket.CloseAbnormalClosure { - h.logger.Error().Err(err).Msg(message) - } - - return nil - }) - - r.Get("/stats", func(w http.ResponseWriter, r *http.Request) error { - if h.isBanned(r) { - return utils.HttpForbidden("banned ip") - } - - s := h.newSession(r) - - // create a new session - username := r.URL.Query().Get("usr") - password := r.URL.Query().Get("pwd") - err := s.create(username, password) - if err != nil { - return utils.HttpForbidden(err.Error()) - } - defer s.destroy() - - if !s.isAdmin { - return utils.HttpUnauthorized().Msg("bad authorization") - } - - w.Header().Set("Content-Type", "application/json") - - // get all sessions - sessions := []api.SessionDataPayload{} - err = s.apiReq(http.MethodGet, "/api/sessions", nil, &sessions) - if err != nil { - return utils.HttpInternalServerError().WithInternalErr(err) - } - - // get stats - newStats := types.Stats{} - err = s.apiReq(http.MethodGet, "/api/stats", nil, &newStats) - if err != nil { - return utils.HttpInternalServerError().WithInternalErr(err) - } - - // get settings - settings := types.Settings{} - err = s.apiReq(http.MethodGet, "/api/room/settings", nil, &settings) - if err != nil { - return utils.HttpInternalServerError().WithInternalErr(err) - } - - var stats oldTypes.Stats - - // create empty array so that it's not null in json - stats.Members = []*oldTypes.Member{} - - for _, session := range sessions { - if session.State.IsConnected { - stats.Connections++ - member, err := profileToMember(session.ID, session.Profile) - if err != nil { - return utils.HttpInternalServerError().WithInternalErr(err) - } - // append members - stats.Members = append(stats.Members, member) - } - } - - locks, err := s.settingsToLocks(settings) - if err != nil { - return err - } - - stats.Host = newStats.HostId - // TODO: stats.Banned, not implemented yet - stats.Locked = locks - stats.ServerStartedAt = newStats.ServerStartedAt - stats.LastAdminLeftAt = newStats.LastAdminLeftAt - stats.LastUserLeftAt = newStats.LastUserLeftAt - stats.ControlProtection = settings.ControlProtection - stats.ImplicitControl = settings.ImplicitHosting - - return json.NewEncoder(w).Encode(stats) - }) - - r.Get("/screenshot.jpg", func(w http.ResponseWriter, r *http.Request) error { - if h.isBanned(r) { - return utils.HttpForbidden("banned ip") - } - - s := h.newSession(r) - - // create a new session - username := r.URL.Query().Get("usr") - password := r.URL.Query().Get("pwd") - err := s.create(username, password) - if err != nil { - return utils.HttpForbidden(err.Error()) - } - defer s.destroy() - - if !s.isAdmin { - return utils.HttpUnauthorized().Msg("bad authorization") - } - - quality := r.URL.Query().Get("quality") - - // get the screenshot - body, headers, err := s.req(http.MethodGet, "/api/room/screen/shot.jpg?quality="+url.QueryEscape(quality), nil, nil) - if err != nil { - return utils.HttpInternalServerError().WithInternalErr(err) - } - - // copy headers - w.Header().Set("Content-Length", headers.Get("Content-Length")) - w.Header().Set("Content-Type", headers.Get("Content-Type")) - - // copy the body to the response writer - _, err = io.Copy(w, body) - return err - }) - - // allow downloading and uploading files - r.Get("/file", func(w http.ResponseWriter, r *http.Request) error { - if h.isBanned(r) { - return utils.HttpForbidden("banned ip") - } - - s := h.newSession(r) - - // create a new session - username := r.URL.Query().Get("usr") - password := r.URL.Query().Get("pwd") - err := s.create(username, password) - if err != nil { - return utils.HttpForbidden(err.Error()) - } - defer s.destroy() - - filename := r.URL.Query().Get("filename") - - body, headers, err := s.req(http.MethodGet, "/api/filetransfer?filename="+url.QueryEscape(filename), r.Header, nil) - if err != nil { - return utils.HttpInternalServerError().WithInternalErr(err) - } - - // copy headers - w.Header().Set("Content-Length", headers.Get("Content-Length")) - w.Header().Set("Content-Type", headers.Get("Content-Type")) - - // copy the body to the response writer - _, err = io.Copy(w, body) - return err - }) - - r.Post("/file", func(w http.ResponseWriter, r *http.Request) error { - if h.isBanned(r) { - return utils.HttpForbidden("banned ip") - } - - s := h.newSession(r) - - // create a new session - username := r.URL.Query().Get("usr") - password := r.URL.Query().Get("pwd") - err := s.create(username, password) - if err != nil { - return utils.HttpForbidden(err.Error()) - } - defer s.destroy() - - body, _, err := s.req(http.MethodPost, "/api/filetransfer", r.Header, r.Body) - if err != nil { - return utils.HttpInternalServerError().WithInternalErr(err) - } - - // copy the body to the response writer - _, err = io.Copy(w, body) - return err - }) - - r.Delete("/file", func(w http.ResponseWriter, r *http.Request) error { - if h.isBanned(r) { - return utils.HttpForbidden("banned ip") - } - - s := h.newSession(r) - - // create a new session - username := r.URL.Query().Get("usr") - password := r.URL.Query().Get("pwd") - err := s.create(username, password) - if err != nil { - return utils.HttpForbidden(err.Error()) - } - defer s.destroy() - - filename := r.URL.Query().Get("filename") - - body, _, err := s.req(http.MethodDelete, "/api/filetransfer?filename="+url.QueryEscape(filename), r.Header, nil) - if err != nil { - return utils.HttpInternalServerError().WithInternalErr(err) - } - - // copy the body to the response writer - _, err = io.Copy(w, body) - return err - }) - - r.Get("/health", func(w http.ResponseWriter, r *http.Request) error { - _, err := w.Write([]byte("true")) - return err - }) -} - -func (h *LegacyHandler) ban(sessionId string) error { - // find session by id - ip, ok := h.sessionIPs[sessionId] - if !ok { - return fmt.Errorf("session not found") - } - - h.bannedIPs[ip] = struct{}{} - return nil -} - -func (h *LegacyHandler) isBanned(r *http.Request) bool { - ip := getIp(r) - _, ok := h.bannedIPs[ip] - return ok -} - -func getIp(r *http.Request) string { - ip, _, err := net.SplitHostPort(r.RemoteAddr) - if err != nil { - if e, ok := err.(*net.AddrError); ok && e.Err == "missing port in address" { - return r.RemoteAddr - } - return "" - } - - return ip -} diff --git a/server/internal/http/legacy/message/messages.go b/server/internal/http/legacy/message/messages.go deleted file mode 100644 index c835848e..00000000 --- a/server/internal/http/legacy/message/messages.go +++ /dev/null @@ -1,159 +0,0 @@ -package message - -import ( - "github.com/m1k1o/neko/server/internal/http/legacy/types" - - "github.com/pion/webrtc/v4" -) - -type Message struct { - Event string `json:"event"` -} - -type SystemInit struct { - Event string `json:"event"` - Locks map[string]string `json:"locks"` - ImplicitHosting bool `json:"implicit_hosting"` - FileTransfer bool `json:"file_transfer"` - HeartbeatInterval int `json:"heartbeat_interval"` -} - -type SystemMessage struct { - Event string `json:"event"` - Title string `json:"title"` - Message string `json:"message"` -} - -type SignalProvide struct { - Event string `json:"event"` - ID string `json:"id"` - SDP string `json:"sdp"` - Lite bool `json:"lite"` - ICE []webrtc.ICEServer `json:"ice"` -} - -type SignalOffer struct { - Event string `json:"event"` - SDP string `json:"sdp"` -} - -type SignalAnswer struct { - Event string `json:"event"` - DisplayName string `json:"displayname"` - SDP string `json:"sdp"` -} - -type SignalCandidate struct { - Event string `json:"event"` - Data string `json:"data"` -} - -type MembersList struct { - Event string `json:"event"` - Members []*types.Member `json:"members"` -} - -type Member struct { - Event string `json:"event"` - *types.Member -} -type MemberDisconnected struct { - Event string `json:"event"` - ID string `json:"id"` -} - -type Clipboard struct { - Event string `json:"event"` - Text string `json:"text"` -} - -type Keyboard struct { - Event string `json:"event"` - Layout *string `json:"layout,omitempty"` - CapsLock *bool `json:"capsLock,omitempty"` - NumLock *bool `json:"numLock,omitempty"` - ScrollLock *bool `json:"scrollLock,omitempty"` // TODO: ScrollLock is deprecated. -} - -type Control struct { - Event string `json:"event"` - ID string `json:"id"` -} - -type ControlTarget struct { - Event string `json:"event"` - ID string `json:"id"` - Target string `json:"target"` -} - -type ChatReceive struct { - Event string `json:"event"` - Content string `json:"content"` -} - -type ChatSend struct { - Event string `json:"event"` - ID string `json:"id"` - Content string `json:"content"` -} - -type EmoteReceive struct { - Event string `json:"event"` - Emote string `json:"emote"` -} - -type EmoteSend struct { - Event string `json:"event"` - ID string `json:"id"` - Emote string `json:"emote"` -} - -type FileTransferList struct { - Event string `json:"event"` - Cwd string `json:"cwd"` - UserDownload bool `json:"user_download"` - UserUpload bool `json:"user_upload"` - UserDelete bool `json:"user_delete"` - Files []types.FileListItem `json:"files"` -} - -type Admin struct { - Event string `json:"event"` - ID string `json:"id"` -} - -type AdminTarget struct { - Event string `json:"event"` - Target string `json:"target"` - ID string `json:"id"` -} - -type AdminLock struct { - Event string `json:"event"` - Resource string `json:"resource"` - ID string `json:"id"` -} - -type ScreenResolution struct { - Event string `json:"event"` - ID string `json:"id,omitempty"` - Width int `json:"width"` - Height int `json:"height"` - Rate int16 `json:"rate"` -} - -type ScreenConfigurations struct { - Event string `json:"event"` - Configurations map[int]types.ScreenConfiguration `json:"configurations"` -} - -type BroadcastStatus struct { - Event string `json:"event"` - URL string `json:"url"` - IsActive bool `json:"isActive"` -} - -type BroadcastCreate struct { - Event string `json:"event"` - URL string `json:"url"` -} diff --git a/server/internal/http/legacy/session.go b/server/internal/http/legacy/session.go deleted file mode 100644 index ce9d135c..00000000 --- a/server/internal/http/legacy/session.go +++ /dev/null @@ -1,232 +0,0 @@ -package legacy - -import ( - "bytes" - "encoding/json" - "fmt" - "io" - "net/http" - "path" - "strings" - "sync" - - oldTypes "github.com/m1k1o/neko/server/internal/http/legacy/types" - - "github.com/m1k1o/neko/server/internal/api" - "github.com/m1k1o/neko/server/pkg/types" - - "github.com/gorilla/websocket" - "github.com/rs/zerolog" -) - -var ( - ErrWebsocketSend = fmt.Errorf("failed to send message to websocket") - ErrBackendRespone = fmt.Errorf("error response from backend") -) - -type memberStruct struct { - member *oldTypes.Member - connected bool - sent bool -} - -type session struct { - r *http.Request - h *LegacyHandler - - logger zerolog.Logger - serverAddr string - pathPrefix string - - id, ip string - token string - cookie string - name string - isAdmin bool - client *http.Client - - lastHostID string - lockedControls bool - lockedLogins bool - lockedFileTransfer bool - sessions map[string]*memberStruct - - muClient sync.Mutex - connClient *websocket.Conn - muBackend sync.Mutex - connBackend *websocket.Conn -} - -func (h *LegacyHandler) newSession(r *http.Request) *session { - transport := http.DefaultTransport.(*http.Transport).Clone() - transport.Proxy = nil // disable proxy for local requests - - return &session{ - r: r, - h: h, - logger: h.logger, - serverAddr: h.serverAddr, - pathPrefix: h.pathPrefix, - client: &http.Client{ - Transport: transport, - }, - sessions: make(map[string]*memberStruct), - } -} - -func (s *session) req(method, reqPath string, headers http.Header, request io.Reader) (io.ReadCloser, http.Header, error) { - req, err := http.NewRequest(method, "http://"+s.serverAddr+path.Join(s.pathPrefix, reqPath), request) - if err != nil { - return nil, nil, err - } - - for k, v := range headers { - req.Header[k] = v - } - - if s.token != "" { - req.Header.Set("Authorization", "Bearer "+s.token) - } else if s.cookie != "" { - req.Header.Set("Cookie", s.cookie) - } - - res, err := s.client.Do(req) - if err != nil { - return nil, nil, err - } - - if res.StatusCode < 200 || res.StatusCode >= 300 { - defer res.Body.Close() - - body, _ := io.ReadAll(res.Body) - // try to unmarsal as json error message - var apiErr struct { - Message string `json:"message"` - } - if err := json.Unmarshal(body, &apiErr); err == nil { - return nil, nil, fmt.Errorf("%w: %s", ErrBackendRespone, apiErr.Message) - } - // return raw body if failed to unmarshal - return nil, nil, fmt.Errorf("unexpected status code: %d, body: %s", res.StatusCode, strings.TrimSpace(string(body))) - } - - return res.Body, res.Header, nil -} - -func (s *session) apiReq(method, path string, request, response any) error { - return s.apiReqWithHeaders(method, path, request, response, nil) -} - -func (s *session) apiReqWithHeaders(method, path string, request, response any, headers http.Header) error { - reqBody, err := json.Marshal(request) - if err != nil { - return err - } - - if headers == nil { - headers = make(http.Header) - } - headers.Set("Content-Type", "application/json") - - resBody, _, err := s.req(method, path, headers, bytes.NewReader(reqBody)) - if err != nil { - return err - } - defer resBody.Close() - - if resBody == nil { - return nil - } - - if response == nil { - io.Copy(io.Discard, resBody) - return nil - } - - return json.NewDecoder(resBody).Decode(response) -} - -// send message to client (in old format) -func (s *session) toClient(payload any) error { - s.muClient.Lock() - defer s.muClient.Unlock() - - err := s.connClient.WriteJSON(payload) - if err != nil { - return fmt.Errorf("%w: %s", ErrWebsocketSend, err) - } - - return nil -} - -// send message to backend (in new format) -func (s *session) toBackend(event string, payload any) error { - s.muBackend.Lock() - defer s.muBackend.Unlock() - - rawPayload, err := json.Marshal(payload) - if err != nil { - return err - } - - err = s.connBackend.WriteJSON(types.WebSocketMessage{ - Event: event, - Payload: rawPayload, - }) - if err != nil { - return fmt.Errorf("%w: %s", ErrWebsocketSend, err) - } - - return nil -} - -func (s *session) create(username, password string) error { - data := api.SessionDataPayload{} - if cookie := s.r.Header.Get("Cookie"); cookie != "" { - err := s.apiReqWithHeaders(http.MethodGet, "/api/whoami", nil, &data, http.Header{ - "Cookie": []string{cookie}, - }) - if err == nil { - s.cookie = cookie - s.setSessionData(data) - return nil - } - } - - err := s.apiReq(http.MethodPost, "/api/login", api.SessionLoginPayload{ - Username: username, - Password: password, - }, &data) - if err != nil { - return err - } - - s.setSessionData(data) - - if s.token == "" { - return fmt.Errorf("token not found") - } - - return nil -} - -func (s *session) setSessionData(data api.SessionDataPayload) { - s.id, s.ip = data.ID, getIp(s.r) - s.h.sessionIPs[s.id] = s.ip // save session ip by id - s.token = data.Token - s.name = data.Profile.Name - s.isAdmin = data.Profile.IsAdmin -} - -func (s *session) destroy() { - defer s.client.CloseIdleConnections() - - // logout session - err := s.apiReq(http.MethodPost, "/api/logout", nil, nil) - if err != nil { - s.logger.Error().Err(err).Msg("failed to logout") - } - - // remove session id from ip map - delete(s.h.sessionIPs, s.id) -} diff --git a/server/internal/http/legacy/session_test.go b/server/internal/http/legacy/session_test.go deleted file mode 100644 index 35101ff6..00000000 --- a/server/internal/http/legacy/session_test.go +++ /dev/null @@ -1,63 +0,0 @@ -package legacy - -import ( - "encoding/json" - "net/http" - "net/http/httptest" - "strings" - "testing" - - "github.com/m1k1o/neko/server/pkg/types" -) - -func TestCreateUsesExistingSessionCookie(t *testing.T) { - backend := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - switch r.URL.Path { - case "/api/whoami": - if r.Method != http.MethodGet { - t.Fatalf("method = %s, want GET", r.Method) - } - if got := r.Header.Get("Cookie"); got != "NEKO_SESSION=session-token" { - t.Fatalf("cookie = %q", got) - } - _ = json.NewEncoder(w).Encode(map[string]any{ - "id": "oauth:user-123", - "profile": types.MemberProfile{ - Name: "Ada Lovelace", - IsAdmin: true, - }, - }) - case "/api/protected": - if got := r.Header.Get("Cookie"); got != "NEKO_SESSION=session-token" { - t.Fatalf("authenticated request cookie = %q", got) - } - _ = json.NewEncoder(w).Encode(true) - default: - http.NotFound(w, r) - } - })) - defer backend.Close() - - request := httptest.NewRequest(http.MethodGet, "http://neko.test/ws", nil) - request.Header.Set("Cookie", "NEKO_SESSION=session-token") - handler := &LegacyHandler{sessionIPs: make(map[string]string)} - session := &session{ - r: request, - h: handler, - serverAddr: strings.TrimPrefix(backend.URL, "http://"), - client: backend.Client(), - } - - if err := session.create("", ""); err != nil { - t.Fatal(err) - } - if session.id != "oauth:user-123" || session.name != "Ada Lovelace" || !session.isAdmin { - t.Fatalf("session = %#v", session) - } - if session.cookie != "NEKO_SESSION=session-token" || session.token != "" { - t.Fatalf("authentication state = cookie %q token %q", session.cookie, session.token) - } - if err := session.apiReq(http.MethodGet, "/api/protected", nil, nil); err != nil { - t.Fatal(err) - } -} diff --git a/server/internal/http/legacy/types/types.go b/server/internal/http/legacy/types/types.go deleted file mode 100644 index 60590b2c..00000000 --- a/server/internal/http/legacy/types/types.go +++ /dev/null @@ -1,39 +0,0 @@ -package types - -import "time" - -type Stats struct { - Connections uint32 `json:"connections"` - Host string `json:"host"` - Members []*Member `json:"members"` - - Banned map[string]string `json:"banned"` // IP -> session ID (that banned it) - Locked map[string]string `json:"locked"` // resource name -> session ID (that locked it) - - ServerStartedAt time.Time `json:"server_started_at"` - LastAdminLeftAt *time.Time `json:"last_admin_left_at"` - LastUserLeftAt *time.Time `json:"last_user_left_at"` - - ControlProtection bool `json:"control_protection"` - ImplicitControl bool `json:"implicit_control"` -} - -type Member struct { - ID string `json:"id"` - Name string `json:"displayname"` - Avatar string `json:"avatar,omitempty"` - Admin bool `json:"admin"` - Muted bool `json:"muted"` -} - -type FileListItem struct { - Filename string `json:"name"` - Type string `json:"type"` - Size int64 `json:"size"` -} - -type ScreenConfiguration struct { - Width int `json:"width"` - Height int `json:"height"` - Rates map[int]int16 `json:"rates"` -} diff --git a/server/internal/http/legacy/wstobackend.go b/server/internal/http/legacy/wstobackend.go deleted file mode 100644 index 01483416..00000000 --- a/server/internal/http/legacy/wstobackend.go +++ /dev/null @@ -1,386 +0,0 @@ -package legacy - -import ( - "encoding/json" - "fmt" - "net/http" - - "github.com/pion/webrtc/v4" - - oldEvent "github.com/m1k1o/neko/server/internal/http/legacy/event" - oldMessage "github.com/m1k1o/neko/server/internal/http/legacy/message" - - "github.com/m1k1o/neko/server/internal/api/room" - "github.com/m1k1o/neko/server/internal/plugins/chat" - "github.com/m1k1o/neko/server/internal/plugins/filetransfer" - "github.com/m1k1o/neko/server/internal/plugins/openinapp" - "github.com/m1k1o/neko/server/pkg/types" - "github.com/m1k1o/neko/server/pkg/types/event" - "github.com/m1k1o/neko/server/pkg/types/message" -) - -func (s *session) wsToBackend(msg []byte) error { - header := oldMessage.Message{} - err := json.Unmarshal(msg, &header) - if err != nil { - return err - } - - switch header.Event { - // Client Events - case oldEvent.CLIENT_HEARTBEAT: - // do nothing - return nil - - // Signal Events - case oldEvent.SIGNAL_OFFER: - request := &oldMessage.SignalOffer{} - err := json.Unmarshal(msg, request) - if err != nil { - return err - } - - return s.toBackend(event.SIGNAL_OFFER, &message.SignalDescription{ - SDP: request.SDP, - }) - - case oldEvent.SIGNAL_ANSWER: - request := &oldMessage.SignalAnswer{} - err := json.Unmarshal(msg, request) - if err != nil { - return err - } - - if request.DisplayName != "" { - // Update display name if it is not set - if s.name == "" { - err = s.apiReq(http.MethodPost, "/api/profile", map[string]any{ - "name": request.DisplayName, - }, nil) - if err != nil { - return err - } - - s.name = request.DisplayName - } - } - - // try to set legacy video stream, if it fails, it will be ignored - if err := s.toBackend(event.SIGNAL_VIDEO, &message.SignalVideo{ - PeerVideoRequest: types.PeerVideoRequest{ - Selector: &types.StreamSelector{ - Type: types.StreamSelectorTypeExact, - ID: "legacy", - }, - }, - }); err != nil { - return err - } - - return s.toBackend(event.SIGNAL_ANSWER, &message.SignalDescription{ - SDP: request.SDP, - }) - - case oldEvent.SIGNAL_CANDIDATE: - request := &oldMessage.SignalCandidate{} - err := json.Unmarshal(msg, request) - if err != nil { - return err - } - - var candidate webrtc.ICECandidateInit - err = json.Unmarshal([]byte(request.Data), &candidate) - if err != nil { - return err - } - - return s.toBackend(event.SIGNAL_CANDIDATE, &message.SignalCandidate{ - ICECandidateInit: candidate, - }) - - // Control Events - case oldEvent.CONTROL_RELEASE: - return s.toBackend(event.CONTROL_RELEASE, nil) - - case oldEvent.CONTROL_REQUEST: - return s.toBackend(event.CONTROL_REQUEST, nil) - - case oldEvent.CONTROL_GIVE: - request := &oldMessage.Control{} - err := json.Unmarshal(msg, request) - if err != nil { - return err - } - - return s.apiReq(http.MethodPost, "/api/room/control/give/"+request.ID, nil, nil) - - case oldEvent.CONTROL_CLIPBOARD: - request := &oldMessage.Clipboard{} - err := json.Unmarshal(msg, request) - if err != nil { - return err - } - - return s.toBackend(event.CLIPBOARD_SET, &message.ClipboardData{ - Text: request.Text, - }) - - case oldEvent.CONTROL_KEYBOARD: - request := &oldMessage.Keyboard{} - err := json.Unmarshal(msg, request) - if err != nil { - return err - } - - if request.Layout != nil { - err = s.toBackend(event.KEYBOARD_MAP, &message.KeyboardMap{ - KeyboardMap: types.KeyboardMap{ - Layout: *request.Layout, - }, - }) - - if err != nil { - return err - } - } - - if request.CapsLock != nil || request.NumLock != nil || request.ScrollLock != nil { - err = s.toBackend(event.KEYBOARD_MODIFIERS, &message.KeyboardModifiers{ - KeyboardModifiers: types.KeyboardModifiers{ - CapsLock: request.CapsLock, - NumLock: request.NumLock, - // ScrollLock: request.ScrollLock, // ScrollLock is deprecated. - }, - }) - - if err != nil { - return err - } - } - - return nil - - // Chat Events - case oldEvent.CHAT_MESSAGE: - request := &oldMessage.ChatReceive{} - err := json.Unmarshal(msg, request) - if err != nil { - return err - } - - return s.toBackend(chat.CHAT_MESSAGE, &chat.Content{ - Text: request.Content, - }) - - case oldEvent.CHAT_EMOTE: - request := &oldMessage.EmoteReceive{} - err := json.Unmarshal(msg, request) - if err != nil { - return err - } - - // loopback emote - err = s.toClient(&oldMessage.EmoteSend{ - Event: oldEvent.CHAT_EMOTE, - ID: s.id, - Emote: request.Emote, - }) - if err != nil { - return err - } - - // broadcast emote to other users - return s.toBackend(event.SEND_BROADCAST, &message.SendBroadcast{ - Sender: s.id, - Subject: "emote", - Body: request.Emote, - }) - - // File Transfer Events - case oldEvent.FILETRANSFER_REFRESH: - return s.toBackend(filetransfer.FILETRANSFER_UPDATE, nil) - - // Open In App Events - case openinapp.OPENINAPP_OPENLINK: - request := &openinapp.Url{} - if err := json.Unmarshal(msg, request); err != nil { - return err - } - return s.apiReq(http.MethodPost, "/api/openinapp/openlink", request, nil) - - // Screen Events - case oldEvent.SCREEN_RESOLUTION: - response := &types.ScreenSize{} - err := s.apiReq(http.MethodGet, "/api/room/screen", nil, response) - if err != nil { - return err - } - - return s.toClient(&oldMessage.ScreenResolution{ - Event: oldEvent.SCREEN_RESOLUTION, - Width: response.Width, - Height: response.Height, - Rate: response.Rate, - }) - - case oldEvent.SCREEN_CONFIGURATIONS: - response := &[]types.ScreenSize{} - err := s.apiReq(http.MethodGet, "/api/room/screen/configurations", nil, response) - if err != nil { - return err - } - - return s.toClient(&oldMessage.ScreenConfigurations{ - Event: oldEvent.SCREEN_CONFIGURATIONS, - Configurations: screenConfigurations(*response), - }) - - case oldEvent.SCREEN_SET: - request := &oldMessage.ScreenResolution{} - err := json.Unmarshal(msg, request) - if err != nil { - return err - } - - return s.toBackend(event.SCREEN_SET, &message.ScreenSize{ - ScreenSize: types.ScreenSize{ - Width: request.Width, - Height: request.Height, - Rate: request.Rate, - }, - }) - - // Broadcast Events - case oldEvent.BROADCAST_CREATE: - request := &oldMessage.BroadcastCreate{} - err := json.Unmarshal(msg, request) - if err != nil { - return err - } - - return s.apiReq(http.MethodPost, "/api/room/broadcast/start", room.BroadcastStatusPayload{ - URL: request.URL, - IsActive: true, - }, nil) - - case oldEvent.BROADCAST_DESTROY: - return s.apiReq(http.MethodPost, "/api/room/broadcast/stop", nil, nil) - - // Admin Events - case oldEvent.ADMIN_LOCK: - request := &oldMessage.AdminLock{} - err := json.Unmarshal(msg, request) - if err != nil { - return err - } - - data := map[string]any{} - - switch request.Resource { - case "login": - data["locked_logins"] = true - case "control": - data["locked_controls"] = true - case "file_transfer": - data["plugins"] = map[string]any{ - "filetransfer.enabled": false, - } - default: - return fmt.Errorf("unknown resource: %s", request.Resource) - } - - return s.apiReq(http.MethodPost, "/api/room/settings", data, nil) - - case oldEvent.ADMIN_UNLOCK: - request := &oldMessage.AdminLock{} - err := json.Unmarshal(msg, request) - if err != nil { - return err - } - - data := map[string]any{} - - switch request.Resource { - case "login": - data["locked_logins"] = false - case "control": - data["locked_controls"] = false - case "file_transfer": - data["plugins"] = map[string]any{ - "filetransfer.enabled": true, - } - default: - return fmt.Errorf("unknown resource: %s", request.Resource) - } - - return s.apiReq(http.MethodPost, "/api/room/settings", data, nil) - - case oldEvent.ADMIN_CONTROL: - return s.apiReq(http.MethodPost, "/api/room/control/take", nil, nil) - - case oldEvent.ADMIN_RELEASE: - return s.apiReq(http.MethodPost, "/api/room/control/reset", nil, nil) - - case oldEvent.ADMIN_GIVE: - request := &oldMessage.Admin{} - err := json.Unmarshal(msg, request) - if err != nil { - return err - } - return s.apiReq(http.MethodPost, "/api/room/control/give/"+request.ID, nil, nil) - - case oldEvent.ADMIN_BAN: - request := &oldMessage.Admin{} - err := json.Unmarshal(msg, request) - if err != nil { - return err - } - - err = s.h.ban(request.ID) - if err != nil { - return err - } - - fallthrough // continue to kick - - case oldEvent.ADMIN_KICK: - request := &oldMessage.Admin{} - err := json.Unmarshal(msg, request) - if err != nil { - return err - } - - return s.apiReq(http.MethodPost, "/api/members/"+request.ID, map[string]any{ - "can_login": false, - }, nil) - - case oldEvent.ADMIN_MUTE: - request := &oldMessage.Admin{} - err := json.Unmarshal(msg, request) - if err != nil { - return err - } - - return s.apiReq(http.MethodPost, "/api/members/"+request.ID, map[string]any{ - "plugins": map[string]any{ - "chat.can_send": false, - }, - }, nil) - - case oldEvent.ADMIN_UNMUTE: - request := &oldMessage.Admin{} - err := json.Unmarshal(msg, request) - if err != nil { - return err - } - - return s.apiReq(http.MethodPost, "/api/members/"+request.ID, map[string]any{ - "plugins": map[string]any{ - "chat.can_send": true, - }, - }, nil) - - default: - return fmt.Errorf("unknown event type: %s", header.Event) - } -} diff --git a/server/internal/http/legacy/wstoclient.go b/server/internal/http/legacy/wstoclient.go deleted file mode 100644 index 571b87d4..00000000 --- a/server/internal/http/legacy/wstoclient.go +++ /dev/null @@ -1,778 +0,0 @@ -package legacy - -import ( - "encoding/json" - "errors" - "fmt" - - "github.com/pion/webrtc/v4" - "github.com/spf13/viper" - - oldEvent "github.com/m1k1o/neko/server/internal/http/legacy/event" - oldMessage "github.com/m1k1o/neko/server/internal/http/legacy/message" - oldTypes "github.com/m1k1o/neko/server/internal/http/legacy/types" - - "github.com/m1k1o/neko/server/internal/plugins/chat" - "github.com/m1k1o/neko/server/internal/plugins/filetransfer" - "github.com/m1k1o/neko/server/internal/plugins/openinapp" - "github.com/m1k1o/neko/server/pkg/types" - "github.com/m1k1o/neko/server/pkg/types/event" - "github.com/m1k1o/neko/server/pkg/types/message" -) - -func profileToMember(id string, profile types.MemberProfile) (*oldTypes.Member, error) { - settings := chat.Settings{ - CanSend: true, // defaults to true - CanReceive: true, // defaults to true - } - - err := profile.Plugins.Unmarshal(chat.PluginName, &settings) - if err != nil && !errors.Is(err, types.ErrPluginSettingsNotFound) { - return nil, fmt.Errorf("unable to unmarshal %s plugin settings from global settings: %w", chat.PluginName, err) - } - - return &oldTypes.Member{ - ID: id, - Name: profile.Name, - Avatar: profile.Avatar, - Admin: profile.IsAdmin, - Muted: !settings.CanSend, - }, nil -} - -func screenConfigurations(screenSizes []types.ScreenSize) map[int]oldTypes.ScreenConfiguration { - rates := map[string]map[int16]struct{}{} - for _, size := range screenSizes { - key := fmt.Sprintf("%dx%d", size.Width, size.Height) - if _, ok := rates[key]; !ok { - rates[key] = map[int16]struct{}{} - } - rates[key][size.Rate] = struct{}{} - } - - usedScreenSizes := map[string]struct{}{} - screenSizesList := map[int]oldTypes.ScreenConfiguration{} - for i, size := range screenSizes { - key := fmt.Sprintf("%dx%d", size.Width, size.Height) - if _, ok := usedScreenSizes[key]; ok { - continue - } - usedScreenSizes[key] = struct{}{} - - c, ratesMap := 0, map[int]int16{} - for rate := range rates[key] { - ratesMap[c] = rate - c++ - } - - screenSizesList[i] = oldTypes.ScreenConfiguration{ - Width: size.Width, - Height: size.Height, - Rates: ratesMap, - } - } - - return screenSizesList -} - -func (s *session) settingsToLocks(settings types.Settings) (map[string]string, error) { - // - // FileTransfer - // - - filetransferSettings := filetransfer.Settings{ - Enabled: true, // defaults to true - } - - err := settings.Plugins.Unmarshal(filetransfer.PluginName, &filetransferSettings) - if err != nil && !errors.Is(err, types.ErrPluginSettingsNotFound) { - return nil, fmt.Errorf("unable to unmarshal %s plugin settings from global settings: %w", filetransfer.PluginName, err) - } - - // - // Locks - // - - locks := map[string]string{} - if settings.LockedLogins { - locks["login"] = "" // TODO: We don't know who locked the login. - s.lockedLogins = true - } - if settings.LockedControls { - locks["control"] = "" // TODO: We don't know who locked the control. - s.lockedControls = true - } - if !filetransferSettings.Enabled { - locks["file_transfer"] = "" // TODO: We don't know who locked the file transfer. - s.lockedFileTransfer = true - } - - return locks, nil -} - -func (s *session) sendControlHost(request message.ControlHost) error { - lastHostID := s.lastHostID - - if request.HasHost { - s.lastHostID = request.ID - - if request.ID == request.HostID { - if request.ID == lastHostID || lastHostID == "" { - return s.toClient(&oldMessage.Control{ - Event: oldEvent.CONTROL_LOCKED, - ID: request.HostID, - }) - } else { - return s.toClient(&oldMessage.AdminTarget{ - Event: oldEvent.ADMIN_CONTROL, - ID: request.ID, - Target: lastHostID, - }) - } - } else if request.ID == "" { - return s.toClient(&oldMessage.Control{ - Event: oldEvent.CONTROL_LOCKED, - ID: request.HostID, - }) - } else { - return s.toClient(&oldMessage.ControlTarget{ - Event: oldEvent.CONTROL_GIVE, - ID: request.ID, - Target: request.HostID, - }) - } - } - - if request.ID != "" { - s.lastHostID = "" - - if request.ID == lastHostID { - return s.toClient(&oldMessage.Control{ - Event: oldEvent.CONTROL_RELEASE, - ID: request.ID, - }) - } else { - return s.toClient(&oldMessage.Control{ - Event: oldEvent.ADMIN_RELEASE, - ID: request.ID, - }) - } - } - - return nil -} - -func (s *session) wsToClient(msg []byte) error { - data := types.WebSocketMessage{} - err := json.Unmarshal(msg, &data) - if err != nil { - return err - } - - switch data.Event { - // System Events - case event.SYSTEM_DISCONNECT: - request := &message.SystemDisconnect{} - err := json.Unmarshal(data.Payload, request) - if err != nil { - return err - } - - // if profile change is the reason for disconnect, it's a kick event - if request.Message == "profile changed" { - request.Message = "kicked" - } - - return s.toClient(&oldMessage.SystemMessage{ - Event: oldEvent.SYSTEM_DISCONNECT, - Message: request.Message, - }) - - case event.SYSTEM_INIT: - request := &message.SystemInit{} - err := json.Unmarshal(data.Payload, request) - if err != nil { - return err - } - - // - // MembersList - // - - membersList := []*oldTypes.Member{} - s.sessions = map[string]*memberStruct{} - for id, session := range request.Sessions { - if !session.State.IsConnected { - continue - } - member, err := profileToMember(id, session.Profile) - if err != nil { - return err - } - membersList = append(membersList, member) - s.sessions[id] = &memberStruct{ - sent: member.Name != "", - connected: true, - member: member, - } - } - - err = s.toClient(&oldMessage.MembersList{ - Event: oldEvent.MEMBER_LIST, - Members: membersList, - }) - if err != nil { - return err - } - - // - // ScreenSize - // - - err = s.toClient(&oldMessage.ScreenResolution{ - Event: oldEvent.SCREEN_RESOLUTION, - Width: request.ScreenSize.Width, - Height: request.ScreenSize.Height, - Rate: request.ScreenSize.Rate, - }) - if err != nil { - return err - } - - // actually its already set when we create the session - s.id = request.SessionId - - // - // ControlHost - // - - err = s.sendControlHost(request.ControlHost) - if err != nil { - return err - } - - locks, err := s.settingsToLocks(request.Settings) - if err != nil { - return err - } - - return s.toClient(&oldMessage.SystemInit{ - Event: oldEvent.SYSTEM_INIT, - ImplicitHosting: request.Settings.ImplicitHosting, - Locks: locks, - // TODO: hack - we don't know if file transfer is enabled, we would need to check the global config. - FileTransfer: viper.GetBool("filetransfer.enabled") || (viper.GetBool("legacy") && viper.GetBool("file_transfer_enabled")), - HeartbeatInterval: request.Settings.HeartbeatInterval, - }) - - case event.SYSTEM_ADMIN: - request := &message.SystemAdmin{} - err := json.Unmarshal(data.Payload, request) - if err != nil { - return err - } - - // - // ScreenSizesList - // - - err = s.toClient(&oldMessage.ScreenConfigurations{ - Event: oldEvent.SCREEN_CONFIGURATIONS, - Configurations: screenConfigurations(request.ScreenSizesList), - }) - if err != nil { - return err - } - - // - // BroadcastStatus - // - - return s.toClient(&oldMessage.BroadcastStatus{ - Event: oldEvent.BROADCAST_STATUS, - URL: request.BroadcastStatus.URL, - IsActive: request.BroadcastStatus.IsActive, - }) - - // Member Events - - case event.SESSION_CREATED: - request := &message.SessionData{} - err := json.Unmarshal(data.Payload, request) - if err != nil { - return err - } - - member, err := profileToMember(request.ID, request.Profile) - if err != nil { - return err - } - - // only save session - will be notified on connect - s.sessions[request.ID] = &memberStruct{ - member: member, - } - - return nil - - case event.SESSION_DELETED: - request := &message.SessionID{} - err := json.Unmarshal(data.Payload, request) - if err != nil { - return err - } - - // only continue if session is in the list - should have been already removed - if _, ok := s.sessions[request.ID]; !ok { - return nil - } - - delete(s.sessions, request.ID) - - return s.toClient(&oldMessage.MemberDisconnected{ - Event: oldEvent.MEMBER_DISCONNECTED, - ID: request.ID, - }) - - case event.SESSION_PROFILE: - request := &message.MemberProfile{} - err := json.Unmarshal(data.Payload, request) - if err != nil { - return err - } - - // session profile is expected to change when updating a name after connecting - m, ok := s.sessions[request.ID] - if !ok || m == nil { - return nil - } - - // update member profile - member, err := profileToMember(request.ID, request.MemberProfile) - if err != nil { - return err - } - - mutedChanged := m.member.Muted != member.Muted - m.member = member - - if m.connected && !m.sent && member.Name != "" { - m.sent = true - - // oldEvent.MEMBER_CONNECTED if not sent already - err = s.toClient(&oldMessage.Member{ - Event: oldEvent.MEMBER_CONNECTED, - Member: member, - }) - if err != nil { - return err - } - } - - if mutedChanged && member.Muted { - return s.toClient(&oldMessage.AdminTarget{ - Event: oldEvent.ADMIN_MUTE, - ID: "", // TODO: We don't know who (un)muted the user. - Target: request.ID, - }) - } else if mutedChanged && !member.Muted { - return s.toClient(&oldMessage.AdminTarget{ - Event: oldEvent.ADMIN_UNMUTE, - ID: "", // TODO: We don't know who (un)muted the user. - Target: request.ID, - }) - } - - return nil - - case event.SESSION_STATE: - request := &message.SessionState{} - err := json.Unmarshal(data.Payload, request) - if err != nil { - return err - } - - m, ok := s.sessions[request.ID] - if !ok { - return nil - } - - if request.IsConnected { - m.connected = true - - if m.member.Muted { - err = s.toClient(&oldMessage.AdminTarget{ - Event: oldEvent.ADMIN_MUTE, - ID: "", // TODO: We don't know who (un)muted the user. - Target: request.ID, - }) - if err != nil { - return err - } - } - - if !m.sent && m.member.Name != "" { - m.sent = true - - // oldEvent.MEMBER_CONNECTED if not sent already - return s.toClient(&oldMessage.Member{ - Event: oldEvent.MEMBER_CONNECTED, - Member: m.member, - }) - } - } - - if !request.IsConnected { - delete(s.sessions, request.ID) - - // oldEvent.MEMBER_DISCONNECTED if nor sent already - return s.toClient(&oldMessage.MemberDisconnected{ - Event: oldEvent.MEMBER_DISCONNECTED, - ID: request.ID, - }) - } - - return nil - - // Signal Events - case event.SIGNAL_VIDEO, event.SIGNAL_AUDIO: - // ignore video and audio signals, because they are not part of the legacy protocol - return nil - - case event.SIGNAL_OFFER: - request := &message.SignalDescription{} - err := json.Unmarshal(data.Payload, request) - if err != nil { - return err - } - - return s.toClient(&oldMessage.SignalOffer{ - Event: oldEvent.SIGNAL_OFFER, - SDP: request.SDP, - }) - - case event.SIGNAL_ANSWER: - request := &message.SignalDescription{} - err := json.Unmarshal(data.Payload, request) - if err != nil { - return err - } - - return s.toClient(&oldMessage.SignalAnswer{ - Event: oldEvent.SIGNAL_ANSWER, - DisplayName: s.name, - SDP: request.SDP, - }) - - case event.SIGNAL_CANDIDATE: - request := &message.SignalCandidate{} - err := json.Unmarshal(data.Payload, request) - if err != nil { - return err - } - - json, err := json.Marshal(request.ICECandidateInit) - if err != nil { - return err - } - - return s.toClient(&oldMessage.SignalCandidate{ - Event: oldEvent.SIGNAL_CANDIDATE, - Data: string(json), - }) - - case event.SIGNAL_PROVIDE: - request := &message.SignalProvide{} - err := json.Unmarshal(data.Payload, request) - if err != nil { - return err - } - - iceServers := []webrtc.ICEServer{} - for _, ice := range request.ICEServers { - iceServers = append(iceServers, webrtc.ICEServer{ - URLs: ice.URLs, - Username: ice.Username, - Credential: ice.Credential, - CredentialType: webrtc.ICECredentialTypePassword, - }) - } - - return s.toClient(&oldMessage.SignalProvide{ - Event: oldEvent.SIGNAL_PROVIDE, - ID: s.id, // SessionId - SDP: request.SDP, - Lite: len(iceServers) == 0, // if no ICE servers are provided, it's a lite offer - ICE: iceServers, - }) - - // Control Events - case event.CLIPBOARD_UPDATED: - request := &message.ClipboardData{} - err := json.Unmarshal(data.Payload, request) - if err != nil { - return err - } - - return s.toClient(&oldMessage.Clipboard{ - Event: oldEvent.CONTROL_CLIPBOARD, - Text: request.Text, - }) - - case event.CONTROL_HOST: - request := &message.ControlHost{} - err := json.Unmarshal(data.Payload, request) - if err != nil { - return err - } - - return s.sendControlHost(*request) - - case event.CONTROL_REQUEST: - request := &message.SessionID{} - err := json.Unmarshal(data.Payload, request) - if err != nil { - return err - } - - if s.id == request.ID { - // if i am the one that is requesting, send CONTROL_REQUEST to me - return s.toClient(&oldMessage.Control{ - Event: oldEvent.CONTROL_REQUEST, - ID: request.ID, - }) - } else { - // if not, let me know someone else is requesting - return s.toClient(&oldMessage.Control{ - Event: oldEvent.CONTROL_REQUESTING, - ID: request.ID, - }) - } - - // Chat Events - case chat.CHAT_INIT: - // ignore chat init, because it is not part of the legacy protocol - return nil - - case chat.CHAT_MESSAGE: - request := &chat.Message{} - err := json.Unmarshal(data.Payload, request) - if err != nil { - return err - } - - return s.toClient(&oldMessage.ChatSend{ - Event: oldEvent.CHAT_MESSAGE, - ID: request.ID, - Content: request.Content.Text, - }) - - case event.SEND_BROADCAST: - request := &message.SendBroadcast{} - err := json.Unmarshal(data.Payload, request) - if err != nil { - return err - } - - if request.Subject == "emote" { - return s.toClient(&oldMessage.EmoteSend{ - Event: oldEvent.CHAT_EMOTE, - ID: request.Sender, - Emote: request.Body.(string), - }) - } - - return nil - - // File Transfer Events - case filetransfer.FILETRANSFER_UPDATE: - request := &filetransfer.Message{} - err := json.Unmarshal(data.Payload, request) - if err != nil { - return err - } - - files := []oldTypes.FileListItem{} - for _, file := range request.Files { - var itemType string - switch file.Type { - case filetransfer.ItemTypeFile: - itemType = "file" - case filetransfer.ItemTypeDir: - itemType = "dir" - } - files = append(files, oldTypes.FileListItem{ - Filename: file.Name, - Type: itemType, - Size: file.Size, - }) - } - - return s.toClient(&oldMessage.FileTransferList{ - Event: oldEvent.FILETRANSFER_LIST, - Cwd: request.RootDir, - UserDownload: request.UserDownload, - UserUpload: request.UserUpload, - UserDelete: request.UserDelete, - Files: files, - }) - - // Open In App Events - case openinapp.OPENINAPP_INIT: - request := &openinapp.Init{} - if err := json.Unmarshal(data.Payload, request); err != nil { - return err - } - return s.toClient(&struct { - Event string `json:"event"` - Enabled bool `json:"enabled"` - }{ - Event: openinapp.OPENINAPP_INIT, - Enabled: request.Enabled, - }) - - // Screen Events - case event.SCREEN_UPDATED: - request := &message.ScreenSizeUpdate{} - err := json.Unmarshal(data.Payload, request) - if err != nil { - return err - } - - return s.toClient(&oldMessage.ScreenResolution{ - Event: oldEvent.SCREEN_RESOLUTION, - ID: request.ID, - Width: request.ScreenSize.Width, - Height: request.ScreenSize.Height, - Rate: request.ScreenSize.Rate, - }) - - // Broadcast Events - case event.BROADCAST_STATUS: - request := &message.BroadcastStatus{} - err := json.Unmarshal(data.Payload, request) - if err != nil { - return err - } - - return s.toClient(&oldMessage.BroadcastStatus{ - Event: oldEvent.BROADCAST_STATUS, - URL: request.URL, - IsActive: request.IsActive, - }) - - // Admin Events - case event.SYSTEM_SETTINGS: - request := &message.SystemSettingsUpdate{} - err := json.Unmarshal(data.Payload, request) - if err != nil { - return err - } - - if s.lockedControls != request.LockedControls { - s.lockedControls = request.LockedControls - - if request.LockedControls { - err = s.toClient(&oldMessage.AdminLock{ - Event: oldEvent.ADMIN_LOCK, - Resource: "control", - ID: request.ID, - }) - } else { - err = s.toClient(&oldMessage.AdminLock{ - Event: oldEvent.ADMIN_UNLOCK, - Resource: "control", - ID: request.ID, - }) - } - - if err != nil { - return err - } - } - - if s.lockedLogins != request.LockedLogins { - s.lockedLogins = request.LockedLogins - - if request.LockedLogins { - err = s.toClient(&oldMessage.AdminLock{ - Event: oldEvent.ADMIN_LOCK, - Resource: "login", - ID: request.ID, - }) - } else { - err = s.toClient(&oldMessage.AdminLock{ - Event: oldEvent.ADMIN_UNLOCK, - Resource: "login", - ID: request.ID, - }) - } - - if err != nil { - return err - } - } - - // - // FileTransfer - // - - filetransferSettings := filetransfer.Settings{ - Enabled: true, // defaults to true - } - - err = request.Settings.Plugins.Unmarshal(filetransfer.PluginName, &filetransferSettings) - if err != nil && !errors.Is(err, types.ErrPluginSettingsNotFound) { - return fmt.Errorf("unable to unmarshal %s plugin settings from global settings: %w", filetransfer.PluginName, err) - } - - if s.lockedFileTransfer != !filetransferSettings.Enabled { - s.lockedFileTransfer = !filetransferSettings.Enabled - - if !filetransferSettings.Enabled { - err = s.toClient(&oldMessage.AdminLock{ - Event: oldEvent.ADMIN_LOCK, - Resource: "file_transfer", - ID: request.ID, - }) - } else { - err = s.toClient(&oldMessage.AdminLock{ - Event: oldEvent.ADMIN_UNLOCK, - Resource: "file_transfer", - ID: request.ID, - }) - } - - if err != nil { - return err - } - } - - return nil - - /* - case: - s.toClient(&oldMessage.AdminTarget{ - Event: oldEvent.ADMIN_BAN, - }) - case: - s.toClient(&oldMessage.AdminTarget{ - Event: oldEvent.ADMIN_KICK, - }) - case: - s.toClient(&oldMessage.AdminTarget{ - Event: oldEvent.ADMIN_MUTE, - }) - case: - s.toClient(&oldMessage.AdminTarget{ - Event: oldEvent.ADMIN_UNMUTE, - }) - */ - - case event.SYSTEM_HEARTBEAT: - return nil - - default: - return fmt.Errorf("unknown event type: %s", data.Event) - } -} diff --git a/server/internal/http/legacy/wstoclient_test.go b/server/internal/http/legacy/wstoclient_test.go deleted file mode 100644 index b9ae893b..00000000 --- a/server/internal/http/legacy/wstoclient_test.go +++ /dev/null @@ -1,20 +0,0 @@ -package legacy - -import ( - "testing" - - "github.com/m1k1o/neko/server/pkg/types" -) - -func TestProfileToMemberPreservesAvatar(t *testing.T) { - member, err := profileToMember("oauth:user-123", types.MemberProfile{ - Name: "Ada Lovelace", - Avatar: "https://example.test/ada.png", - }) - if err != nil { - t.Fatal(err) - } - if member.Name != "Ada Lovelace" || member.Avatar != "https://example.test/ada.png" { - t.Fatalf("member = %#v", member) - } -} diff --git a/server/internal/http/manager.go b/server/internal/http/manager.go index 8593acee..3be60f58 100644 --- a/server/internal/http/manager.go +++ b/server/internal/http/manager.go @@ -2,7 +2,6 @@ package http import ( "context" - "net" "net/http" "os" "path" @@ -10,10 +9,8 @@ import ( "github.com/prometheus/client_golang/prometheus/promhttp" "github.com/rs/zerolog" "github.com/rs/zerolog/log" - "github.com/spf13/viper" "github.com/m1k1o/neko/server/internal/config" - "github.com/m1k1o/neko/server/internal/http/legacy" "github.com/m1k1o/neko/server/pkg/types" ) @@ -139,24 +136,6 @@ func (manager *HttpManagerCtx) Start() { } }() manager.logger.Info().Msgf("https listening on %s", manager.http.Addr) - - // if we have legacy mode, we need to start local http server too - if viper.GetBool("legacy") { - // create a listener for the API server with a random port - listener, err := net.Listen("tcp", "127.0.0.1:0") - if err != nil { - manager.logger.Panic().Err(err).Msg("unable to start legacy http proxy") - } - - go func() { - if err := http.Serve(listener, manager.router); err != http.ErrServerClosed { - manager.logger.Panic().Err(err).Msg("unable to start http server") - } - }() - manager.logger.Info().Msgf("legacy proxy listening on %s", listener.Addr().String()) - - legacy.New(listener.Addr().String(), manager.config.PathPrefix).Route(manager.router) - } } else { go func() { if err := manager.http.ListenAndServe(); err != http.ErrServerClosed { @@ -164,11 +143,6 @@ func (manager *HttpManagerCtx) Start() { } }() manager.logger.Info().Msgf("http listening on %s", manager.http.Addr) - - // start legacy proxy if enabled - if viper.GetBool("legacy") { - legacy.New(manager.http.Addr, manager.config.PathPrefix).Route(manager.router) - } } } diff --git a/server/internal/session/session.go b/server/internal/session/session.go index 4b4c0a99..dc591e47 100644 --- a/server/internal/session/session.go +++ b/server/internal/session/session.go @@ -47,16 +47,11 @@ func (session *SessionCtx) profileChanged() { } if (!session.profile.CanConnect || !session.profile.CanLogin || !session.profile.CanWatch) && session.state.IsWatching { - // TODO: Needed for legacy implementation. Websocket must die before webrtc and deliver signal close message - // otherwise webrtc destroy would trigger websocket reconnect. In case of kick event, webrtc destroy is called - // before websocket destroy that delivers the information about the kick. - time.AfterFunc(time.Second, func() { - // The peer may have been removed if the user disconnected while - // waiting for the delayed teardown. - if webrtcPeer := session.GetWebRTCPeer(); webrtcPeer != nil { - webrtcPeer.Destroy() - } - }) + // The peer may have been removed if the user disconnected while + // waiting for the delayed teardown. + if webrtcPeer := session.GetWebRTCPeer(); webrtcPeer != nil { + webrtcPeer.Destroy() + } } if (!session.profile.CanConnect || !session.profile.CanLogin) && session.state.IsConnected { @@ -77,18 +72,6 @@ func (session *SessionCtx) IsHost() bool { return session.manager.isHost(session) } -// only needed for legacy webrtc handler -func (session *SessionCtx) LegacyIsHost() bool { - settings := session.manager.Settings() - if !session.profile.CanHost || session.PrivateModeEnabled() { - return false - } - if settings.LockedControls && !session.profile.IsAdmin { - return false - } - return settings.ImplicitHosting || session.manager.isHost(session) -} - func (session *SessionCtx) SetAsHost() { session.manager.setHost(session, session) } diff --git a/server/internal/webrtc/legacyhandler.go b/server/internal/webrtc/legacyhandler.go deleted file mode 100644 index 1185e6e0..00000000 --- a/server/internal/webrtc/legacyhandler.go +++ /dev/null @@ -1,140 +0,0 @@ -package webrtc - -import ( - "bytes" - "encoding/binary" - "strconv" - - "github.com/m1k1o/neko/server/pkg/types" - - "github.com/rs/zerolog" -) - -const ( - OP_MOVE = 0x01 - OP_SCROLL = 0x02 - OP_KEY_DOWN = 0x03 - OP_KEY_UP = 0x04 - OP_KEY_CLK = 0x05 -) - -type PayloadHeader struct { - Event uint8 - Length uint16 -} - -type PayloadMove struct { - PayloadHeader - X uint16 - Y uint16 -} - -type PayloadScroll struct { - PayloadHeader - X int16 - Y int16 -} - -type PayloadKey struct { - PayloadHeader - Key uint64 // TODO: uint32 -} - -func (manager *WebRTCManagerCtx) handleLegacy( - logger zerolog.Logger, data []byte, - session types.Session, -) error { - // continue only if session is host - if !session.LegacyIsHost() { - return nil - } - - buffer := bytes.NewBuffer(data) - header := &PayloadHeader{} - hbytes := make([]byte, 3) - - if _, err := buffer.Read(hbytes); err != nil { - return err - } - - if err := binary.Read(bytes.NewBuffer(hbytes), binary.LittleEndian, header); err != nil { - return err - } - - buffer = bytes.NewBuffer(data) - - switch header.Event { - case OP_MOVE: - payload := &PayloadMove{} - if err := binary.Read(buffer, binary.LittleEndian, payload); err != nil { - return err - } - - manager.desktop.Move(int(payload.X), int(payload.Y)) - case OP_SCROLL: - payload := &PayloadScroll{} - if err := binary.Read(buffer, binary.LittleEndian, payload); err != nil { - return err - } - - logger. - Trace(). - Str("x", strconv.Itoa(int(payload.X))). - Str("y", strconv.Itoa(int(payload.Y))). - Msg("scroll") - - manager.desktop.Scroll(int(payload.X), int(payload.Y), false) - case OP_KEY_DOWN: - payload := &PayloadKey{} - if err := binary.Read(buffer, binary.LittleEndian, payload); err != nil { - return err - } - - if payload.Key < 8 { - err := manager.desktop.ButtonDown(uint32(payload.Key)) - if err != nil { - logger.Warn().Err(err).Msg("button down failed") - return nil - } - - logger.Trace().Msgf("button down %d", payload.Key) - } else { - err := manager.desktop.KeyDown(uint32(payload.Key)) - if err != nil { - logger.Warn().Err(err).Msg("key down failed") - return nil - } - - logger.Trace().Msgf("key down %d", payload.Key) - } - case OP_KEY_UP: - payload := &PayloadKey{} - err := binary.Read(buffer, binary.LittleEndian, payload) - if err != nil { - return err - } - - if payload.Key < 8 { - err := manager.desktop.ButtonUp(uint32(payload.Key)) - if err != nil { - logger.Warn().Err(err).Msg("button up failed") - return nil - } - - logger.Trace().Msgf("button up %d", payload.Key) - } else { - err := manager.desktop.KeyUp(uint32(payload.Key)) - if err != nil { - logger.Warn().Err(err).Msg("key up failed") - return nil - } - - logger.Trace().Msgf("key up %d", payload.Key) - } - case OP_KEY_CLK: - // unused - break - } - - return nil -} diff --git a/server/internal/webrtc/manager.go b/server/internal/webrtc/manager.go index 1a0c71e6..13d2bdcc 100644 --- a/server/internal/webrtc/manager.go +++ b/server/internal/webrtc/manager.go @@ -18,7 +18,6 @@ import ( "github.com/pion/webrtc/v4" "github.com/rs/zerolog" "github.com/rs/zerolog/log" - "github.com/spf13/viper" "github.com/m1k1o/neko/server/internal/config" "github.com/m1k1o/neko/server/internal/webrtc/cursor" @@ -485,23 +484,6 @@ func (manager *WebRTCManagerCtx) CreatePeer(session types.Session) (*webrtc.Sess connection.OnDataChannel(func(dc *webrtc.DataChannel) { logger.Info().Interface("data_channel", dc).Msg("got remote data channel") - - // - // old implementation created a new data channel on client side - // new implementation creates a new data channel on server side - // - - if viper.GetBool("legacy") { - // handle legacy data channel - dc.OnMessage(func(message webrtc.DataChannelMessage) { - if err := manager.handleLegacy(logger, message.Data, session); err != nil { - logger.Err(err).Msg("data handle failed") - } - }) - - // handle legacy data channel - peer.dataChannel = dc - } }) var once sync.Once diff --git a/server/pkg/types/session.go b/server/pkg/types/session.go index cb2b3ab3..7179a6c8 100644 --- a/server/pkg/types/session.go +++ b/server/pkg/types/session.go @@ -69,7 +69,6 @@ type Session interface { Profile() MemberProfile State() SessionState IsHost() bool - LegacyIsHost() bool SetAsHost() SetAsHostBy(session Session) ClearHost() diff --git a/webpage/docs/migration-from-v2/README.md b/webpage/docs/migration-from-v2/README.md index b8f9bded..8ccba18f 100644 --- a/webpage/docs/migration-from-v2/README.md +++ b/webpage/docs/migration-from-v2/README.md @@ -1,14 +1,6 @@ # Migration from V2 -Currently, Neko is in compatibility mode, meaning that as soon as a single V2 configuration option is set, the legacy mode is enabled. This approach allows for a smooth transition from V2 to V3, where it does not expose the V2 API for new users but still allows existing users who use the old configuration to continue using it as before. - -The legacy mode includes a compatibility layer that allows V2 clients to connect to V3. Currently, the **client is not compatible with V3**, so the legacy mode is enabled by default. It can be explicitly enabled or disabled by setting the `NEKO_LEGACY` environment variable to `true` or `false`. - -:::warning -The legacy mode is **still used by the client**. It is recommended to migrate to the new configuration options, but do not disable the legacy mode unless you are using a new client that is compatible with V3 (e.g., [demodesk/neko-client](https://github.com/demodesk/neko-client)). Once the new client is released, the legacy mode will be automatically removed from the server. -::: - -If you set both V3 and V2 configuration options, the V2 configuration options will take precedence over the V3 configuration options. This is to ensure that the legacy mode works as expected and does not break existing configurations. +Neko v3 accepts many V2 configuration options as deprecated aliases, which are mapped to their V3 equivalents. When both forms are set, the V2 option takes precedence. The V2 API and client compatibility layer have been removed; clients must use the V3 API. ## Docker Images {#docker} @@ -20,7 +12,7 @@ All applications available in the V2 images are also available in the V3 images. ## Configuration {#config} -V3 is compatible with V2 configuration options when legacy support is enabled. You should be able to run V3 with the V2 configuration without any issues. +V3 continues to accept the V2 configuration aliases listed below. This does not enable or restore the V2 API. The configuration in Neko V3 has been structured differently compared to V2. The V3 configuration is more modular and allows for more flexibility. The V3 configuration is split into multiple sections, each section is responsible for a specific part of the application. This allows for better organization and easier management of the configuration. @@ -63,9 +55,7 @@ See the V3 configuration options for the [WebRTC Video](/docs/v3/configuration/c | `NEKO_MAX_FPS` | **removed**, use [custom pipeline](/docs/v3/configuration/capture#video.gst_pipeline) instead | -:::warning Limitation -V2 did not have client-side cursor support, the cursor was always part of the video stream. In V3, the cursor is sent separately from the video stream. Therefore, when using legacy configuration, there will be two video streams created, one with the cursor (for V2 clients) and one without the cursor (for V3 clients). Please consider using new configuration options if this is not the desired behavior. -::: +V2 video settings map to a single V3 video pipeline. The V3 client renders the cursor separately from the video stream. ### WebRTC Audio {#config.audio} @@ -109,7 +99,7 @@ See the V3 configuration options for the [Authentication](/docs/v3/configuration | `NEKO_PASSWORD` | `NEKO_MEMBER_MULTIUSER_USER_PASSWORD` with `NEKO_MEMBER_PROVIDER=multiuser` | | `NEKO_PASSWORD_ADMIN` | `NEKO_MEMBER_MULTIUSER_ADMIN_PASSWORD` with `NEKO_MEMBER_PROVIDER=multiuser` | -In order for the legacy authentication to work, you need to set [Multi-user](/docs/v3/configuration/authentication#member.multiuser). +The V2 password settings map to the [Multi-user](/docs/v3/configuration/authentication#member.multiuser) provider. :::warning Limitation V2 clients might not be compatible with any other authentication provider than the `multiuser`. @@ -131,7 +121,7 @@ See the V3 configuration options for the [WebRTC](/docs/v3/configuration/webrtc) ### Full V2 Configuration Reference {#config.full} -Here is a full list of all the configuration options available in Neko V2 that are still available in Neko V3 with legacy support enabled. +Here is a full list of V2 configuration options that remain available as deprecated aliases in Neko V3. import { ConfigurationTab } from '@site/src/components/Configuration'; import configOptions from './help.json'; @@ -142,95 +132,10 @@ See the full [V3 configuration reference](/docs/v3/configuration/#full) for more ## API {#api} -V3 is compatible with the V2 API when legacy support is enabled. There was specifically created a compatibility layer (legacy API) that allows V2 clients to connect to V3. The legacy API is enabled by default, but it can be disabled if needed. In later versions, the legacy API will be removed. - -### Authentication {#api.auth} - -In V2 there was only one authentication provider available, as in V3 called the [`multiuser`](/docs/v3/configuration/authentication#member.multiuser) provider. The API knew based on the provided password (as `?pwd=` query string) if the user is an admin or not. - -Since V3 handles authentication differently (see [API documentation](/docs/v3/api#authentication)), there has been added `?usr=` query string to the API to specify the username. The password is still provided as `?pwd=` query string. The `?usr=` query string is still optional, if not provided, the API will generate a random username. - -:::warning Limitation -For every request in the legacy API, a new user session is created based on the `?usr=&pwd=` query string. The session is destroyed after the API request is completed. So for HTTP API requests, the sessions are short-lived but for WebSocket API requests, the session is kept alive until the WebSocket connection is closed. -::: - -Only the [`multiuser`](/docs/v3/configuration/authentication#member.multiuser) provider (or the [`noauth`](/docs/v3/configuration/authentication#member.noauth) provider) is supported without specifying the `?usr=` query string. - -### WebSocket Messages {#api.ws} - -Since WebSocket messages are not user-facing API, there exists no migration guide for them. When the legacy API is enabled, the user connects to the `/ws` endpoint and is handled by the compatibility layer V2 API. The V3 API is available at the `/api/ws` endpoint. - -V2 used to send WebSocket ping messages every 60 seconds, whereas V3 sends them every 10 seconds and additionally uses a heartbeat mechanism to verify if the connection is still active. - -### WebRTC API {#api.webrtc} - -Since the WebRTC API is not user-facing API, there exists no migration guide for it. It has been changed to Big Endian format (previously Little Endian) to allow easier manipulation on the client side. -V2 created a new data channel on the client side, V3 creates a new data channel on the server side. That means, the server just listens for a new data channel from the client and accepts it with the legacy API handler. It overwrites the existing V3 data channel with the legacy one. - -### HTTP API {#api.http} - -The V2 version had a very limited HTTP API, the V3 API is much more powerful and flexible. See the [API documentation](/docs/v3/api) for more details. - -#### GET `/health` {#api.http.health} - -Migrated to the [Health](/docs/v3/api/healthcheck) endpoint for server health checks. - -Returns `200 OK` if the server is running. - -#### GET `/stats` {#api.http.stats} - -Migrated to the [Stats](/docs/v3/api/stats) endpoint for server statistics and the [List Sessions](/docs/v3/api/sessions-get) endpoint for the session list. - -Returns a JSON object with the following structure: - -```json -{ - // How many connections are currently active - "connections": 0, - // Who is currently having a session (empty if no one) - "host": "", - // List of currently connected users - "members": [ - { - "session_id": "", - "displayname": "Name", - "admin": true, - "muted": false, - } - ], - // List of banned IPs and who banned them as a session_id - "banned": { - "": "" - }, - // List of locked resources and who locked them as a session_id - "locked": { - "": "" - }, - // Server uptime - "server_started_at": "2021-01-01T00:00:00Z", - // When was the last admin or user left the session - "last_admin_left_at": "2021-01-01T00:00:00Z", - "last_user_left_at": "2021-01-01T00:00:00Z", - // Whether the control protection or implicit control is enabled - "control_protection": false, - "implicit_control": false, -} -``` - -#### GET `/screenshot.jpg` {#api.http.screenshot} - -Migrated to the [Screenshot](/docs/v3/api/screen-shot-image) endpoint for taking screenshots. - -Returns a screenshot of the desktop as a JPEG image. - -#### GET `/file` {#api.http.file} - -The whole functionality of file transfer has been moved to a [File Transfer Plugin](/docs/v3/configuration/plugins#filetransfer). +The V2 API compatibility layer has been removed. Migrate clients to the [V3 API](/docs/v3/api); the V2 `/ws` endpoint and V2 HTTP endpoints are no longer available. ## Limitations {#limitations} In v2, locks and muted users were managed using a simple map that tracked who set the lock and what was locked. In v3, locks are now implemented as setting options and no longer store the `session_id` of the user who applied the lock. As a result, if a client refreshes the page or reconnects, the lock information is lost, and the user who set the lock is displayed as `Somebody`. -Additionally, when using the legacy API with a v2 client, API calls occur in a different order than expected. The client first retrieves the session list before registering the user, meaning the current `session_id` is not known when the session list is fetched. That means, the current user appears as `Somebody` in the session list. - Currently, the v3 has no native support for pipeline generation, meaning that the user has to manually specify the pipeline for video and audio if he wants to customize it. The v2 had a simple built-in support for setting the video bitrate, fps, audio bitrate, and hardware encoding. Since the introduction of multiple video pipelines in v3, this feature has been removed. diff --git a/webpage/docs/migration-from-v2/help.json b/webpage/docs/migration-from-v2/help.json index 3e2b728a..8461727a 100644 --- a/webpage/docs/migration-from-v2/help.json +++ b/webpage/docs/migration-from-v2/help.json @@ -1,12 +1,4 @@ [ - { - "key": [ - "legacy" - ], - "type": "boolean", - "defaultValue": "true", - "description": "enable legacy mode" - }, { "key": [ "logs" diff --git a/webpage/docs/migration-from-v2/help.txt b/webpage/docs/migration-from-v2/help.txt index e530e856..10c8385c 100644 --- a/webpage/docs/migration-from-v2/help.txt +++ b/webpage/docs/migration-from-v2/help.txt @@ -1,6 +1,4 @@ - --legacy enable legacy mode (default true) - --logs save logs to file --cert string path to the SSL cert used to secure the neko server