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.
This commit is contained in:
Miroslav Šedivý
2026-10-16 20:23:56 +02:00
parent ff6dd395c9
commit 3191dbb058
29 changed files with 97 additions and 2679 deletions
+3 -9
View File
@@ -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())
+23 -30
View File
@@ -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) {
-1
View File
@@ -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)
+20 -41
View File
@@ -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")
}
}
+4 -6
View File
@@ -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")
}
}
+4 -6
View File
@@ -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")
}
}
-22
View File
@@ -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)
}
}
+10 -12
View File
@@ -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")
}
}
+7 -9
View File
@@ -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")
}
}
+11 -13
View File
@@ -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")
}
}
@@ -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"
)
-463
View File
@@ -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
}
@@ -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"`
}
-232
View File
@@ -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)
}
@@ -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)
}
}
@@ -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"`
}
-386
View File
@@ -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)
}
}
-778
View File
@@ -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)
}
}
@@ -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)
}
}
-26
View File
@@ -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)
}
}
}
+5 -22
View File
@@ -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)
}
-140
View File
@@ -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
}
-18
View File
@@ -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
-1
View File
@@ -69,7 +69,6 @@ type Session interface {
Profile() MemberProfile
State() SessionState
IsHost() bool
LegacyIsHost() bool
SetAsHost()
SetAsHostBy(session Session)
ClearHost()