diff --git a/.gitignore b/.gitignore index ddf01a01..87d7eabd 100644 --- a/.gitignore +++ b/.gitignore @@ -34,19 +34,7 @@ bin # Environment files *.env -# # Neko files -# - bin/ .idea .env.development - -runtime/fonts/* -!runtime/fonts/.gitkeep - -runtime/icon-theme/* -!runtime/icon-theme/.gitkeep - -plugins/* -!plugins/.gitkeep diff --git a/build b/build deleted file mode 100755 index ee092a8c..00000000 --- a/build +++ /dev/null @@ -1,87 +0,0 @@ -#!/bin/bash - -# -# aborting if any command returns a non-zero value -set -e - -# -# do not build plugins when passing "core" as first argument -if [ "$1" = "core" ]; -then - skip_plugins="true" -fi - -# -# set git build variables if git exists -if git status > /dev/null 2>&1 && [ -z $GIT_COMMIT ] && [ -z $GIT_BRANCH ] && [ -z $GIT_TAG ]; -then - GIT_COMMIT=`git rev-parse --short HEAD` - GIT_BRANCH=`git rev-parse --symbolic-full-name --abbrev-ref HEAD` - GIT_TAG=`git tag --points-at $GIT_COMMIT | head -n 1` -fi - -# -# load server dependencies -go get -v -t -d . - -# -# build server -go build \ - -o bin/neko \ - -ldflags " - -s -w - -X 'github.com/demodesk/neko.buildDate=`date -u +'%Y-%m-%dT%H:%M:%SZ'`' - -X 'github.com/demodesk/neko.gitCommit=${GIT_COMMIT}' - -X 'github.com/demodesk/neko.gitBranch=${GIT_BRANCH}' - -X 'github.com/demodesk/neko.gitTag=${GIT_TAG}' - " \ - cmd/neko/main.go; - -# -# ensure plugins folder exists -mkdir -p bin/plugins - -# -# if plugins are ignored -if [ "$skip_plugins" = "true" ]; -then - echo "Not building plugins..." - exit 0 -fi - -# -# if plugins directory does not exist -if [ ! -d "./plugins" ]; -then - echo "No plugins directory found, skipping..." - exit 0 -fi - -# -# remove old plugins -rm -f bin/plugins/* - -# -# build plugins -for plugPath in ./plugins/*; do - if [ ! -d $plugPath ]; - then - continue - fi - - pushd $plugPath - - echo "Building plugin: $plugPath" - - if [ ! -f "go.plug.mod" ]; - then - echo "go.plug.mod not found, skipping..." - popd - continue - fi - - # build plugin - go build -modfile=go.plug.mod -buildmode=plugin -buildvcs=false -o "../../bin/plugins/${plugPath##*/}.so" - - popd -done diff --git a/cmd/neko/main.go b/cmd/neko/main.go deleted file mode 100644 index dec9df74..00000000 --- a/cmd/neko/main.go +++ /dev/null @@ -1,18 +0,0 @@ -package main - -import ( - "fmt" - - "github.com/rs/zerolog/log" - - "github.com/demodesk/neko" - "github.com/demodesk/neko/cmd" - "github.com/demodesk/neko/pkg/utils" -) - -func main() { - fmt.Print(utils.Colorf(neko.Header, "server", neko.Version)) - if err := cmd.Execute(); err != nil { - log.Panic().Err(err).Msg("failed to execute command") - } -} diff --git a/cmd/root.go b/cmd/root.go deleted file mode 100644 index fb5b5d0e..00000000 --- a/cmd/root.go +++ /dev/null @@ -1,165 +0,0 @@ -package cmd - -import ( - "fmt" - "io" - "os" - "path/filepath" - "runtime" - "strings" - "time" - - "github.com/rs/zerolog" - "github.com/rs/zerolog/diode" - "github.com/rs/zerolog/log" - "github.com/spf13/cobra" - "github.com/spf13/viper" - - "github.com/demodesk/neko" - "github.com/demodesk/neko/internal/config" -) - -func Execute() error { - // properly log unhandled panics - defer func() { - panicVal := recover() - if panicVal != nil { - log.Panic().Msgf("%v", panicVal) - } - }() - - return root.Execute() -} - -var root = &cobra.Command{ - Use: "neko", - Short: "neko streaming server", - Long: `neko streaming server`, - Version: neko.Version.String(), -} - -func init() { - rootConfig := config.Root{} - - cobra.OnInitialize(func() { - ////// - // configs - ////// - - config := viper.GetString("config") // Use config file from the flag. - if config == "" { - config = os.Getenv("NEKO_CONFIG") // Use config file from the environment variable. - } - - if config != "" { - viper.SetConfigFile(config) - } else { - if runtime.GOOS == "linux" { - viper.AddConfigPath("/etc/neko/") - } - - viper.AddConfigPath(".") - viper.SetConfigName("neko") - } - - viper.SetEnvPrefix("NEKO") - viper.SetEnvKeyReplacer(strings.NewReplacer(".", "_")) - viper.AutomaticEnv() // read in environment variables that match - - // read config values - err := viper.ReadInConfig() - if err != nil { - _, notFound := err.(viper.ConfigFileNotFoundError) - if !notFound { - log.Fatal().Err(err).Msg("unable to read config file") - } - } - - // get full config file path - config = viper.ConfigFileUsed() - - // set root config values - rootConfig.Set() - - ////// - // logs - ////// - var logWriter io.Writer - - // log to a directory instead of stderr - if rootConfig.LogDir != "" { - if _, err := os.Stat(rootConfig.LogDir); os.IsNotExist(err) { - _ = os.Mkdir(rootConfig.LogDir, os.ModePerm) - } - - latest := filepath.Join(rootConfig.LogDir, "neko-latest.log") - if _, err := os.Stat(latest); err == nil { - err = os.Rename(latest, filepath.Join(rootConfig.LogDir, "neko."+time.Now().Format("2006-01-02T15-04-05Z07-00")+".log")) - if err != nil { - log.Fatal().Err(err).Msg("failed to rotate log file") - } - } - - logf, err := os.OpenFile(latest, os.O_RDWR|os.O_CREATE, 0666) - if err != nil { - log.Fatal().Err(err).Msg("failed to open log file") - } - - logWriter = diode.NewWriter(logf, 1000, 10*time.Millisecond, func(missed int) { - fmt.Printf("logger dropped %d messages", missed) - }) - } else { - logWriter = os.Stderr - } - - // log console output instead of json - if !rootConfig.LogJson { - logWriter = zerolog.ConsoleWriter{ - Out: logWriter, - NoColor: rootConfig.LogNocolor, - } - } - - // save new logger output - log.Logger = log.Output(logWriter) - - // set custom log level - if rootConfig.LogLevel != zerolog.NoLevel { - zerolog.SetGlobalLevel(rootConfig.LogLevel) - } - - // set custom log tiem format - if rootConfig.LogTime != "" { - zerolog.TimeFieldFormat = rootConfig.LogTime - } - - timeFormat := rootConfig.LogTime - if rootConfig.LogTime == zerolog.TimeFormatUnix { - timeFormat = "UNIX" - } - - logger := log.With(). - Str("config", config). - Str("log-level", zerolog.GlobalLevel().String()). - Bool("log-json", rootConfig.LogJson). - Str("log-time", timeFormat). - Str("log-dir", rootConfig.LogDir). - Logger() - - if config == "" { - logger.Warn().Msg("preflight complete without config file") - } else { - if _, err := os.Stat(config); os.IsNotExist(err) { - logger.Err(err).Msg("preflight complete with nonexistent config file") - } else { - logger.Info().Msg("preflight complete with config file") - } - } - }) - - if err := rootConfig.Init(root); err != nil { - log.Panic().Err(err).Msg("unable to run root command") - } - - root.SetVersionTemplate(neko.Version.Details()) -} diff --git a/cmd/serve.go b/cmd/serve.go deleted file mode 100644 index d709a523..00000000 --- a/cmd/serve.go +++ /dev/null @@ -1,212 +0,0 @@ -package cmd - -import ( - "os" - "os/signal" - - "github.com/rs/zerolog" - "github.com/rs/zerolog/log" - "github.com/spf13/cobra" - - "github.com/demodesk/neko/internal/api" - "github.com/demodesk/neko/internal/capture" - "github.com/demodesk/neko/internal/config" - "github.com/demodesk/neko/internal/desktop" - "github.com/demodesk/neko/internal/http" - "github.com/demodesk/neko/internal/member" - "github.com/demodesk/neko/internal/plugins" - "github.com/demodesk/neko/internal/session" - "github.com/demodesk/neko/internal/webrtc" - "github.com/demodesk/neko/internal/websocket" -) - -func init() { - service := serve{} - - command := &cobra.Command{ - Use: "serve", - Short: "serve neko streaming server", - Long: `serve neko streaming server`, - PreRun: service.PreRun, - Run: service.Run, - } - - if err := service.Init(command); err != nil { - log.Panic().Err(err).Msg("unable to initialize configuration") - } - - root.AddCommand(command) -} - -type serve struct { - logger zerolog.Logger - - configs struct { - Desktop config.Desktop - Capture config.Capture - WebRTC config.WebRTC - Member config.Member - Session config.Session - Plugins config.Plugins - Server config.Server - } - - managers struct { - desktop *desktop.DesktopManagerCtx - capture *capture.CaptureManagerCtx - webRTC *webrtc.WebRTCManagerCtx - member *member.MemberManagerCtx - session *session.SessionManagerCtx - webSocket *websocket.WebSocketManagerCtx - plugins *plugins.ManagerCtx - api *api.ApiManagerCtx - http *http.HttpManagerCtx - } -} - -func (c *serve) Init(cmd *cobra.Command) error { - if err := c.configs.Desktop.Init(cmd); err != nil { - return err - } - if err := c.configs.Capture.Init(cmd); err != nil { - return err - } - if err := c.configs.WebRTC.Init(cmd); err != nil { - return err - } - if err := c.configs.Member.Init(cmd); err != nil { - return err - } - if err := c.configs.Session.Init(cmd); err != nil { - return err - } - if err := c.configs.Plugins.Init(cmd); err != nil { - return err - } - if err := c.configs.Server.Init(cmd); err != nil { - return err - } - - return nil -} - -func (c *serve) PreRun(cmd *cobra.Command, args []string) { - c.logger = log.With().Str("service", "neko").Logger() - - c.configs.Desktop.Set() - c.configs.Capture.Set() - c.configs.WebRTC.Set() - c.configs.Member.Set() - c.configs.Session.Set() - c.configs.Plugins.Set() - c.configs.Server.Set() -} - -func (c *serve) Start(cmd *cobra.Command) { - c.managers.session = session.New( - &c.configs.Session, - ) - - c.managers.member = member.New( - c.managers.session, - &c.configs.Member, - ) - - if err := c.managers.member.Connect(); err != nil { - c.logger.Panic().Err(err).Msg("unable to connect to member manager") - } - - c.managers.desktop = desktop.New( - &c.configs.Desktop, - ) - c.managers.desktop.Start() - - c.managers.capture = capture.New( - c.managers.desktop, - &c.configs.Capture, - ) - c.managers.capture.Start() - - c.managers.webRTC = webrtc.New( - c.managers.desktop, - c.managers.capture, - &c.configs.WebRTC, - ) - c.managers.webRTC.Start() - - c.managers.webSocket = websocket.New( - c.managers.session, - c.managers.desktop, - c.managers.capture, - c.managers.webRTC, - ) - c.managers.webSocket.Start() - - c.managers.api = api.New( - c.managers.session, - c.managers.member, - c.managers.desktop, - c.managers.capture, - ) - - c.managers.plugins = plugins.New( - &c.configs.Plugins, - ) - - // init and set configuration now - // this means it won't be in --help - c.managers.plugins.InitConfigs(cmd) - c.managers.plugins.SetConfigs() - - c.managers.plugins.Start( - c.managers.session, - c.managers.webSocket, - c.managers.api, - ) - - c.managers.http = http.New( - c.managers.webSocket, - c.managers.api, - &c.configs.Server, - ) - c.managers.http.Start() -} - -func (c *serve) Shutdown() { - var err error - - err = c.managers.http.Shutdown() - c.logger.Err(err).Msg("http manager shutdown") - - err = c.managers.plugins.Shutdown() - c.logger.Err(err).Msg("plugins manager shutdown") - - err = c.managers.webSocket.Shutdown() - c.logger.Err(err).Msg("websocket manager shutdown") - - err = c.managers.webRTC.Shutdown() - c.logger.Err(err).Msg("webrtc manager shutdown") - - err = c.managers.capture.Shutdown() - c.logger.Err(err).Msg("capture manager shutdown") - - err = c.managers.desktop.Shutdown() - c.logger.Err(err).Msg("desktop manager shutdown") - - err = c.managers.member.Disconnect() - c.logger.Err(err).Msg("member manager disconnect") -} - -func (c *serve) Run(cmd *cobra.Command, args []string) { - c.logger.Info().Msg("starting neko server") - c.Start(cmd) - c.logger.Info().Msg("neko ready") - - quit := make(chan os.Signal, 1) - signal.Notify(quit, os.Interrupt) - sig := <-quit - - c.logger.Warn().Msgf("received %s, attempting graceful shutdown", sig) - c.Shutdown() - c.logger.Info().Msg("shutdown complete") -} diff --git a/go.mod b/go.mod deleted file mode 100644 index eb971ff6..00000000 --- a/go.mod +++ /dev/null @@ -1,68 +0,0 @@ -module github.com/demodesk/neko - -go 1.21 - -require ( - github.com/PaesslerAG/gval v1.2.2 - github.com/go-chi/chi v1.5.5 - github.com/go-chi/cors v1.2.1 - github.com/gorilla/websocket v1.5.1 - github.com/kataras/go-events v0.0.3 - github.com/pion/ice/v2 v2.3.12 - github.com/pion/interceptor v0.1.25 - github.com/pion/logging v0.2.2 - github.com/pion/rtcp v1.2.13 - github.com/pion/webrtc/v3 v3.2.24 - github.com/prometheus/client_golang v1.18.0 - github.com/rs/zerolog v1.31.0 - github.com/spf13/cobra v1.8.0 - github.com/spf13/viper v1.18.2 -) - -require ( - github.com/beorn7/perks v1.0.1 // indirect - github.com/cespare/xxhash/v2 v2.2.0 // indirect - github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect - github.com/fsnotify/fsnotify v1.7.0 // indirect - github.com/google/uuid v1.6.0 // indirect - github.com/hashicorp/hcl v1.0.0 // indirect - github.com/inconshreveable/mousetrap v1.1.0 // indirect - github.com/magiconair/properties v1.8.7 // indirect - github.com/mattn/go-colorable v0.1.13 // indirect - github.com/mattn/go-isatty v0.0.20 // indirect - github.com/mitchellh/mapstructure v1.5.0 // indirect - github.com/pelletier/go-toml/v2 v2.1.1 // indirect - github.com/pion/datachannel v1.5.5 // indirect - github.com/pion/dtls/v2 v2.2.9 // indirect - github.com/pion/mdns v0.0.9 // indirect - github.com/pion/randutil v0.1.0 // indirect - github.com/pion/rtp v1.8.3 // indirect - github.com/pion/sctp v1.8.9 // indirect - github.com/pion/sdp/v3 v3.0.6 // indirect - github.com/pion/srtp/v2 v2.0.18 // indirect - github.com/pion/stun v0.6.1 // indirect - github.com/pion/transport/v2 v2.2.4 // indirect - github.com/pion/turn/v2 v2.1.4 // indirect - github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect - github.com/prometheus/client_model v0.5.0 // indirect - github.com/prometheus/common v0.46.0 // indirect - github.com/prometheus/procfs v0.12.0 // indirect - github.com/sagikazarmark/locafero v0.4.0 // indirect - github.com/sagikazarmark/slog-shim v0.1.0 // indirect - github.com/shopspring/decimal v1.3.1 // indirect - github.com/sourcegraph/conc v0.3.0 // indirect - github.com/spf13/afero v1.11.0 // indirect - github.com/spf13/cast v1.6.0 // indirect - github.com/spf13/pflag v1.0.5 // indirect - github.com/stretchr/testify v1.8.4 // indirect - github.com/subosito/gotenv v1.6.0 // indirect - go.uber.org/multierr v1.11.0 // indirect - golang.org/x/crypto v0.18.0 // indirect - golang.org/x/exp v0.0.0-20240119083558-1b970713d09a // indirect - golang.org/x/net v0.20.0 // indirect - golang.org/x/sys v0.16.0 // indirect - golang.org/x/text v0.14.0 // indirect - google.golang.org/protobuf v1.32.0 // indirect - gopkg.in/ini.v1 v1.67.0 // indirect - gopkg.in/yaml.v3 v3.0.1 // indirect -) diff --git a/go.sum b/go.sum deleted file mode 100644 index 84008d03..00000000 --- a/go.sum +++ /dev/null @@ -1,305 +0,0 @@ -github.com/PaesslerAG/gval v1.2.2 h1:Y7iBzhgE09IGTt5QgGQ2IdaYYYOU134YGHBThD+wm9E= -github.com/PaesslerAG/gval v1.2.2/go.mod h1:XRFLwvmkTEdYziLdaCeCa5ImcGVrfQbeNUbVR+C6xac= -github.com/PaesslerAG/jsonpath v0.1.0 h1:gADYeifvlqK3R3i2cR5B4DGgxLXIPb3TRTH1mGi0jPI= -github.com/PaesslerAG/jsonpath v0.1.0/go.mod h1:4BzmtoM/PI8fPO4aQGIusjGxGir2BzcV0grWtFzq1Y8= -github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM= -github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw= -github.com/cespare/xxhash/v2 v2.2.0 h1:DC2CZ1Ep5Y4k3ZQ899DldepgrayRUGE6BBZ/cd9Cj44= -github.com/cespare/xxhash/v2 v2.2.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= -github.com/coreos/go-systemd/v22 v22.5.0/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc= -github.com/cpuguy83/go-md2man/v2 v2.0.3/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= -github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= -github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= -github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM= -github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= -github.com/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHkI4W8= -github.com/frankban/quicktest v1.14.6/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7zb5vbUoiM6w0= -github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo= -github.com/fsnotify/fsnotify v1.4.9/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4IgpuI1SZQ= -github.com/fsnotify/fsnotify v1.7.0 h1:8JEhPFa5W2WU7YfeZzPNqzMP6Lwt7L2715Ggo0nosvA= -github.com/fsnotify/fsnotify v1.7.0/go.mod h1:40Bi/Hjc2AVfZrqy+aj+yEI+/bRxZnMJyTJwOpGvigM= -github.com/go-chi/chi v1.5.5 h1:vOB/HbEMt9QqBqErz07QehcOKHaWFtuj87tTDVz2qXE= -github.com/go-chi/chi v1.5.5/go.mod h1:C9JqLr3tIYjDOZpzn+BCuxY8z8vmca43EeMgyZt7irw= -github.com/go-chi/cors v1.2.1 h1:xEC8UT3Rlp2QuWNEr4Fs/c2EAGVKBwy/1vHx3bppil4= -github.com/go-chi/cors v1.2.1/go.mod h1:sSbTewc+6wYHBBCW7ytsFSn836hqM7JxpglAy2Vzc58= -github.com/go-task/slim-sprig v0.0.0-20210107165309-348f09dbbbc0/go.mod h1:fyg7847qk6SyHyPtNmDHnmrv/HOrqktSC+C9fM+CJOE= -github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA= -github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= -github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8= -github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA= -github.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrUpVNzEA03Pprs= -github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:WU3c8KckQ9AFe+yFwt9sWVRKCVIyN9cPHBJSNnbL67w= -github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0= -github.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI= -github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk= -github.com/golang/protobuf v1.5.2/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY= -github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= -github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= -github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= -github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= -github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38= -github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= -github.com/google/uuid v1.3.1/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= -github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= -github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= -github.com/gorilla/websocket v1.5.1 h1:gmztn0JnHVt9JZquRuzLw3g4wouNVzKL15iLr/zn/QY= -github.com/gorilla/websocket v1.5.1/go.mod h1:x3kM2JMyaluk02fnUJpQuwD2dCS5NDG2ZHL0uE0tcaY= -github.com/hashicorp/hcl v1.0.0 h1:0Anlzjpi4vEasTeNFn2mLJgTSwt0+6sfsiTG8qcWGx4= -github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ= -github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU= -github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8= -github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw= -github.com/kataras/go-events v0.0.3 h1:o5YK53uURXtrlg7qE/vovxd/yKOJcLuFtPQbf1rYMC4= -github.com/kataras/go-events v0.0.3/go.mod h1:bFBgtzwwzrag7kQmGuU1ZaVxhK2qseYPQomXoVEMsj4= -github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= -github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= -github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= -github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= -github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= -github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= -github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= -github.com/magiconair/properties v1.8.7 h1:IeQXZAiQcpL9mgcAe1Nu6cX9LLw6ExEHKjN0VQdvPDY= -github.com/magiconair/properties v1.8.7/go.mod h1:Dhd985XPs7jluiymwWYZ0G4Z61jb3vdS329zhj2hYo0= -github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA= -github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg= -github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM= -github.com/mattn/go-isatty v0.0.19/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= -github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= -github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= -github.com/mitchellh/mapstructure v1.5.0 h1:jeMsZIYE/09sWLaz43PL7Gy6RuMjD2eJVyuac5Z2hdY= -github.com/mitchellh/mapstructure v1.5.0/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo= -github.com/nxadm/tail v1.4.4/go.mod h1:kenIhsEOeOJmVchQTgglprH7qJGnHDVpk1VPCcaMI8A= -github.com/nxadm/tail v1.4.8/go.mod h1:+ncqLTQzXmGhMZNUePPaPqPvBxHAIsmXswZKocGu+AU= -github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= -github.com/onsi/ginkgo v1.12.1/go.mod h1:zj2OWP4+oCPe1qIXoGWkgMRwljMUYCdkwsT2108oapk= -github.com/onsi/ginkgo v1.16.4/go.mod h1:dX+/inL/fNMqNlz0e9LfyB9TswhZpCVdJM/Z6Vvnwo0= -github.com/onsi/ginkgo v1.16.5/go.mod h1:+E8gABHa3K6zRBolWtd+ROzc/U5bkGt0FwiG042wbpU= -github.com/onsi/gomega v1.7.1/go.mod h1:XdKZgCCFLUoM/7CFJVPcG8C1xQ1AJ0vpAezJrB7JYyY= -github.com/onsi/gomega v1.10.1/go.mod h1:iN09h71vgCQne3DLsj+A5owkum+a2tYe+TOCB1ybHNo= -github.com/onsi/gomega v1.17.0/go.mod h1:HnhC7FXeEQY45zxNK3PPoIUhzk/80Xly9PcubAlGdZY= -github.com/pelletier/go-toml/v2 v2.1.1 h1:LWAJwfNvjQZCFIDKWYQaM62NcYeYViCmWIwmOStowAI= -github.com/pelletier/go-toml/v2 v2.1.1/go.mod h1:tJU2Z3ZkXwnxa4DPO899bsyIoywizdUvyaeZurnPPDc= -github.com/pion/datachannel v1.5.5 h1:10ef4kwdjije+M9d7Xm9im2Y3O6A6ccQb0zcqZcJew8= -github.com/pion/datachannel v1.5.5/go.mod h1:iMz+lECmfdCMqFRhXhcA/219B0SQlbpoR2V118yimL0= -github.com/pion/dtls/v2 v2.2.7/go.mod h1:8WiMkebSHFD0T+dIU+UeBaoV7kDhOW5oDCzZ7WZ/F9s= -github.com/pion/dtls/v2 v2.2.9 h1:K+D/aVf9/REahQvqk6G5JavdrD8W1PWDKC11UlwN7ts= -github.com/pion/dtls/v2 v2.2.9/go.mod h1:8WiMkebSHFD0T+dIU+UeBaoV7kDhOW5oDCzZ7WZ/F9s= -github.com/pion/ice/v2 v2.3.11/go.mod h1:hPcLC3kxMa+JGRzMHqQzjoSj3xtE9F+eoncmXLlCL4E= -github.com/pion/ice/v2 v2.3.12 h1:NWKW2b3+oSZS3klbQMIEWQ0i52Kuo0KBg505a5kQv4s= -github.com/pion/ice/v2 v2.3.12/go.mod h1:hPcLC3kxMa+JGRzMHqQzjoSj3xtE9F+eoncmXLlCL4E= -github.com/pion/interceptor v0.1.25 h1:pwY9r7P6ToQ3+IF0bajN0xmk/fNw/suTgaTdlwTDmhc= -github.com/pion/interceptor v0.1.25/go.mod h1:wkbPYAak5zKsfpVDYMtEfWEy8D4zL+rpxCxPImLOg3Y= -github.com/pion/logging v0.2.2 h1:M9+AIj/+pxNsDfAT64+MAVgJO0rsyLnoJKCqf//DoeY= -github.com/pion/logging v0.2.2/go.mod h1:k0/tDVsRCX2Mb2ZEmTqNa7CWsQPc+YYCB7Q+5pahoms= -github.com/pion/mdns v0.0.8/go.mod h1:hYE72WX8WDveIhg7fmXgMKivD3Puklk0Ymzog0lSyaI= -github.com/pion/mdns v0.0.9 h1:7Ue5KZsqq8EuqStnpPWV33vYYEH0+skdDN5L7EiEsI4= -github.com/pion/mdns v0.0.9/go.mod h1:2JA5exfxwzXiCihmxpTKgFUpiQws2MnipoPK09vecIc= -github.com/pion/randutil v0.1.0 h1:CFG1UdESneORglEsnimhUjf33Rwjubwj6xfiOXBa3mA= -github.com/pion/randutil v0.1.0/go.mod h1:XcJrSMMbbMRhASFVOlj/5hQial/Y8oH/HVo7TBZq+j8= -github.com/pion/rtcp v1.2.10/go.mod h1:ztfEwXZNLGyF1oQDttz/ZKIBaeeg/oWbRYqzBM9TL1I= -github.com/pion/rtcp v1.2.12/go.mod h1:sn6qjxvnwyAkkPzPULIbVqSKI5Dv54Rv7VG0kNxh9L4= -github.com/pion/rtcp v1.2.13 h1:+EQijuisKwm/8VBs8nWllr0bIndR7Lf7cZG200mpbNo= -github.com/pion/rtcp v1.2.13/go.mod h1:sn6qjxvnwyAkkPzPULIbVqSKI5Dv54Rv7VG0kNxh9L4= -github.com/pion/rtp v1.8.2/go.mod h1:pBGHaFt/yW7bf1jjWAoUjpSNoDnw98KTMg+jWWvziqU= -github.com/pion/rtp v1.8.3 h1:VEHxqzSVQxCkKDSHro5/4IUUG1ea+MFdqR2R3xSpNU8= -github.com/pion/rtp v1.8.3/go.mod h1:pBGHaFt/yW7bf1jjWAoUjpSNoDnw98KTMg+jWWvziqU= -github.com/pion/sctp v1.8.5/go.mod h1:SUFFfDpViyKejTAdwD1d/HQsCu+V/40cCs2nZIvC3s0= -github.com/pion/sctp v1.8.8/go.mod h1:igF9nZBrjh5AtmKc7U30jXltsFHicFCXSmWA2GWRaWs= -github.com/pion/sctp v1.8.9 h1:TP5ZVxV5J7rz7uZmbyvnUvsn7EJ2x/5q9uhsTtXbI3g= -github.com/pion/sctp v1.8.9/go.mod h1:cMLT45jqw3+jiJCrtHVwfQLnfR0MGZ4rgOJwUOIqLkI= -github.com/pion/sdp/v3 v3.0.6 h1:WuDLhtuFUUVpTfus9ILC4HRyHsW6TdugjEX/QY9OiUw= -github.com/pion/sdp/v3 v3.0.6/go.mod h1:iiFWFpQO8Fy3S5ldclBkpXqmWy02ns78NOKoLLL0YQw= -github.com/pion/srtp/v2 v2.0.18 h1:vKpAXfawO9RtTRKZJbG4y0v1b11NZxQnxRl85kGuUlo= -github.com/pion/srtp/v2 v2.0.18/go.mod h1:0KJQjA99A6/a0DOVTu1PhDSw0CXF2jTkqOoMg3ODqdA= -github.com/pion/stun v0.6.1 h1:8lp6YejULeHBF8NmV8e2787BogQhduZugh5PdhDyyN4= -github.com/pion/stun v0.6.1/go.mod h1:/hO7APkX4hZKu/D0f2lHzNyvdkTGtIy3NDmLR7kSz/8= -github.com/pion/transport v0.14.1 h1:XSM6olwW+o8J4SCmOBb/BpwZypkHeyM0PGFCxNQBr40= -github.com/pion/transport v0.14.1/go.mod h1:4tGmbk00NeYA3rUa9+n+dzCCoKkcy3YlYb99Jn2fNnI= -github.com/pion/transport/v2 v2.2.1/go.mod h1:cXXWavvCnFF6McHTft3DWS9iic2Mftcz1Aq29pGcU5g= -github.com/pion/transport/v2 v2.2.2/go.mod h1:OJg3ojoBJopjEeECq2yJdXH9YVrUJ1uQ++NjXLOUorc= -github.com/pion/transport/v2 v2.2.3/go.mod h1:q2U/tf9FEfnSBGSW6w5Qp5PFWRLRj3NjLhCCgpRK4p0= -github.com/pion/transport/v2 v2.2.4 h1:41JJK6DZQYSeVLxILA2+F4ZkKb4Xd/tFJZRFZQ9QAlo= -github.com/pion/transport/v2 v2.2.4/go.mod h1:q2U/tf9FEfnSBGSW6w5Qp5PFWRLRj3NjLhCCgpRK4p0= -github.com/pion/transport/v3 v3.0.1 h1:gDTlPJwROfSfz6QfSi0ZmeCSkFcnWWiiR9ES0ouANiM= -github.com/pion/transport/v3 v3.0.1/go.mod h1:UY7kiITrlMv7/IKgd5eTUcaahZx5oUN3l9SzK5f5xE0= -github.com/pion/turn/v2 v2.1.3/go.mod h1:huEpByKKHix2/b9kmTAM3YoX6MKP+/D//0ClgUYR2fY= -github.com/pion/turn/v2 v2.1.4 h1:2xn8rduI5W6sCZQkEnIUDAkrBQNl2eYIBCHMZ3QMmP8= -github.com/pion/turn/v2 v2.1.4/go.mod h1:huEpByKKHix2/b9kmTAM3YoX6MKP+/D//0ClgUYR2fY= -github.com/pion/webrtc/v3 v3.2.24 h1:MiFL5DMo2bDaaIFWr0DDpwiV/L4EGbLZb+xoRvfEo1Y= -github.com/pion/webrtc/v3 v3.2.24/go.mod h1:1CaT2fcZzZ6VZA+O1i9yK2DU4EOcXVvSbWG9pr5jefs= -github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= -github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= -github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U= -github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= -github.com/prometheus/client_golang v1.18.0 h1:HzFfmkOzH5Q8L8G+kSJKUx5dtG87sewO+FoDDqP5Tbk= -github.com/prometheus/client_golang v1.18.0/go.mod h1:T+GXkCk5wSJyOqMIzVgvvjFDlkOQntgjkJWKrN5txjA= -github.com/prometheus/client_model v0.5.0 h1:VQw1hfvPvk3Uv6Qf29VrPF32JB6rtbgI6cYPYQjL0Qw= -github.com/prometheus/client_model v0.5.0/go.mod h1:dTiFglRmd66nLR9Pv9f0mZi7B7fk5Pm3gvsjB5tr+kI= -github.com/prometheus/common v0.46.0 h1:doXzt5ybi1HBKpsZOL0sSkaNHJJqkyfEWZGGqqScV0Y= -github.com/prometheus/common v0.46.0/go.mod h1:Tp0qkxpb9Jsg54QMe+EAmqXkSV7Evdy1BTn+g2pa/hQ= -github.com/prometheus/procfs v0.12.0 h1:jluTpSng7V9hY0O2R9DzzJHYb2xULk9VTR1V1R/k6Bo= -github.com/prometheus/procfs v0.12.0/go.mod h1:pcuDEFsWDnvcgNzo4EEweacyhjeA9Zk3cnaOZAZEfOo= -github.com/rogpeppe/go-internal v1.10.0 h1:TMyTOH3F/DB16zRVcYyreMH6GnZZrwQVAoYjRBZyWFQ= -github.com/rogpeppe/go-internal v1.10.0/go.mod h1:UQnix2H7Ngw/k4C5ijL5+65zddjncjaFoBhdsK/akog= -github.com/rs/xid v1.5.0/go.mod h1:trrq9SKmegXys3aeAKXMUTdJsYXVwGY3RLcfgqegfbg= -github.com/rs/zerolog v1.31.0 h1:FcTR3NnLWW+NnTwwhFWiJSZr4ECLpqCm6QsEnyvbV4A= -github.com/rs/zerolog v1.31.0/go.mod h1:/7mN4D5sKwJLZQ2b/znpjC3/GQWY/xaDXUM0kKWRHss= -github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= -github.com/sagikazarmark/locafero v0.4.0 h1:HApY1R9zGo4DBgr7dqsTH/JJxLTTsOt7u6keLGt6kNQ= -github.com/sagikazarmark/locafero v0.4.0/go.mod h1:Pe1W6UlPYUk/+wc/6KFhbORCfqzgYEpgQ3O5fPuL3H4= -github.com/sagikazarmark/slog-shim v0.1.0 h1:diDBnUNK9N/354PgrxMywXnAwEr1QZcOr6gto+ugjYE= -github.com/sagikazarmark/slog-shim v0.1.0/go.mod h1:SrcSrq8aKtyuqEI1uvTDTK1arOWRIczQRv+GVI1AkeQ= -github.com/sclevine/agouti v3.0.0+incompatible/go.mod h1:b4WX9W9L1sfQKXeJf1mUTLZKJ48R1S7H23Ji7oFO5Bw= -github.com/shopspring/decimal v1.3.1 h1:2Usl1nmF/WZucqkFZhnfFYxxxu8LG21F6nPQBE5gKV8= -github.com/shopspring/decimal v1.3.1/go.mod h1:DKyhrW/HYNuLGql+MJL6WCR6knT2jwCFRcu2hWCYk4o= -github.com/sourcegraph/conc v0.3.0 h1:OQTbbt6P72L20UqAkXXuLOj79LfEanQ+YQFNpLA9ySo= -github.com/sourcegraph/conc v0.3.0/go.mod h1:Sdozi7LEKbFPqYX2/J+iBAM6HpqSLTASQIKqDmF7Mt0= -github.com/spf13/afero v1.11.0 h1:WJQKhtpdm3v2IzqG8VMqrr6Rf3UYpEF239Jy9wNepM8= -github.com/spf13/afero v1.11.0/go.mod h1:GH9Y3pIexgf1MTIWtNGyogA5MwRIDXGUr+hbWNoBjkY= -github.com/spf13/cast v1.6.0 h1:GEiTHELF+vaR5dhz3VqZfFSzZjYbgeKDpBxQVS4GYJ0= -github.com/spf13/cast v1.6.0/go.mod h1:ancEpBxwJDODSW/UG4rDrAqiKolqNNh2DX3mk86cAdo= -github.com/spf13/cobra v1.8.0 h1:7aJaZx1B85qltLMc546zn58BxxfZdR/W22ej9CFoEf0= -github.com/spf13/cobra v1.8.0/go.mod h1:WXLWApfZ71AjXPya3WOlMsY9yMs7YeiHhFVlvLyhcho= -github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA= -github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= -github.com/spf13/viper v1.18.2 h1:LUXCnvUvSM6FXAsj6nnfc8Q2tp1dIgUfY9Kc8GsSOiQ= -github.com/spf13/viper v1.18.2/go.mod h1:EKmWIqdnk5lOcmR72yw6hS+8OPYcwD0jteitLMVB+yk= -github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= -github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= -github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= -github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA= -github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= -github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= -github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= -github.com/stretchr/testify v1.8.3/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= -github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk= -github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= -github.com/subosito/gotenv v1.6.0 h1:9NlTDc1FTs4qu0DDq7AEtTPNw6SVm7uBMsUCUjABIf8= -github.com/subosito/gotenv v1.6.0/go.mod h1:Dk4QP5c2W3ibzajGcXpNraDfq2IrhjMIvMSWPKKo0FU= -github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= -github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= -go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0= -go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y= -golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= -golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= -golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= -golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= -golang.org/x/crypto v0.8.0/go.mod h1:mRqEX+O9/h5TFCrQhkgjo2yKi0yYA+9ecGkdQoHrywE= -golang.org/x/crypto v0.10.0/go.mod h1:o4eNf7Ede1fv+hwOwZsTHl9EsPFO6q6ZvYR8vYfY45I= -golang.org/x/crypto v0.11.0/go.mod h1:xgJhtzW8F9jGdVFWZESrid1U1bjeNy4zgy5cRr/CIio= -golang.org/x/crypto v0.12.0/go.mod h1:NF0Gs7EO5K4qLn+Ylc+fih8BSTeIjAP05siRnAh98yw= -golang.org/x/crypto v0.13.0/go.mod h1:y6Z2r+Rw4iayiXXAIxJIDAJ1zMW4yaTpebo8fPOliYc= -golang.org/x/crypto v0.18.0 h1:PGVlW0xEltQnzFZ55hkuX5+KLyrMYhHld1YHO4AKcdc= -golang.org/x/crypto v0.18.0/go.mod h1:R0j02AL6hcrfOiy9T4ZYp/rcWeMxM3L6QYxlOuEG1mg= -golang.org/x/exp v0.0.0-20240119083558-1b970713d09a h1:Q8/wZp0KX97QFTc2ywcOE0YRjZPVIx+MXInMzdvQqcA= -golang.org/x/exp v0.0.0-20240119083558-1b970713d09a/go.mod h1:idGWGoKP1toJGkd5/ig9ZLuPcZBC3ewk7SzmH0uou08= -golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= -golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= -golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= -golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= -golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= -golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= -golang.org/x/net v0.0.0-20200520004742-59133d7f0dd7/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= -golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= -golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= -golang.org/x/net v0.0.0-20210428140749-89ef3d95e781/go.mod h1:OJAsFXCWl8Ukc7SiCT/9KSuxbyM7479/AVlXFRxuMCk= -golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= -golang.org/x/net v0.1.0/go.mod h1:Cx3nUiGt4eDBEyega/BKRp+/AlGL8hYe7U9odMt2Cco= -golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= -golang.org/x/net v0.9.0/go.mod h1:d48xBJpPfHeWQsugry2m+kC02ZBRGRgulfHnEXEuWns= -golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg= -golang.org/x/net v0.11.0/go.mod h1:2L/ixqYpgIVXmeoSA/4Lu7BzTG4KIyPIryS4IsOd1oQ= -golang.org/x/net v0.13.0/go.mod h1:zEVYFnQC7m/vmpQFELhcD1EWkZlX69l4oqgmer6hfKA= -golang.org/x/net v0.14.0/go.mod h1:PpSgVXXLK0OxS0F31C1/tv6XNguvCrnXIDrFMspZIUI= -golang.org/x/net v0.15.0/go.mod h1:idbUs1IY1+zTqbi8yxTbhexhEEk5ur9LInksu6HrEpk= -golang.org/x/net v0.20.0 h1:aCL9BSgETF1k+blQaYUBx9hJ9LOGP3gAVemcZlf1Kpo= -golang.org/x/net v0.20.0/go.mod h1:z8BVo6PvndSri0LbOE3hAn0apkU+1YvI6E70E9jsnvY= -golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= -golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= -golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20190904154756-749cb33beabd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20191005200804-aed5e4c7ecf9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20191120155948-bd437916bb0e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20210112080510-489259a85091/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.2.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.7.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.9.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.10.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.11.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.16.0 h1:xWw16ngr6ZMtmxDyKyIgsE93KNKz5HKmMa3b8ALHidU= -golang.org/x/sys v0.16.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= -golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= -golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= -golang.org/x/term v0.1.0/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= -golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k= -golang.org/x/term v0.7.0/go.mod h1:P32HKFT3hSsZrRxla30E9HqToFYAQPCMs/zFMBUFqPY= -golang.org/x/term v0.8.0/go.mod h1:xPskH00ivmX89bAKVGSKKtLOWNx2+17Eiy94tnKShWo= -golang.org/x/term v0.9.0/go.mod h1:M6DEAAIenWoTxdKrOltXcmDY3rSplQUkrvaDU5FcQyo= -golang.org/x/term v0.10.0/go.mod h1:lpqdcUyK/oCiQxvxVrppt5ggO2KCZ5QblwqPnfZ6d5o= -golang.org/x/term v0.11.0/go.mod h1:zC9APTIj3jG3FdV/Ons+XE1riIZXG4aZ4GTHiPZJPIU= -golang.org/x/term v0.12.0/go.mod h1:owVbMEjm3cBLCHdkQu9b1opXd4ETQWc3BhuQGKgXgvU= -golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= -golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= -golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= -golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= -golang.org/x/text v0.4.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= -golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= -golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8= -golang.org/x/text v0.10.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE= -golang.org/x/text v0.11.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE= -golang.org/x/text v0.12.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE= -golang.org/x/text v0.13.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE= -golang.org/x/text v0.14.0 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ= -golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= -golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= -golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= -golang.org/x/tools v0.0.0-20201224043029-2b0845dc783e/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= -golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= -golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU= -golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= -golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= -golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= -golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= -google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8= -google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0= -google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM= -google.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miEFZTKqfCUM6K7xSMQL9OKL/b6hQv+e19PK+JZNE= -google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo= -google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= -google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw= -google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc= -google.golang.org/protobuf v1.32.0 h1:pPC6BG5ex8PDFnkbrGU3EixyhKcQ2aDuBS36lqK/C7I= -google.golang.org/protobuf v1.32.0/go.mod h1:c6P6GXX6sHbq/GpV6MGZEdwhWPcYBgnhAHhKbcUYpos= -gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= -gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= -gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= -gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= -gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys= -gopkg.in/ini.v1 v1.67.0 h1:Dgnx+6+nfE+IfzjUEISNeydPJh9AXNNsWbGP9KzCsOA= -gopkg.in/ini.v1 v1.67.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k= -gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw= -gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= -gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= -gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= -gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= -gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= -gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= -gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/internal/api/room/settings.go b/internal/api/room/settings.go deleted file mode 100644 index 33e41552..00000000 --- a/internal/api/room/settings.go +++ /dev/null @@ -1,24 +0,0 @@ -package room - -import ( - "net/http" - - "github.com/demodesk/neko/pkg/utils" -) - -func (h *RoomHandler) settingsGet(w http.ResponseWriter, r *http.Request) error { - settings := h.sessions.Settings() - return utils.HttpSuccess(w, settings) -} - -func (h *RoomHandler) settingsSet(w http.ResponseWriter, r *http.Request) error { - settings := h.sessions.Settings() - - if err := utils.HttpJsonRequest(w, r, &settings); err != nil { - return err - } - - h.sessions.UpdateSettings(settings) - - return utils.HttpSuccess(w) -} diff --git a/internal/capture/broadcast.go b/internal/capture/broadcast.go deleted file mode 100644 index 2ded2cd0..00000000 --- a/internal/capture/broadcast.go +++ /dev/null @@ -1,156 +0,0 @@ -package capture - -import ( - "sync" - - "github.com/prometheus/client_golang/prometheus" - "github.com/prometheus/client_golang/prometheus/promauto" - "github.com/rs/zerolog" - "github.com/rs/zerolog/log" - - "github.com/demodesk/neko/pkg/gst" - "github.com/demodesk/neko/pkg/types" -) - -type BroacastManagerCtx struct { - logger zerolog.Logger - mu sync.Mutex - - pipeline gst.Pipeline - pipelineMu sync.Mutex - pipelineFn func(url string) (string, error) - - url string - started bool - - // metrics - pipelinesCounter prometheus.Counter - pipelinesActive prometheus.Gauge -} - -func broadcastNew(pipelineFn func(url string) (string, error), defaultUrl string) *BroacastManagerCtx { - logger := log.With(). - Str("module", "capture"). - Str("submodule", "broadcast"). - Logger() - - return &BroacastManagerCtx{ - logger: logger, - pipelineFn: pipelineFn, - url: defaultUrl, - started: defaultUrl != "", - - // metrics - pipelinesCounter: promauto.NewCounter(prometheus.CounterOpts{ - Name: "pipelines_total", - Namespace: "neko", - Subsystem: "capture", - Help: "Total number of created pipelines.", - ConstLabels: map[string]string{ - "submodule": "broadcast", - "video_id": "main", - "codec_name": "-", - "codec_type": "-", - }, - }), - pipelinesActive: promauto.NewGauge(prometheus.GaugeOpts{ - Name: "pipelines_active", - Namespace: "neko", - Subsystem: "capture", - Help: "Total number of active pipelines.", - ConstLabels: map[string]string{ - "submodule": "broadcast", - "video_id": "main", - "codec_name": "-", - "codec_type": "-", - }, - }), - } -} - -func (manager *BroacastManagerCtx) shutdown() { - manager.logger.Info().Msgf("shutdown") - - manager.destroyPipeline() -} - -func (manager *BroacastManagerCtx) Start(url string) error { - manager.mu.Lock() - defer manager.mu.Unlock() - - err := manager.createPipeline() - if err != nil { - return err - } - - manager.url = url - manager.started = true - return nil -} - -func (manager *BroacastManagerCtx) Stop() { - manager.mu.Lock() - defer manager.mu.Unlock() - - manager.started = false - manager.destroyPipeline() -} - -func (manager *BroacastManagerCtx) Started() bool { - manager.mu.Lock() - defer manager.mu.Unlock() - - return manager.started -} - -func (manager *BroacastManagerCtx) Url() string { - manager.mu.Lock() - defer manager.mu.Unlock() - - return manager.url -} - -func (manager *BroacastManagerCtx) createPipeline() error { - manager.pipelineMu.Lock() - defer manager.pipelineMu.Unlock() - - if manager.pipeline != nil { - return types.ErrCapturePipelineAlreadyExists - } - - pipelineStr, err := manager.pipelineFn(manager.url) - if err != nil { - return err - } - - manager.logger.Info(). - Str("url", manager.url). - Str("src", pipelineStr). - Msgf("starting pipeline") - - manager.pipeline, err = gst.CreatePipeline(pipelineStr) - if err != nil { - return err - } - - manager.pipeline.Play() - manager.pipelinesCounter.Inc() - manager.pipelinesActive.Set(1) - - return nil -} - -func (manager *BroacastManagerCtx) destroyPipeline() { - manager.pipelineMu.Lock() - defer manager.pipelineMu.Unlock() - - if manager.pipeline == nil { - return - } - - manager.pipeline.Destroy() - manager.logger.Info().Msgf("destroying pipeline") - manager.pipeline = nil - - manager.pipelinesActive.Set(0) -} diff --git a/internal/capture/manager.go b/internal/capture/manager.go deleted file mode 100644 index 2a71485e..00000000 --- a/internal/capture/manager.go +++ /dev/null @@ -1,269 +0,0 @@ -package capture - -import ( - "errors" - "fmt" - "strings" - - "github.com/rs/zerolog" - "github.com/rs/zerolog/log" - - "github.com/demodesk/neko/internal/config" - "github.com/demodesk/neko/pkg/types" - "github.com/demodesk/neko/pkg/types/codec" -) - -type CaptureManagerCtx struct { - logger zerolog.Logger - desktop types.DesktopManager - config *config.Capture - - // sinks - broadcast *BroacastManagerCtx - screencast *ScreencastManagerCtx - audio *StreamSinkManagerCtx - video *StreamSelectorManagerCtx - - // sources - webcam *StreamSrcManagerCtx - microphone *StreamSrcManagerCtx -} - -func New(desktop types.DesktopManager, config *config.Capture) *CaptureManagerCtx { - logger := log.With().Str("module", "capture").Logger() - - videos := map[string]types.StreamSinkManager{} - for video_id, cnf := range config.VideoPipelines { - pipelineConf := cnf - - createPipeline := func() (string, error) { - if pipelineConf.GstPipeline != "" { - // replace {display} with valid display - return strings.Replace(pipelineConf.GstPipeline, "{display}", config.Display, 1), nil - } - - screen := desktop.GetScreenSize() - pipeline, err := pipelineConf.GetPipeline(screen) - if err != nil { - return "", err - } - - return fmt.Sprintf( - "ximagesrc display-name=%s show-pointer=false use-damage=false "+ - "%s ! appsink name=appsink", config.Display, pipeline, - ), nil - } - - // trigger function to catch evaluation errors at startup - pipeline, err := createPipeline() - if err != nil { - logger.Panic().Err(err). - Str("video_id", video_id). - Msg("failed to create video pipeline") - } - - logger.Info(). - Str("video_id", video_id). - Str("pipeline", pipeline). - Msg("syntax check for video stream pipeline passed") - - // append to videos - videos[video_id] = streamSinkNew(config.VideoCodec, createPipeline, video_id) - } - - return &CaptureManagerCtx{ - logger: logger, - desktop: desktop, - config: config, - - // sinks - broadcast: broadcastNew(func(url string) (string, error) { - if config.BroadcastPipeline != "" { - var pipeline = config.BroadcastPipeline - // replace {display} with valid display - pipeline = strings.Replace(pipeline, "{display}", config.Display, 1) - // replace {device} with valid device - pipeline = strings.Replace(pipeline, "{device}", config.AudioDevice, 1) - // replace {url} with valid URL - return strings.Replace(pipeline, "{url}", url, 1), nil - } - - return fmt.Sprintf( - "flvmux name=mux ! rtmpsink location='%s live=1' "+ - "pulsesrc device=%s "+ - "! audio/x-raw,channels=2 "+ - "! audioconvert "+ - "! queue "+ - "! voaacenc bitrate=%d "+ - "! mux. "+ - "ximagesrc display-name=%s show-pointer=true use-damage=false "+ - "! video/x-raw "+ - "! videoconvert "+ - "! queue "+ - "! x264enc threads=4 bitrate=%d key-int-max=15 byte-stream=true tune=zerolatency speed-preset=%s "+ - "! mux.", url, config.AudioDevice, config.BroadcastAudioBitrate*1000, config.Display, config.BroadcastVideoBitrate, config.BroadcastPreset, - ), nil - }, config.BroadcastUrl), - screencast: screencastNew(config.ScreencastEnabled, func() string { - if config.ScreencastPipeline != "" { - // replace {display} with valid display - return strings.Replace(config.ScreencastPipeline, "{display}", config.Display, 1) - } - - return fmt.Sprintf( - "ximagesrc display-name=%s show-pointer=true use-damage=false "+ - "! video/x-raw,framerate=%s "+ - "! videoconvert "+ - "! queue "+ - "! jpegenc quality=%s "+ - "! appsink name=appsink", config.Display, config.ScreencastRate, config.ScreencastQuality, - ) - }()), - - audio: streamSinkNew(config.AudioCodec, func() (string, error) { - if config.AudioPipeline != "" { - // replace {device} with valid device - return strings.Replace(config.AudioPipeline, "{device}", config.AudioDevice, 1), nil - } - - return fmt.Sprintf( - "pulsesrc device=%s "+ - "! audio/x-raw,channels=2 "+ - "! audioconvert "+ - "! queue "+ - "! %s "+ - "! appsink name=appsink", config.AudioDevice, config.AudioCodec.Pipeline, - ), nil - }, "audio"), - video: streamSelectorNew(config.VideoCodec, videos, config.VideoIDs), - - // sources - webcam: streamSrcNew(config.WebcamEnabled, map[string]string{ - codec.VP8().Name: "appsrc format=time is-live=true do-timestamp=true name=appsrc " + - fmt.Sprintf("! application/x-rtp, payload=%d, encoding-name=VP8-DRAFT-IETF-01 ", codec.VP8().PayloadType) + - "! rtpvp8depay " + - "! decodebin " + - "! videoconvert " + - "! videorate " + - "! videoscale " + - fmt.Sprintf("! video/x-raw,width=%d,height=%d ", config.WebcamWidth, config.WebcamHeight) + - "! identity drop-allocation=true " + - fmt.Sprintf("! v4l2sink sync=false device=%s", config.WebcamDevice), - // TODO: Test this pipeline. - codec.VP9().Name: "appsrc format=time is-live=true do-timestamp=true name=appsrc " + - "! application/x-rtp " + - "! rtpvp9depay " + - "! decodebin " + - "! videoconvert " + - "! videorate " + - "! videoscale " + - fmt.Sprintf("! video/x-raw,width=%d,height=%d ", config.WebcamWidth, config.WebcamHeight) + - "! identity drop-allocation=true " + - fmt.Sprintf("! v4l2sink sync=false device=%s", config.WebcamDevice), - // TODO: Test this pipeline. - codec.H264().Name: "appsrc format=time is-live=true do-timestamp=true name=appsrc " + - "! application/x-rtp " + - "! rtph264depay " + - "! decodebin " + - "! videoconvert " + - "! videorate " + - "! videoscale " + - fmt.Sprintf("! video/x-raw,width=%d,height=%d ", config.WebcamWidth, config.WebcamHeight) + - "! identity drop-allocation=true " + - fmt.Sprintf("! v4l2sink sync=false device=%s", config.WebcamDevice), - }, "webcam"), - microphone: streamSrcNew(config.MicrophoneEnabled, map[string]string{ - codec.Opus().Name: "appsrc format=time is-live=true do-timestamp=true name=appsrc " + - fmt.Sprintf("! application/x-rtp, payload=%d, encoding-name=OPUS ", codec.Opus().PayloadType) + - "! rtpopusdepay " + - "! decodebin " + - fmt.Sprintf("! pulsesink device=%s", config.MicrophoneDevice), - // TODO: Test this pipeline. - codec.G722().Name: "appsrc format=time is-live=true do-timestamp=true name=appsrc " + - "! application/x-rtp clock-rate=8000 " + - "! rtpg722depay " + - "! decodebin " + - fmt.Sprintf("! pulsesink device=%s", config.MicrophoneDevice), - }, "microphone"), - } -} - -func (manager *CaptureManagerCtx) Start() { - if manager.broadcast.Started() { - if err := manager.broadcast.createPipeline(); err != nil { - manager.logger.Panic().Err(err).Msg("unable to create broadcast pipeline") - } - } - - manager.desktop.OnBeforeScreenSizeChange(func() { - manager.video.destroyPipelines() - - if manager.broadcast.Started() { - manager.broadcast.destroyPipeline() - } - - if manager.screencast.Started() { - manager.screencast.destroyPipeline() - } - }) - - manager.desktop.OnAfterScreenSizeChange(func() { - err := manager.video.recreatePipelines() - if err != nil { - manager.logger.Panic().Err(err).Msg("unable to recreate video pipelines") - } - - if manager.broadcast.Started() { - err := manager.broadcast.createPipeline() - if err != nil && !errors.Is(err, types.ErrCapturePipelineAlreadyExists) { - manager.logger.Panic().Err(err).Msg("unable to recreate broadcast pipeline") - } - } - - if manager.screencast.Started() { - err := manager.screencast.createPipeline() - if err != nil && !errors.Is(err, types.ErrCapturePipelineAlreadyExists) { - manager.logger.Panic().Err(err).Msg("unable to recreate screencast pipeline") - } - } - }) -} - -func (manager *CaptureManagerCtx) Shutdown() error { - manager.logger.Info().Msgf("shutdown") - - manager.broadcast.shutdown() - manager.screencast.shutdown() - - manager.audio.shutdown() - manager.video.shutdown() - - manager.webcam.shutdown() - manager.microphone.shutdown() - - return nil -} - -func (manager *CaptureManagerCtx) Broadcast() types.BroadcastManager { - return manager.broadcast -} - -func (manager *CaptureManagerCtx) Screencast() types.ScreencastManager { - return manager.screencast -} - -func (manager *CaptureManagerCtx) Audio() types.StreamSinkManager { - return manager.audio -} - -func (manager *CaptureManagerCtx) Video() types.StreamSelectorManager { - return manager.video -} - -func (manager *CaptureManagerCtx) Webcam() types.StreamSrcManager { - return manager.webcam -} - -func (manager *CaptureManagerCtx) Microphone() types.StreamSrcManager { - return manager.microphone -} diff --git a/internal/capture/streamsink.go b/internal/capture/streamsink.go deleted file mode 100644 index 99703bba..00000000 --- a/internal/capture/streamsink.go +++ /dev/null @@ -1,414 +0,0 @@ -package capture - -import ( - "errors" - "reflect" - "sync" - "sync/atomic" - "time" - - "github.com/prometheus/client_golang/prometheus" - "github.com/prometheus/client_golang/prometheus/promauto" - - "github.com/rs/zerolog" - "github.com/rs/zerolog/log" - - "github.com/demodesk/neko/pkg/gst" - "github.com/demodesk/neko/pkg/types" - "github.com/demodesk/neko/pkg/types/codec" -) - -var moveSinkListenerMu = sync.Mutex{} - -type StreamSinkManagerCtx struct { - id string - - // wait for a keyframe before sending samples - waitForKf bool - - bitrate uint64 // atomic - brBuckets map[int]float64 - - logger zerolog.Logger - mu sync.Mutex - wg sync.WaitGroup - - codec codec.RTPCodec - pipeline gst.Pipeline - pipelineMu sync.Mutex - pipelineFn func() (string, error) - - listeners map[uintptr]types.SampleListener - listenersKf map[uintptr]types.SampleListener // keyframe lobby - listenersMu sync.Mutex - - // metrics - currentListeners prometheus.Gauge - totalBytes prometheus.Counter - pipelinesCounter prometheus.Counter - pipelinesActive prometheus.Gauge -} - -func streamSinkNew(codec codec.RTPCodec, pipelineFn func() (string, error), id string) *StreamSinkManagerCtx { - logger := log.With(). - Str("module", "capture"). - Str("submodule", "stream-sink"). - Str("id", id).Logger() - - manager := &StreamSinkManagerCtx{ - id: id, - - // only wait for keyframes if the codec is video - waitForKf: codec.IsVideo(), - - bitrate: 0, - brBuckets: map[int]float64{}, - - logger: logger, - codec: codec, - pipelineFn: pipelineFn, - - listeners: map[uintptr]types.SampleListener{}, - listenersKf: map[uintptr]types.SampleListener{}, - - // metrics - currentListeners: promauto.NewGauge(prometheus.GaugeOpts{ - Name: "streamsink_listeners", - Namespace: "neko", - Subsystem: "capture", - Help: "Current number of listeners for a pipeline.", - ConstLabels: map[string]string{ - "video_id": id, - "codec_name": codec.Name, - "codec_type": codec.Type.String(), - }, - }), - totalBytes: promauto.NewCounter(prometheus.CounterOpts{ - Name: "streamsink_bytes", - Namespace: "neko", - Subsystem: "capture", - Help: "Total number of bytes created by the pipeline.", - ConstLabels: map[string]string{ - "video_id": id, - "codec_name": codec.Name, - "codec_type": codec.Type.String(), - }, - }), - pipelinesCounter: promauto.NewCounter(prometheus.CounterOpts{ - Name: "pipelines_total", - Namespace: "neko", - Subsystem: "capture", - Help: "Total number of created pipelines.", - ConstLabels: map[string]string{ - "submodule": "streamsink", - "video_id": id, - "codec_name": codec.Name, - "codec_type": codec.Type.String(), - }, - }), - pipelinesActive: promauto.NewGauge(prometheus.GaugeOpts{ - Name: "pipelines_active", - Namespace: "neko", - Subsystem: "capture", - Help: "Total number of active pipelines.", - ConstLabels: map[string]string{ - "submodule": "streamsink", - "video_id": id, - "codec_name": codec.Name, - "codec_type": codec.Type.String(), - }, - }), - } - - return manager -} - -func (manager *StreamSinkManagerCtx) shutdown() { - manager.logger.Info().Msgf("shutdown") - - manager.listenersMu.Lock() - for key := range manager.listeners { - delete(manager.listeners, key) - } - for key := range manager.listenersKf { - delete(manager.listenersKf, key) - } - manager.listenersMu.Unlock() - - manager.DestroyPipeline() - manager.wg.Wait() -} - -func (manager *StreamSinkManagerCtx) ID() string { - return manager.id -} - -func (manager *StreamSinkManagerCtx) Bitrate() uint64 { - return atomic.LoadUint64(&manager.bitrate) -} - -func (manager *StreamSinkManagerCtx) Codec() codec.RTPCodec { - return manager.codec -} - -func (manager *StreamSinkManagerCtx) start() error { - if len(manager.listeners)+len(manager.listenersKf) == 0 { - err := manager.CreatePipeline() - if err != nil && !errors.Is(err, types.ErrCapturePipelineAlreadyExists) { - return err - } - - manager.logger.Info().Msgf("first listener, starting") - } - - return nil -} - -func (manager *StreamSinkManagerCtx) stop() { - if len(manager.listeners)+len(manager.listenersKf) == 0 { - manager.DestroyPipeline() - manager.logger.Info().Msgf("last listener, stopping") - } -} - -func (manager *StreamSinkManagerCtx) addListener(listener types.SampleListener) { - ptr := reflect.ValueOf(listener).Pointer() - emitKeyframe := false - - manager.listenersMu.Lock() - if manager.waitForKf { - // if this is the first listener, we need to emit a keyframe - emitKeyframe = len(manager.listenersKf) == 0 - // if we're waiting for a keyframe, add it to the keyframe lobby - manager.listenersKf[ptr] = listener - } else { - // otherwise, add it as a regular listener - manager.listeners[ptr] = listener - } - manager.listenersMu.Unlock() - - manager.logger.Debug().Interface("ptr", ptr).Msgf("adding listener") - manager.currentListeners.Set(float64(manager.ListenersCount())) - - // if we will be waiting for a keyframe, emit one now - if manager.pipeline != nil && emitKeyframe { - manager.pipeline.EmitVideoKeyframe() - } -} - -func (manager *StreamSinkManagerCtx) removeListener(listener types.SampleListener) { - ptr := reflect.ValueOf(listener).Pointer() - - manager.listenersMu.Lock() - delete(manager.listeners, ptr) - delete(manager.listenersKf, ptr) // if it's a keyframe listener, remove it too - manager.listenersMu.Unlock() - - manager.logger.Debug().Interface("ptr", ptr).Msgf("removing listener") - manager.currentListeners.Set(float64(manager.ListenersCount())) -} - -func (manager *StreamSinkManagerCtx) AddListener(listener types.SampleListener) error { - manager.mu.Lock() - defer manager.mu.Unlock() - - if listener == nil { - return errors.New("listener cannot be nil") - } - - // start if stopped - if err := manager.start(); err != nil { - return err - } - - // add listener - manager.addListener(listener) - - return nil -} - -func (manager *StreamSinkManagerCtx) RemoveListener(listener types.SampleListener) error { - manager.mu.Lock() - defer manager.mu.Unlock() - - if listener == nil { - return errors.New("listener cannot be nil") - } - - // remove listener - manager.removeListener(listener) - - // stop if started - manager.stop() - - return nil -} - -// moving listeners between streams ensures, that target pipeline is running -// before listener is added, and stops source pipeline if there are 0 listeners -func (manager *StreamSinkManagerCtx) MoveListenerTo(listener types.SampleListener, stream types.StreamSinkManager) error { - if listener == nil { - return errors.New("listener cannot be nil") - } - - targetStream, ok := stream.(*StreamSinkManagerCtx) - if !ok { - return errors.New("target stream manager does not support moving listeners") - } - - // we need to acquire both mutextes, from source stream and from target stream - // in order to do that safely (without possibility of deadlock) we need third - // global mutex, that ensures atomic locking - - // lock global mutex - moveSinkListenerMu.Lock() - - // lock source stream - manager.mu.Lock() - defer manager.mu.Unlock() - - // lock target stream - targetStream.mu.Lock() - defer targetStream.mu.Unlock() - - // unlock global mutex - moveSinkListenerMu.Unlock() - - // start if stopped - if err := targetStream.start(); err != nil { - return err - } - - // swap listeners - manager.removeListener(listener) - targetStream.addListener(listener) - - // stop if started - manager.stop() - - return nil -} - -func (manager *StreamSinkManagerCtx) ListenersCount() int { - manager.listenersMu.Lock() - defer manager.listenersMu.Unlock() - - return len(manager.listeners) + len(manager.listenersKf) -} - -func (manager *StreamSinkManagerCtx) Started() bool { - return manager.ListenersCount() > 0 -} - -func (manager *StreamSinkManagerCtx) CreatePipeline() error { - manager.pipelineMu.Lock() - defer manager.pipelineMu.Unlock() - - if manager.pipeline != nil { - return types.ErrCapturePipelineAlreadyExists - } - - pipelineStr, err := manager.pipelineFn() - if err != nil { - return err - } - - manager.logger.Info(). - Str("codec", manager.codec.Name). - Str("src", pipelineStr). - Msgf("creating pipeline") - - manager.pipeline, err = gst.CreatePipeline(pipelineStr) - if err != nil { - return err - } - - manager.pipeline.AttachAppsink("appsink") - manager.pipeline.Play() - - manager.wg.Add(1) - pipeline := manager.pipeline - - go func() { - manager.logger.Debug().Msg("started emitting samples") - defer manager.wg.Done() - - for { - sample, ok := <-pipeline.Sample() - if !ok { - manager.logger.Debug().Msg("stopped emitting samples") - return - } - - manager.onSample(sample) - } - }() - - manager.pipelinesCounter.Inc() - manager.pipelinesActive.Set(1) - - return nil -} - -func (manager *StreamSinkManagerCtx) saveSampleBitrate(timestamp time.Time, delta float64) { - // get unix timestamp in seconds - sec := timestamp.Unix() - // last bucket is timestamp rounded to 3 seconds - 1 second - last := int((sec - 1) % 3) - // current bucket is timestamp rounded to 3 seconds - curr := int(sec % 3) - // next bucket is timestamp rounded to 3 seconds + 1 second - next := int((sec + 1) % 3) - - if manager.brBuckets[next] != 0 { - // atomic update bitrate - atomic.StoreUint64(&manager.bitrate, uint64(manager.brBuckets[last])) - // empty next bucket - manager.brBuckets[next] = 0 - } - - // add rate to current bucket - manager.brBuckets[curr] += delta -} - -func (manager *StreamSinkManagerCtx) onSample(sample types.Sample) { - manager.listenersMu.Lock() - defer manager.listenersMu.Unlock() - - // save to metrics - length := float64(sample.Length) - manager.totalBytes.Add(length) - manager.saveSampleBitrate(sample.Timestamp, length) - - // if is not delta unit -> it can be decoded independently -> it is a keyframe - if manager.waitForKf && !sample.DeltaUnit && len(manager.listenersKf) > 0 { - // if current sample is a keyframe, move listeners from - // keyframe lobby to actual listeners map and clear lobby - for k, v := range manager.listenersKf { - manager.listeners[k] = v - } - manager.listenersKf = make(map[uintptr]types.SampleListener) - } - - for _, l := range manager.listeners { - l.WriteSample(sample) - } -} - -func (manager *StreamSinkManagerCtx) DestroyPipeline() { - manager.pipelineMu.Lock() - defer manager.pipelineMu.Unlock() - - if manager.pipeline == nil { - return - } - - manager.pipeline.Destroy() - manager.logger.Info().Msgf("destroying pipeline") - manager.pipeline = nil - - manager.pipelinesActive.Set(0) - - manager.brBuckets = make(map[int]float64) - atomic.StoreUint64(&manager.bitrate, 0) -} diff --git a/internal/config/capture.go b/internal/config/capture.go deleted file mode 100644 index 6e23b4a8..00000000 --- a/internal/config/capture.go +++ /dev/null @@ -1,246 +0,0 @@ -package config - -import ( - "os" - - "github.com/rs/zerolog/log" - "github.com/spf13/cobra" - "github.com/spf13/viper" - - "github.com/demodesk/neko/pkg/types" - "github.com/demodesk/neko/pkg/types/codec" - "github.com/demodesk/neko/pkg/utils" -) - -type Capture struct { - Display string - - VideoCodec codec.RTPCodec - VideoIDs []string - VideoPipelines map[string]types.VideoConfig - - AudioDevice string - AudioCodec codec.RTPCodec - AudioPipeline string - - BroadcastAudioBitrate int - BroadcastVideoBitrate int - BroadcastPreset string - BroadcastPipeline string - BroadcastUrl string - - ScreencastEnabled bool - ScreencastRate string - ScreencastQuality string - ScreencastPipeline string - - WebcamEnabled bool - WebcamDevice string - WebcamWidth int - WebcamHeight int - - MicrophoneEnabled bool - MicrophoneDevice string -} - -func (Capture) Init(cmd *cobra.Command) error { - // audio - cmd.PersistentFlags().String("capture.audio.device", "audio_output.monitor", "pulseaudio device to capture") - if err := viper.BindPFlag("capture.audio.device", cmd.PersistentFlags().Lookup("capture.audio.device")); err != nil { - return err - } - - cmd.PersistentFlags().String("capture.audio.codec", "opus", "audio codec to be used") - if err := viper.BindPFlag("capture.audio.codec", cmd.PersistentFlags().Lookup("capture.audio.codec")); err != nil { - return err - } - - cmd.PersistentFlags().String("capture.audio.pipeline", "", "gstreamer pipeline used for audio streaming") - if err := viper.BindPFlag("capture.audio.pipeline", cmd.PersistentFlags().Lookup("capture.audio.pipeline")); err != nil { - return err - } - - // videos - cmd.PersistentFlags().String("capture.video.codec", "vp8", "video codec to be used") - if err := viper.BindPFlag("capture.video.codec", cmd.PersistentFlags().Lookup("capture.video.codec")); err != nil { - return err - } - - cmd.PersistentFlags().StringSlice("capture.video.ids", []string{}, "ordered list of video ids") - if err := viper.BindPFlag("capture.video.ids", cmd.PersistentFlags().Lookup("capture.video.ids")); err != nil { - return err - } - - cmd.PersistentFlags().String("capture.video.pipelines", "[]", "pipelines config in JSON used for video streaming") - if err := viper.BindPFlag("capture.video.pipelines", cmd.PersistentFlags().Lookup("capture.video.pipelines")); err != nil { - return err - } - - // broadcast - cmd.PersistentFlags().Int("capture.broadcast.audio_bitrate", 128, "broadcast audio bitrate in KB/s") - if err := viper.BindPFlag("capture.broadcast.audio_bitrate", cmd.PersistentFlags().Lookup("capture.broadcast.audio_bitrate")); err != nil { - return err - } - - cmd.PersistentFlags().Int("capture.broadcast.video_bitrate", 4096, "broadcast video bitrate in KB/s") - if err := viper.BindPFlag("capture.broadcast.video_bitrate", cmd.PersistentFlags().Lookup("capture.broadcast.video_bitrate")); err != nil { - return err - } - - cmd.PersistentFlags().String("capture.broadcast.preset", "veryfast", "broadcast speed preset for h264 encoding") - if err := viper.BindPFlag("capture.broadcast.preset", cmd.PersistentFlags().Lookup("capture.broadcast.preset")); err != nil { - return err - } - - cmd.PersistentFlags().String("capture.broadcast.pipeline", "", "gstreamer pipeline used for broadcasting") - if err := viper.BindPFlag("capture.broadcast.pipeline", cmd.PersistentFlags().Lookup("capture.broadcast.pipeline")); err != nil { - return err - } - - cmd.PersistentFlags().String("capture.broadcast.url", "", "initial URL for broadcasting, setting this value will automatically start broadcasting") - if err := viper.BindPFlag("capture.broadcast.url", cmd.PersistentFlags().Lookup("capture.broadcast.url")); err != nil { - return err - } - - // screencast - cmd.PersistentFlags().Bool("capture.screencast.enabled", false, "enable screencast") - if err := viper.BindPFlag("capture.screencast.enabled", cmd.PersistentFlags().Lookup("capture.screencast.enabled")); err != nil { - return err - } - - cmd.PersistentFlags().String("capture.screencast.rate", "10/1", "screencast frame rate") - if err := viper.BindPFlag("capture.screencast.rate", cmd.PersistentFlags().Lookup("capture.screencast.rate")); err != nil { - return err - } - - cmd.PersistentFlags().String("capture.screencast.quality", "60", "screencast JPEG quality") - if err := viper.BindPFlag("capture.screencast.quality", cmd.PersistentFlags().Lookup("capture.screencast.quality")); err != nil { - return err - } - - cmd.PersistentFlags().String("capture.screencast.pipeline", "", "gstreamer pipeline used for screencasting") - if err := viper.BindPFlag("capture.screencast.pipeline", cmd.PersistentFlags().Lookup("capture.screencast.pipeline")); err != nil { - return err - } - - // webcam - cmd.PersistentFlags().Bool("capture.webcam.enabled", false, "enable webcam stream") - if err := viper.BindPFlag("capture.webcam.enabled", cmd.PersistentFlags().Lookup("capture.webcam.enabled")); err != nil { - return err - } - - // sudo apt install v4l2loopback-dkms v4l2loopback-utils - // sudo apt-get install linux-headers-`uname -r` linux-modules-extra-`uname -r` - // sudo modprobe v4l2loopback exclusive_caps=1 - cmd.PersistentFlags().String("capture.webcam.device", "/dev/video0", "v4l2sink device used for webcam") - if err := viper.BindPFlag("capture.webcam.device", cmd.PersistentFlags().Lookup("capture.webcam.device")); err != nil { - return err - } - - cmd.PersistentFlags().Int("capture.webcam.width", 1280, "webcam stream width") - if err := viper.BindPFlag("capture.webcam.width", cmd.PersistentFlags().Lookup("capture.webcam.width")); err != nil { - return err - } - - cmd.PersistentFlags().Int("capture.webcam.height", 720, "webcam stream height") - if err := viper.BindPFlag("capture.webcam.height", cmd.PersistentFlags().Lookup("capture.webcam.height")); err != nil { - return err - } - - // microphone - cmd.PersistentFlags().Bool("capture.microphone.enabled", true, "enable microphone stream") - if err := viper.BindPFlag("capture.microphone.enabled", cmd.PersistentFlags().Lookup("capture.microphone.enabled")); err != nil { - return err - } - - cmd.PersistentFlags().String("capture.microphone.device", "audio_input", "pulseaudio device used for microphone") - if err := viper.BindPFlag("capture.microphone.device", cmd.PersistentFlags().Lookup("capture.microphone.device")); err != nil { - return err - } - - return nil -} - -func (s *Capture) Set() { - var ok bool - - // Display is provided by env variable - s.Display = os.Getenv("DISPLAY") - - // video - videoCodec := viper.GetString("capture.video.codec") - s.VideoCodec, ok = codec.ParseStr(videoCodec) - if !ok || !s.VideoCodec.IsVideo() { - log.Warn().Str("codec", videoCodec).Msgf("unknown video codec, using Vp8") - s.VideoCodec = codec.VP8() - } - - s.VideoIDs = viper.GetStringSlice("capture.video.ids") - if err := viper.UnmarshalKey("capture.video.pipelines", &s.VideoPipelines, viper.DecodeHook( - utils.JsonStringAutoDecode(s.VideoPipelines), - )); err != nil { - log.Warn().Err(err).Msgf("unable to parse video pipelines") - } - - // default video - if len(s.VideoPipelines) == 0 { - log.Warn().Msgf("no video pipelines specified, using defaults") - - s.VideoCodec = codec.VP8() - s.VideoPipelines = map[string]types.VideoConfig{ - "main": { - Fps: "25", - GstEncoder: "vp8enc", - GstParams: map[string]string{ - "target-bitrate": "round(3072 * 650)", - "cpu-used": "4", - "end-usage": "cbr", - "threads": "4", - "deadline": "1", - "undershoot": "95", - "buffer-size": "(3072 * 4)", - "buffer-initial-size": "(3072 * 2)", - "buffer-optimal-size": "(3072 * 3)", - "keyframe-max-dist": "25", - "min-quantizer": "4", - "max-quantizer": "20", - }, - }, - } - s.VideoIDs = []string{"main"} - } - - // audio - s.AudioDevice = viper.GetString("capture.audio.device") - s.AudioPipeline = viper.GetString("capture.audio.pipeline") - - audioCodec := viper.GetString("capture.audio.codec") - s.AudioCodec, ok = codec.ParseStr(audioCodec) - if !ok || !s.AudioCodec.IsAudio() { - log.Warn().Str("codec", audioCodec).Msgf("unknown audio codec, using Opus") - s.AudioCodec = codec.Opus() - } - - // broadcast - s.BroadcastAudioBitrate = viper.GetInt("capture.broadcast.audio_bitrate") - s.BroadcastVideoBitrate = viper.GetInt("capture.broadcast.video_bitrate") - s.BroadcastPreset = viper.GetString("capture.broadcast.preset") - s.BroadcastPipeline = viper.GetString("capture.broadcast.pipeline") - s.BroadcastUrl = viper.GetString("capture.broadcast.url") - - // screencast - s.ScreencastEnabled = viper.GetBool("capture.screencast.enabled") - s.ScreencastRate = viper.GetString("capture.screencast.rate") - s.ScreencastQuality = viper.GetString("capture.screencast.quality") - s.ScreencastPipeline = viper.GetString("capture.screencast.pipeline") - - // webcam - s.WebcamEnabled = viper.GetBool("capture.webcam.enabled") - s.WebcamDevice = viper.GetString("capture.webcam.device") - s.WebcamWidth = viper.GetInt("capture.webcam.width") - s.WebcamHeight = viper.GetInt("capture.webcam.height") - - // microphone - s.MicrophoneEnabled = viper.GetBool("capture.microphone.enabled") - s.MicrophoneDevice = viper.GetString("capture.microphone.device") -} diff --git a/internal/config/config.go b/internal/config/config.go deleted file mode 100644 index 68a2abc2..00000000 --- a/internal/config/config.go +++ /dev/null @@ -1,8 +0,0 @@ -package config - -import "github.com/spf13/cobra" - -type Config interface { - Init(cmd *cobra.Command) error - Set() -} diff --git a/internal/config/desktop.go b/internal/config/desktop.go deleted file mode 100644 index a021ddfc..00000000 --- a/internal/config/desktop.go +++ /dev/null @@ -1,91 +0,0 @@ -package config - -import ( - "os" - "regexp" - "strconv" - - "github.com/spf13/cobra" - "github.com/spf13/viper" - - "github.com/demodesk/neko/pkg/types" -) - -type Desktop struct { - Display string - - ScreenSize types.ScreenSize - - UseInputDriver bool - InputSocket string - - Unminimize bool - UploadDrop bool - FileChooserDialog bool -} - -func (Desktop) Init(cmd *cobra.Command) error { - cmd.PersistentFlags().String("desktop.screen", "1280x720@30", "default screen size and framerate") - if err := viper.BindPFlag("desktop.screen", cmd.PersistentFlags().Lookup("desktop.screen")); err != nil { - return err - } - - cmd.PersistentFlags().Bool("desktop.input.enabled", true, "whether custom xf86 input driver should be used to handle touchscreen") - if err := viper.BindPFlag("desktop.input.enabled", cmd.PersistentFlags().Lookup("desktop.input.enabled")); err != nil { - return err - } - - cmd.PersistentFlags().String("desktop.input.socket", "/tmp/xf86-input-neko.sock", "socket path for custom xf86 input driver connection") - if err := viper.BindPFlag("desktop.input.socket", cmd.PersistentFlags().Lookup("desktop.input.socket")); err != nil { - return err - } - - cmd.PersistentFlags().Bool("desktop.unminimize", true, "automatically unminimize window when it is minimized") - if err := viper.BindPFlag("desktop.unminimize", cmd.PersistentFlags().Lookup("desktop.unminimize")); err != nil { - return err - } - - cmd.PersistentFlags().Bool("desktop.upload_drop", true, "whether drop upload is enabled") - if err := viper.BindPFlag("desktop.upload_drop", cmd.PersistentFlags().Lookup("desktop.upload_drop")); err != nil { - return err - } - - cmd.PersistentFlags().Bool("desktop.file_chooser_dialog", false, "whether to handle file chooser dialog externally") - if err := viper.BindPFlag("desktop.file_chooser_dialog", cmd.PersistentFlags().Lookup("desktop.file_chooser_dialog")); err != nil { - return err - } - - return nil -} - -func (s *Desktop) Set() { - // Display is provided by env variable - s.Display = os.Getenv("DISPLAY") - - s.ScreenSize = types.ScreenSize{ - Width: 1280, - Height: 720, - Rate: 30, - } - - r := regexp.MustCompile(`([0-9]{1,4})x([0-9]{1,4})@([0-9]{1,3})`) - res := r.FindStringSubmatch(viper.GetString("desktop.screen")) - - if len(res) > 0 { - width, err1 := strconv.ParseInt(res[1], 10, 64) - height, err2 := strconv.ParseInt(res[2], 10, 64) - rate, err3 := strconv.ParseInt(res[3], 10, 64) - - if err1 == nil && err2 == nil && err3 == nil { - s.ScreenSize.Width = int(width) - s.ScreenSize.Height = int(height) - s.ScreenSize.Rate = int16(rate) - } - } - - s.UseInputDriver = viper.GetBool("desktop.input.enabled") - s.InputSocket = viper.GetString("desktop.input.socket") - s.Unminimize = viper.GetBool("desktop.unminimize") - s.UploadDrop = viper.GetBool("desktop.upload_drop") - s.FileChooserDialog = viper.GetBool("desktop.file_chooser_dialog") -} diff --git a/internal/config/root.go b/internal/config/root.go deleted file mode 100644 index e9870eda..00000000 --- a/internal/config/root.go +++ /dev/null @@ -1,97 +0,0 @@ -package config - -import ( - "os" - - "github.com/rs/zerolog" - "github.com/rs/zerolog/log" - "github.com/spf13/cobra" - "github.com/spf13/viper" -) - -type Root struct { - Config string - - LogLevel zerolog.Level - LogTime string - LogJson bool - LogNocolor bool - LogDir string -} - -func (Root) Init(cmd *cobra.Command) error { - cmd.PersistentFlags().StringP("config", "c", "", "configuration file path") - if err := viper.BindPFlag("config", cmd.PersistentFlags().Lookup("config")); err != nil { - return err - } - - // just a shortcut - cmd.PersistentFlags().BoolP("debug", "d", false, "enable debug mode") - if err := viper.BindPFlag("debug", cmd.PersistentFlags().Lookup("debug")); 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 - } - - cmd.PersistentFlags().String("log.time", "unix", "time format used in logs (unix, unixms, unixmicro)") - if err := viper.BindPFlag("log.time", cmd.PersistentFlags().Lookup("log.time")); err != nil { - return err - } - - cmd.PersistentFlags().Bool("log.json", false, "logs in JSON format") - if err := viper.BindPFlag("log.json", cmd.PersistentFlags().Lookup("log.json")); err != nil { - return err - } - - cmd.PersistentFlags().Bool("log.nocolor", false, "no ANSI colors in non-JSON output") - if err := viper.BindPFlag("log.nocolor", cmd.PersistentFlags().Lookup("log.nocolor")); err != nil { - return err - } - - cmd.PersistentFlags().String("log.dir", "", "logging directory to store logs") - if err := viper.BindPFlag("log.dir", cmd.PersistentFlags().Lookup("log.dir")); err != nil { - return err - } - - return nil -} - -func (s *Root) Set() { - s.Config = viper.GetString("config") - - logLevel := viper.GetString("log.level") - level, err := zerolog.ParseLevel(logLevel) - if err != nil { - log.Warn().Msgf("unknown log level %s", logLevel) - } else { - s.LogLevel = level - } - - logTime := viper.GetString("log.time") - switch logTime { - case "unix": - s.LogTime = zerolog.TimeFormatUnix - case "unixms": - s.LogTime = zerolog.TimeFormatUnixMs - case "unixmicro": - s.LogTime = zerolog.TimeFormatUnixMicro - default: - log.Warn().Msgf("unknown log time %s", logTime) - } - - s.LogJson = viper.GetBool("log.json") - s.LogNocolor = viper.GetBool("log.nocolor") - s.LogDir = viper.GetString("log.dir") - - if viper.GetBool("debug") && s.LogLevel != zerolog.TraceLevel { - s.LogLevel = zerolog.DebugLevel - } - - // support for NO_COLOR env variable: https://no-color.org/ - if os.Getenv("NO_COLOR") != "" { - s.LogNocolor = true - } -} diff --git a/internal/config/server.go b/internal/config/server.go deleted file mode 100644 index 9ed30859..00000000 --- a/internal/config/server.go +++ /dev/null @@ -1,103 +0,0 @@ -package config - -import ( - "path" - - "github.com/spf13/cobra" - "github.com/spf13/viper" - - "github.com/demodesk/neko/pkg/utils" -) - -type Server struct { - Cert string - Key string - Bind string - Proxy bool - Static string - PathPrefix string - PProf bool - Metrics bool - CORS []string -} - -func (Server) Init(cmd *cobra.Command) error { - cmd.PersistentFlags().String("server.bind", "127.0.0.1:8080", "address/port/socket to serve neko") - if err := viper.BindPFlag("server.bind", cmd.PersistentFlags().Lookup("server.bind")); err != nil { - return err - } - - cmd.PersistentFlags().String("server.cert", "", "path to the SSL cert used to secure the neko server") - if err := viper.BindPFlag("server.cert", cmd.PersistentFlags().Lookup("server.cert")); err != nil { - return err - } - - cmd.PersistentFlags().String("server.key", "", "path to the SSL key used to secure the neko server") - if err := viper.BindPFlag("server.key", cmd.PersistentFlags().Lookup("server.key")); err != nil { - return err - } - - cmd.PersistentFlags().Bool("server.proxy", false, "trust reverse proxy headers") - if err := viper.BindPFlag("server.proxy", cmd.PersistentFlags().Lookup("server.proxy")); err != nil { - return err - } - - cmd.PersistentFlags().String("server.static", "", "path to neko client files to serve") - if err := viper.BindPFlag("server.static", cmd.PersistentFlags().Lookup("server.static")); err != nil { - return err - } - - cmd.PersistentFlags().String("server.path_prefix", "/", "path prefix for HTTP requests") - if err := viper.BindPFlag("server.path_prefix", cmd.PersistentFlags().Lookup("server.path_prefix")); err != nil { - return err - } - - cmd.PersistentFlags().Bool("server.pprof", false, "enable pprof endpoint available at /debug/pprof") - if err := viper.BindPFlag("server.pprof", cmd.PersistentFlags().Lookup("server.pprof")); err != nil { - return err - } - - cmd.PersistentFlags().Bool("server.metrics", true, "enable prometheus metrics available at /metrics") - if err := viper.BindPFlag("server.metrics", cmd.PersistentFlags().Lookup("server.metrics")); err != nil { - return err - } - - cmd.PersistentFlags().StringSlice("server.cors", []string{}, "list of allowed origins for CORS, if empty CORS is disabled, if '*' is present all origins are allowed") - if err := viper.BindPFlag("server.cors", cmd.PersistentFlags().Lookup("server.cors")); err != nil { - return err - } - - return nil -} - -func (s *Server) Set() { - s.Cert = viper.GetString("server.cert") - s.Key = viper.GetString("server.key") - s.Bind = viper.GetString("server.bind") - s.Proxy = viper.GetBool("server.proxy") - s.Static = viper.GetString("server.static") - s.PathPrefix = path.Join("/", path.Clean(viper.GetString("server.path_prefix"))) - s.PProf = viper.GetBool("server.pprof") - s.Metrics = viper.GetBool("server.metrics") - - s.CORS = viper.GetStringSlice("server.cors") - in, _ := utils.ArrayIn("*", s.CORS) - if len(s.CORS) == 0 || in { - s.CORS = []string{"*"} - } -} - -func (s *Server) HasCors() bool { - return len(s.CORS) > 0 -} - -func (s *Server) AllowOrigin(origin string) bool { - // if CORS is disabled, allow all origins - if len(s.CORS) == 0 { - return true - } - - // if CORS is enabled, allow only origins in the list - in, _ := utils.ArrayIn(origin, s.CORS) - return in || s.CORS[0] == "*" -} diff --git a/internal/config/webrtc.go b/internal/config/webrtc.go deleted file mode 100644 index f3023007..00000000 --- a/internal/config/webrtc.go +++ /dev/null @@ -1,273 +0,0 @@ -package config - -import ( - "strconv" - "strings" - "time" - - "github.com/rs/zerolog/log" - "github.com/spf13/cobra" - "github.com/spf13/viper" - - "github.com/demodesk/neko/pkg/types" - "github.com/demodesk/neko/pkg/utils" -) - -// default stun server -const defStunSrv = "stun:stun.l.google.com:19302" - -type WebRTCEstimator struct { - Enabled bool - Passive bool - Debug bool - InitialBitrate int - - // how often to read and process bandwidth estimation reports - ReadInterval time.Duration - // how long to wait for stable connection (only neutral or upward trend) before upgrading - StableDuration time.Duration - // how long to wait for unstable connection (downward trend) before downgrading - UnstableDuration time.Duration - // how long to wait for stalled connection (neutral trend with low bandwidth) before downgrading - StalledDuration time.Duration - // how long to wait before downgrading again after previous downgrade - DowngradeBackoff time.Duration - // how long to wait before upgrading again after previous upgrade - UpgradeBackoff time.Duration - // how bigger the difference between estimated and stream bitrate must be to trigger upgrade/downgrade - DiffThreshold float64 -} - -type WebRTC struct { - ICELite bool - ICETrickle bool - ICEServersFrontend []types.ICEServer - ICEServersBackend []types.ICEServer - EphemeralMin uint16 - EphemeralMax uint16 - TCPMux int - UDPMux int - - NAT1To1IPs []string - IpRetrievalUrl string - - Estimator WebRTCEstimator -} - -func (WebRTC) Init(cmd *cobra.Command) error { - cmd.PersistentFlags().Bool("webrtc.icelite", false, "configures whether or not the ICE agent should be a lite agent") - if err := viper.BindPFlag("webrtc.icelite", cmd.PersistentFlags().Lookup("webrtc.icelite")); err != nil { - return err - } - - cmd.PersistentFlags().Bool("webrtc.icetrickle", true, "configures whether cadidates should be sent asynchronously using Trickle ICE") - if err := viper.BindPFlag("webrtc.icetrickle", cmd.PersistentFlags().Lookup("webrtc.icetrickle")); err != nil { - return err - } - - // Looks like this is conflicting with the frontend and backend ICE servers since latest versions - //cmd.PersistentFlags().String("webrtc.iceservers", "[]", "Global STUN and TURN servers in JSON format with `urls`, `username` and `credential` keys") - //if err := viper.BindPFlag("webrtc.iceservers", cmd.PersistentFlags().Lookup("webrtc.iceservers")); err != nil { - // return err - //} - - cmd.PersistentFlags().String("webrtc.iceservers.frontend", "[]", "Frontend only STUN and TURN servers in JSON format with `urls`, `username` and `credential` keys") - if err := viper.BindPFlag("webrtc.iceservers.frontend", cmd.PersistentFlags().Lookup("webrtc.iceservers.frontend")); err != nil { - return err - } - - cmd.PersistentFlags().String("webrtc.iceservers.backend", "[]", "Backend only STUN and TURN servers in JSON format with `urls`, `username` and `credential` keys") - if err := viper.BindPFlag("webrtc.iceservers.backend", cmd.PersistentFlags().Lookup("webrtc.iceservers.backend")); err != nil { - return err - } - - cmd.PersistentFlags().String("webrtc.epr", "", "limits the pool of ephemeral ports that ICE UDP connections can allocate from") - if err := viper.BindPFlag("webrtc.epr", cmd.PersistentFlags().Lookup("webrtc.epr")); err != nil { - return err - } - - cmd.PersistentFlags().Int("webrtc.tcpmux", 0, "single TCP mux port for all peers") - if err := viper.BindPFlag("webrtc.tcpmux", cmd.PersistentFlags().Lookup("webrtc.tcpmux")); err != nil { - return err - } - - cmd.PersistentFlags().Int("webrtc.udpmux", 0, "single UDP mux port for all peers, replaces EPR") - if err := viper.BindPFlag("webrtc.udpmux", cmd.PersistentFlags().Lookup("webrtc.udpmux")); err != nil { - return err - } - - cmd.PersistentFlags().StringSlice("webrtc.nat1to1", []string{}, "sets a list of external IP addresses of 1:1 (D)NAT and a candidate type for which the external IP address is used") - if err := viper.BindPFlag("webrtc.nat1to1", cmd.PersistentFlags().Lookup("webrtc.nat1to1")); err != nil { - return err - } - - cmd.PersistentFlags().String("webrtc.ip_retrieval_url", "https://checkip.amazonaws.com", "URL address used for retrieval of the external IP address") - if err := viper.BindPFlag("webrtc.ip_retrieval_url", cmd.PersistentFlags().Lookup("webrtc.ip_retrieval_url")); err != nil { - return err - } - - // bandwidth estimator - - cmd.PersistentFlags().Bool("webrtc.estimator.enabled", false, "enables the bandwidth estimator") - if err := viper.BindPFlag("webrtc.estimator.enabled", cmd.PersistentFlags().Lookup("webrtc.estimator.enabled")); err != nil { - return err - } - - cmd.PersistentFlags().Bool("webrtc.estimator.passive", false, "passive estimator mode, when it does not switch pipelines, only estimates") - if err := viper.BindPFlag("webrtc.estimator.passive", cmd.PersistentFlags().Lookup("webrtc.estimator.passive")); err != nil { - return err - } - - cmd.PersistentFlags().Bool("webrtc.estimator.debug", false, "enables debug logging for the bandwidth estimator") - if err := viper.BindPFlag("webrtc.estimator.debug", cmd.PersistentFlags().Lookup("webrtc.estimator.debug")); err != nil { - return err - } - - cmd.PersistentFlags().Int("webrtc.estimator.initial_bitrate", 1_000_000, "initial bitrate for the bandwidth estimator") - if err := viper.BindPFlag("webrtc.estimator.initial_bitrate", cmd.PersistentFlags().Lookup("webrtc.estimator.initial_bitrate")); err != nil { - return err - } - - cmd.PersistentFlags().Duration("webrtc.estimator.read_interval", 2*time.Second, "how often to read and process bandwidth estimation reports") - if err := viper.BindPFlag("webrtc.estimator.read_interval", cmd.PersistentFlags().Lookup("webrtc.estimator.read_interval")); err != nil { - return err - } - - cmd.PersistentFlags().Duration("webrtc.estimator.stable_duration", 12*time.Second, "how long to wait for stable connection (upward or neutral trend) before upgrading") - if err := viper.BindPFlag("webrtc.estimator.stable_duration", cmd.PersistentFlags().Lookup("webrtc.estimator.stable_duration")); err != nil { - return err - } - - cmd.PersistentFlags().Duration("webrtc.estimator.unstable_duration", 6*time.Second, "how long to wait for stalled connection (neutral trend with low bandwidth) before downgrading") - if err := viper.BindPFlag("webrtc.estimator.unstable_duration", cmd.PersistentFlags().Lookup("webrtc.estimator.unstable_duration")); err != nil { - return err - } - - cmd.PersistentFlags().Duration("webrtc.estimator.stalled_duration", 24*time.Second, "how long to wait for stalled bandwidth estimation before downgrading") - if err := viper.BindPFlag("webrtc.estimator.stalled_duration", cmd.PersistentFlags().Lookup("webrtc.estimator.stalled_duration")); err != nil { - return err - } - - cmd.PersistentFlags().Duration("webrtc.estimator.downgrade_backoff", 10*time.Second, "how long to wait before downgrading again after previous downgrade") - if err := viper.BindPFlag("webrtc.estimator.downgrade_backoff", cmd.PersistentFlags().Lookup("webrtc.estimator.downgrade_backoff")); err != nil { - return err - } - - cmd.PersistentFlags().Duration("webrtc.estimator.upgrade_backoff", 5*time.Second, "how long to wait before upgrading again after previous upgrade") - if err := viper.BindPFlag("webrtc.estimator.upgrade_backoff", cmd.PersistentFlags().Lookup("webrtc.estimator.upgrade_backoff")); err != nil { - return err - } - - cmd.PersistentFlags().Float64("webrtc.estimator.diff_threshold", 0.15, "how bigger the difference between estimated and stream bitrate must be to trigger upgrade/downgrade") - if err := viper.BindPFlag("webrtc.estimator.diff_threshold", cmd.PersistentFlags().Lookup("webrtc.estimator.diff_threshold")); err != nil { - return err - } - - return nil -} - -func (s *WebRTC) Set() { - s.ICELite = viper.GetBool("webrtc.icelite") - s.ICETrickle = viper.GetBool("webrtc.icetrickle") - - // parse frontend ice servers - if err := viper.UnmarshalKey("webrtc.iceservers.frontend", &s.ICEServersFrontend, viper.DecodeHook( - utils.JsonStringAutoDecode([]types.ICEServer{}), - )); err != nil { - log.Warn().Err(err).Msgf("unable to parse frontend ICE servers") - } - - // parse backend ice servers - if err := viper.UnmarshalKey("webrtc.iceservers.backend", &s.ICEServersBackend, viper.DecodeHook( - utils.JsonStringAutoDecode([]types.ICEServer{}), - )); err != nil { - log.Warn().Err(err).Msgf("unable to parse backend ICE servers") - } - - if s.ICELite && len(s.ICEServersBackend) > 0 { - log.Warn().Msgf("ICE Lite is enabled, but backend ICE servers are configured. Backend ICE servers will be ignored.") - } - - // if no frontend or backend ice servers are configured - if len(s.ICEServersFrontend) == 0 && len(s.ICEServersBackend) == 0 { - // parse global ice servers - var iceServers []types.ICEServer - if err := viper.UnmarshalKey("webrtc.iceservers", &iceServers, viper.DecodeHook( - utils.JsonStringAutoDecode([]types.ICEServer{}), - )); err != nil { - log.Warn().Err(err).Msgf("unable to parse global ICE servers") - } - - // add default stun server if none are configured - if len(iceServers) == 0 { - iceServers = append(iceServers, types.ICEServer{ - URLs: []string{defStunSrv}, - }) - } - - s.ICEServersFrontend = append(s.ICEServersFrontend, iceServers...) - s.ICEServersBackend = append(s.ICEServersBackend, iceServers...) - } - - s.TCPMux = viper.GetInt("webrtc.tcpmux") - s.UDPMux = viper.GetInt("webrtc.udpmux") - - epr := viper.GetString("webrtc.epr") - if epr != "" { - ports := strings.SplitN(epr, "-", -1) - if len(ports) > 1 { - min, err := strconv.ParseUint(ports[0], 10, 16) - if err != nil { - log.Panic().Err(err).Msgf("unable to parse ephemeral min port") - } - - max, err := strconv.ParseUint(ports[1], 10, 16) - if err != nil { - log.Panic().Err(err).Msgf("unable to parse ephemeral max port") - } - - s.EphemeralMin = uint16(min) - s.EphemeralMax = uint16(max) - } - - if s.EphemeralMin > s.EphemeralMax { - log.Panic().Msgf("ephemeral min port cannot be bigger than max") - } - } - - if epr == "" && s.TCPMux == 0 && s.UDPMux == 0 { - // using default epr range - s.EphemeralMin = 59000 - s.EphemeralMax = 59100 - - log.Warn(). - Uint16("min", s.EphemeralMin). - Uint16("max", s.EphemeralMax). - Msgf("no TCP, UDP mux or epr specified, using default epr range") - } - - s.NAT1To1IPs = viper.GetStringSlice("webrtc.nat1to1") - s.IpRetrievalUrl = viper.GetString("webrtc.ip_retrieval_url") - if s.IpRetrievalUrl != "" && len(s.NAT1To1IPs) == 0 { - ip, err := utils.HttpRequestGET(s.IpRetrievalUrl) - if err == nil { - s.NAT1To1IPs = append(s.NAT1To1IPs, ip) - } else { - log.Warn().Err(err).Msgf("IP retrieval failed") - } - } - - // bandwidth estimator - - s.Estimator.Enabled = viper.GetBool("webrtc.estimator.enabled") - s.Estimator.Passive = viper.GetBool("webrtc.estimator.passive") - s.Estimator.Debug = viper.GetBool("webrtc.estimator.debug") - s.Estimator.InitialBitrate = viper.GetInt("webrtc.estimator.initial_bitrate") - s.Estimator.ReadInterval = viper.GetDuration("webrtc.estimator.read_interval") - s.Estimator.StableDuration = viper.GetDuration("webrtc.estimator.stable_duration") - s.Estimator.UnstableDuration = viper.GetDuration("webrtc.estimator.unstable_duration") - s.Estimator.StalledDuration = viper.GetDuration("webrtc.estimator.stalled_duration") - s.Estimator.DowngradeBackoff = viper.GetDuration("webrtc.estimator.downgrade_backoff") - s.Estimator.UpgradeBackoff = viper.GetDuration("webrtc.estimator.upgrade_backoff") - s.Estimator.DiffThreshold = viper.GetFloat64("webrtc.estimator.diff_threshold") -} diff --git a/internal/desktop/clipboard.go b/internal/desktop/clipboard.go deleted file mode 100644 index f354fe38..00000000 --- a/internal/desktop/clipboard.go +++ /dev/null @@ -1,122 +0,0 @@ -package desktop - -import ( - "bytes" - "fmt" - "os/exec" - "strings" - - "github.com/demodesk/neko/pkg/types" - "github.com/demodesk/neko/pkg/xevent" -) - -func (manager *DesktopManagerCtx) ClipboardGetText() (*types.ClipboardText, error) { - text, err := manager.ClipboardGetBinary("STRING") - if err != nil { - return nil, err - } - - // Rich text must not always be available, can fail silently. - html, _ := manager.ClipboardGetBinary("text/html") - - return &types.ClipboardText{ - Text: string(text), - HTML: string(html), - }, nil -} - -func (manager *DesktopManagerCtx) ClipboardSetText(data types.ClipboardText) error { - // TODO: Refactor. - // Current implementation is unable to set multiple targets. HTML - // is set, if available. Otherwise plain text. - - if data.HTML != "" { - return manager.ClipboardSetBinary("text/html", []byte(data.HTML)) - } - - return manager.ClipboardSetBinary("STRING", []byte(data.Text)) -} - -func (manager *DesktopManagerCtx) ClipboardGetBinary(mime string) ([]byte, error) { - cmd := exec.Command("xclip", "-selection", "clipboard", "-out", "-target", mime) - - var stdout, stderr bytes.Buffer - cmd.Stdout = &stdout - cmd.Stderr = &stderr - - err := cmd.Run() - if err != nil { - msg := strings.TrimSpace(stderr.String()) - return nil, fmt.Errorf("%s", msg) - } - - return stdout.Bytes(), nil -} - -func (manager *DesktopManagerCtx) ClipboardSetBinary(mime string, data []byte) error { - cmd := exec.Command("xclip", "-selection", "clipboard", "-in", "-target", mime) - - var stderr bytes.Buffer - cmd.Stderr = &stderr - - stdin, err := cmd.StdinPipe() - if err != nil { - return err - } - - // TODO: Refactor. - // We need to wait until the data came to the clipboard. - wait := make(chan struct{}) - xevent.Emmiter.Once("clipboard-updated", func(payload ...any) { - wait <- struct{}{} - }) - - err = cmd.Start() - if err != nil { - msg := strings.TrimSpace(stderr.String()) - return fmt.Errorf("%s", msg) - } - - _, err = stdin.Write(data) - if err != nil { - return err - } - - stdin.Close() - - // TODO: Refactor. - // cmd.Wait() - <-wait - - return nil -} - -func (manager *DesktopManagerCtx) ClipboardGetTargets() ([]string, error) { - cmd := exec.Command("xclip", "-selection", "clipboard", "-out", "-target", "TARGETS") - - var stdout, stderr bytes.Buffer - cmd.Stdout = &stdout - cmd.Stderr = &stderr - - err := cmd.Run() - if err != nil { - msg := strings.TrimSpace(stderr.String()) - return nil, fmt.Errorf("%s", msg) - } - - var response []string - targets := strings.Split(stdout.String(), "\n") - for _, target := range targets { - if target == "" { - continue - } - - if !strings.Contains(target, "/") { - continue - } - - response = append(response, target) - } - - return response, nil -} diff --git a/internal/desktop/manager.go b/internal/desktop/manager.go deleted file mode 100644 index da998df2..00000000 --- a/internal/desktop/manager.go +++ /dev/null @@ -1,138 +0,0 @@ -package desktop - -import ( - "sync" - "time" - - "github.com/kataras/go-events" - "github.com/rs/zerolog" - "github.com/rs/zerolog/log" - - "github.com/demodesk/neko/internal/config" - "github.com/demodesk/neko/pkg/types" - "github.com/demodesk/neko/pkg/xevent" - "github.com/demodesk/neko/pkg/xinput" - "github.com/demodesk/neko/pkg/xorg" -) - -var mu = sync.Mutex{} - -type DesktopManagerCtx struct { - logger zerolog.Logger - wg sync.WaitGroup - shutdown chan struct{} - emmiter events.EventEmmiter - config *config.Desktop - screenSize types.ScreenSize // cached screen size - input xinput.Driver -} - -func New(config *config.Desktop) *DesktopManagerCtx { - var input xinput.Driver - if config.UseInputDriver { - input = xinput.NewDriver(config.InputSocket) - } else { - input = xinput.NewDummy() - } - - return &DesktopManagerCtx{ - logger: log.With().Str("module", "desktop").Logger(), - shutdown: make(chan struct{}), - emmiter: events.New(), - config: config, - screenSize: config.ScreenSize, - input: input, - } -} - -func (manager *DesktopManagerCtx) Start() { - if xorg.DisplayOpen(manager.config.Display) { - manager.logger.Panic().Str("display", manager.config.Display).Msg("unable to open display") - } - - // X11 can throw errors below, and the default error handler exits - xevent.SetupErrorHandler() - - xorg.GetScreenConfigurations() - - screenSize, err := xorg.ChangeScreenSize(manager.config.ScreenSize) - if err != nil { - manager.logger.Err(err). - Str("screen_size", screenSize.String()). - Msgf("unable to set initial screen size") - } else { - // cache screen size - manager.screenSize = screenSize - manager.logger.Info(). - Str("screen_size", screenSize.String()). - Msgf("setting initial screen size") - } - - err = manager.input.Connect() - if err != nil { - // TODO: fail silently to dummy driver? - manager.logger.Panic().Err(err).Msg("unable to connect to input driver") - } - - // set up event listeners - xevent.Unminimize = manager.config.Unminimize - xevent.FileChooserDialog = manager.config.FileChooserDialog - go xevent.EventLoop(manager.config.Display) - - // in case it was opened - if manager.config.FileChooserDialog { - go manager.CloseFileChooserDialog() - } - - manager.OnEventError(func(error_code uint8, message string, request_code uint8, minor_code uint8) { - manager.logger.Warn(). - Uint8("error_code", error_code). - Str("message", message). - Uint8("request_code", request_code). - Uint8("minor_code", minor_code). - Msg("X event error occured") - }) - - manager.wg.Add(1) - - go func() { - defer manager.wg.Done() - - ticker := time.NewTicker(1 * time.Second) - defer ticker.Stop() - - const debounceDuration = 10 * time.Second - - for { - select { - case <-manager.shutdown: - return - case <-ticker.C: - xorg.CheckKeys(debounceDuration) - manager.input.Debounce(debounceDuration) - } - } - }() -} - -func (manager *DesktopManagerCtx) OnBeforeScreenSizeChange(listener func()) { - manager.emmiter.On("before_screen_size_change", func(payload ...any) { - listener() - }) -} - -func (manager *DesktopManagerCtx) OnAfterScreenSizeChange(listener func()) { - manager.emmiter.On("after_screen_size_change", func(payload ...any) { - listener() - }) -} - -func (manager *DesktopManagerCtx) Shutdown() error { - manager.logger.Info().Msgf("shutdown") - - close(manager.shutdown) - manager.wg.Wait() - - xorg.DisplayClose() - return nil -} diff --git a/internal/desktop/xevent.go b/internal/desktop/xevent.go deleted file mode 100644 index 59df1d7e..00000000 --- a/internal/desktop/xevent.go +++ /dev/null @@ -1,35 +0,0 @@ -package desktop - -import ( - "github.com/demodesk/neko/pkg/xevent" -) - -func (manager *DesktopManagerCtx) OnCursorChanged(listener func(serial uint64)) { - xevent.Emmiter.On("cursor-changed", func(payload ...any) { - listener(payload[0].(uint64)) - }) -} - -func (manager *DesktopManagerCtx) OnClipboardUpdated(listener func()) { - xevent.Emmiter.On("clipboard-updated", func(payload ...any) { - listener() - }) -} - -func (manager *DesktopManagerCtx) OnFileChooserDialogOpened(listener func()) { - xevent.Emmiter.On("file-chooser-dialog-opened", func(payload ...any) { - listener() - }) -} - -func (manager *DesktopManagerCtx) OnFileChooserDialogClosed(listener func()) { - xevent.Emmiter.On("file-chooser-dialog-closed", func(payload ...any) { - listener() - }) -} - -func (manager *DesktopManagerCtx) OnEventError(listener func(error_code uint8, message string, request_code uint8, minor_code uint8)) { - xevent.Emmiter.On("event-error", func(payload ...any) { - listener(payload[0].(uint8), payload[1].(string), payload[2].(uint8), payload[3].(uint8)) - }) -} diff --git a/internal/desktop/xorg.go b/internal/desktop/xorg.go deleted file mode 100644 index c0feb5dc..00000000 --- a/internal/desktop/xorg.go +++ /dev/null @@ -1,202 +0,0 @@ -package desktop - -import ( - "image" - "os/exec" - "regexp" - "time" - - "github.com/demodesk/neko/pkg/types" - "github.com/demodesk/neko/pkg/xorg" -) - -func (manager *DesktopManagerCtx) Move(x, y int) { - xorg.Move(x, y) -} - -func (manager *DesktopManagerCtx) GetCursorPosition() (int, int) { - return xorg.GetCursorPosition() -} - -func (manager *DesktopManagerCtx) Scroll(deltaX, deltaY int, controlKey bool) { - xorg.Scroll(deltaX, deltaY, controlKey) -} - -func (manager *DesktopManagerCtx) ButtonDown(code uint32) error { - return xorg.ButtonDown(code) -} - -func (manager *DesktopManagerCtx) KeyDown(code uint32) error { - return xorg.KeyDown(code) -} - -func (manager *DesktopManagerCtx) ButtonUp(code uint32) error { - return xorg.ButtonUp(code) -} - -func (manager *DesktopManagerCtx) KeyUp(code uint32) error { - return xorg.KeyUp(code) -} - -func (manager *DesktopManagerCtx) ButtonPress(code uint32) error { - xorg.ResetKeys() - defer xorg.ResetKeys() - - return xorg.ButtonDown(code) -} - -func (manager *DesktopManagerCtx) KeyPress(codes ...uint32) error { - xorg.ResetKeys() - defer xorg.ResetKeys() - - for _, code := range codes { - if err := xorg.KeyDown(code); err != nil { - return err - } - } - - if len(codes) > 1 { - time.Sleep(10 * time.Millisecond) - } - - return nil -} - -func (manager *DesktopManagerCtx) ResetKeys() { - xorg.ResetKeys() -} - -func (manager *DesktopManagerCtx) ScreenConfigurations() []types.ScreenSize { - var configs []types.ScreenSize - for _, size := range xorg.ScreenConfigurations { - for _, fps := range size.Rates { - // filter out all irrelevant rates - if fps > 60 || (fps > 30 && fps%10 != 0) { - continue - } - - configs = append(configs, types.ScreenSize{ - Width: size.Width, - Height: size.Height, - Rate: fps, - }) - } - } - return configs -} - -func (manager *DesktopManagerCtx) SetScreenSize(screenSize types.ScreenSize) (types.ScreenSize, error) { - mu.Lock() - manager.emmiter.Emit("before_screen_size_change") - - defer func() { - manager.emmiter.Emit("after_screen_size_change") - mu.Unlock() - }() - - screenSize, err := xorg.ChangeScreenSize(screenSize) - if err == nil { - // cache the new screen size - manager.screenSize = screenSize - } - - return screenSize, err -} - -func (manager *DesktopManagerCtx) GetScreenSize() types.ScreenSize { - return xorg.GetScreenSize() -} - -func (manager *DesktopManagerCtx) SetKeyboardMap(kbd types.KeyboardMap) error { - // TOOD: Use native API. - cmd := exec.Command("setxkbmap", "-layout", kbd.Layout, "-variant", kbd.Variant) - _, err := cmd.Output() - return err -} - -func (manager *DesktopManagerCtx) GetKeyboardMap() (*types.KeyboardMap, error) { - // TOOD: Use native API. - cmd := exec.Command("setxkbmap", "-query") - res, err := cmd.Output() - if err != nil { - return nil, err - } - - kbd := types.KeyboardMap{} - - re := regexp.MustCompile(`layout:\s+(.*)\n`) - arr := re.FindStringSubmatch(string(res)) - if len(arr) > 1 { - kbd.Layout = arr[1] - } - - re = regexp.MustCompile(`variant:\s+(.*)\n`) - arr = re.FindStringSubmatch(string(res)) - if len(arr) > 1 { - kbd.Variant = arr[1] - } - - return &kbd, nil -} - -func (manager *DesktopManagerCtx) SetKeyboardModifiers(mod types.KeyboardModifiers) { - if mod.Shift != nil { - xorg.SetKeyboardModifier(xorg.KbdModShift, *mod.Shift) - } - - if mod.CapsLock != nil { - xorg.SetKeyboardModifier(xorg.KbdModCapsLock, *mod.CapsLock) - } - - if mod.Control != nil { - xorg.SetKeyboardModifier(xorg.KbdModControl, *mod.Control) - } - - if mod.Alt != nil { - xorg.SetKeyboardModifier(xorg.KbdModAlt, *mod.Alt) - } - - if mod.NumLock != nil { - xorg.SetKeyboardModifier(xorg.KbdModNumLock, *mod.NumLock) - } - - if mod.Meta != nil { - xorg.SetKeyboardModifier(xorg.KbdModMeta, *mod.Meta) - } - - if mod.Super != nil { - xorg.SetKeyboardModifier(xorg.KbdModSuper, *mod.Super) - } - - if mod.AltGr != nil { - xorg.SetKeyboardModifier(xorg.KbdModAltGr, *mod.AltGr) - } -} - -func (manager *DesktopManagerCtx) GetKeyboardModifiers() types.KeyboardModifiers { - modifiers := xorg.GetKeyboardModifiers() - - isset := func(mod xorg.KbdMod) *bool { - x := modifiers&mod != 0 - return &x - } - - return types.KeyboardModifiers{ - Shift: isset(xorg.KbdModShift), - CapsLock: isset(xorg.KbdModCapsLock), - Control: isset(xorg.KbdModControl), - Alt: isset(xorg.KbdModAlt), - NumLock: isset(xorg.KbdModNumLock), - Meta: isset(xorg.KbdModMeta), - Super: isset(xorg.KbdModSuper), - AltGr: isset(xorg.KbdModAltGr), - } -} - -func (manager *DesktopManagerCtx) GetCursorImage() *types.CursorImage { - return xorg.GetCursorImage() -} - -func (manager *DesktopManagerCtx) GetScreenshotImage() *image.RGBA { - return xorg.GetScreenshotImage() -} diff --git a/internal/http/logger.go b/internal/http/logger.go deleted file mode 100644 index 258eec54..00000000 --- a/internal/http/logger.go +++ /dev/null @@ -1,135 +0,0 @@ -package http - -import ( - "fmt" - "net/http" - "time" - - "github.com/go-chi/chi/middleware" - "github.com/rs/zerolog" - - "github.com/demodesk/neko/pkg/types" - "github.com/demodesk/neko/pkg/utils" -) - -type logFormatter struct { - logger zerolog.Logger -} - -func (l *logFormatter) NewLogEntry(r *http.Request) middleware.LogEntry { - // exclude health & metrics from logs - if r.RequestURI == "/health" || r.RequestURI == "/metrics" { - return &nulllog{} - } - - req := map[string]any{} - - if reqID := middleware.GetReqID(r.Context()); reqID != "" { - req["id"] = reqID - } - - scheme := "http" - if r.TLS != nil { - scheme = "https" - } - - req["scheme"] = scheme - req["proto"] = r.Proto - req["method"] = r.Method - req["remote"] = r.RemoteAddr - req["agent"] = r.UserAgent() - req["uri"] = fmt.Sprintf("%s://%s%s", scheme, r.Host, r.RequestURI) - - return &logEntry{ - logger: l.logger.With().Interface("req", req).Logger(), - } -} - -type logEntry struct { - logger zerolog.Logger - err error - panic *logPanic - session types.Session -} - -type logPanic struct { - message string - stack string -} - -func (e *logEntry) Panic(v any, stack []byte) { - e.panic = &logPanic{ - message: fmt.Sprintf("%+v", v), - stack: string(stack), - } -} - -func (e *logEntry) Error(err error) { - e.err = err -} - -func (e *logEntry) SetSession(session types.Session) { - e.session = session -} - -func (e *logEntry) Write(status, bytes int, header http.Header, elapsed time.Duration, extra any) { - res := map[string]any{} - res["time"] = time.Now().UTC().Format(time.RFC1123) - res["status"] = status - res["bytes"] = bytes - res["elapsed"] = float64(elapsed.Nanoseconds()) / 1000000.0 - - logger := e.logger.With().Interface("res", res).Logger() - - // add session ID to logs (if exists) - if e.session != nil { - logger = logger.With().Str("session_id", e.session.ID()).Logger() - } - - // handle panic error message - if e.panic != nil { - logger.WithLevel(zerolog.PanicLevel). - Err(e.err). - Str("stack", e.panic.stack). - Msgf("request failed (%d): %s", status, e.panic.message) - return - } - - // handle panic error message - if e.err != nil { - httpErr, ok := e.err.(*utils.HTTPError) - if !ok { - logger.Err(e.err).Msgf("request failed (%d)", status) - return - } - - if httpErr.Message == "" { - httpErr.Message = http.StatusText(httpErr.Code) - } - - var logLevel zerolog.Level - if httpErr.Code < 500 { - logLevel = zerolog.WarnLevel - } else { - logLevel = zerolog.ErrorLevel - } - - message := httpErr.Message - if httpErr.InternalMsg != "" { - message = httpErr.InternalMsg - } - - logger.WithLevel(logLevel).Err(httpErr.InternalErr).Msgf("request failed (%d): %s", status, message) - return - } - - logger.Debug().Msgf("request complete (%d)", status) -} - -type nulllog struct{} - -func (e *nulllog) Panic(v any, stack []byte) {} -func (e *nulllog) Error(err error) {} -func (e *nulllog) SetSession(session types.Session) {} -func (e *nulllog) Write(status, bytes int, header http.Header, elapsed time.Duration, extra any) { -} diff --git a/internal/session/manager.go b/internal/session/manager.go deleted file mode 100644 index df853456..00000000 --- a/internal/session/manager.go +++ /dev/null @@ -1,412 +0,0 @@ -package session - -import ( - "errors" - "sync" - "sync/atomic" - - "github.com/kataras/go-events" - "github.com/rs/zerolog" - "github.com/rs/zerolog/log" - - "github.com/demodesk/neko/internal/config" - "github.com/demodesk/neko/pkg/types" - "github.com/demodesk/neko/pkg/utils" -) - -func New(config *config.Session) *SessionManagerCtx { - manager := &SessionManagerCtx{ - logger: log.With().Str("module", "session").Logger(), - config: config, - settings: types.Settings{ - PrivateMode: config.PrivateMode, - LockedControls: config.LockedControls, - ImplicitHosting: config.ImplicitHosting, - InactiveCursors: config.InactiveCursors, - MercifulReconnect: config.MercifulReconnect, - }, - tokens: make(map[string]string), - sessions: make(map[string]*SessionCtx), - cursors: make(map[types.Session][]types.Cursor), - emmiter: events.New(), - } - - // create API session - if config.APIToken != "" { - manager.apiSession = &SessionCtx{ - id: "API", - token: config.APIToken, - manager: manager, - logger: manager.logger.With().Str("session_id", "API").Logger(), - profile: types.MemberProfile{ - Name: "API Session", - IsAdmin: true, - CanLogin: true, - CanConnect: false, - CanWatch: true, - CanHost: true, - CanAccessClipboard: true, - }, - } - } - - // try to load sessions from file - manager.load() - - return manager -} - -type SessionManagerCtx struct { - logger zerolog.Logger - config *config.Session - - settings types.Settings - settingsMu sync.Mutex - - tokens map[string]string - sessions map[string]*SessionCtx - sessionsMu sync.Mutex - - hostId atomic.Value - - cursors map[types.Session][]types.Cursor - cursorsMu sync.Mutex - - emmiter events.EventEmmiter - apiSession *SessionCtx -} - -func (manager *SessionManagerCtx) Create(id string, profile types.MemberProfile) (types.Session, string, error) { - token, err := utils.NewUID(64) - if err != nil { - return nil, "", err - } - - manager.sessionsMu.Lock() - if _, ok := manager.sessions[id]; ok { - manager.sessionsMu.Unlock() - return nil, "", types.ErrSessionAlreadyExists - } - - if _, ok := manager.tokens[token]; ok { - manager.sessionsMu.Unlock() - return nil, "", errors.New("session token already exists") - } - - session := &SessionCtx{ - id: id, - token: token, - manager: manager, - logger: manager.logger.With().Str("session_id", id).Logger(), - profile: profile, - } - - manager.tokens[token] = id - manager.sessions[id] = session - manager.sessionsMu.Unlock() - - manager.emmiter.Emit("created", session) - manager.save() - - return session, token, nil -} - -func (manager *SessionManagerCtx) Update(id string, profile types.MemberProfile) error { - manager.sessionsMu.Lock() - - session, ok := manager.sessions[id] - if !ok { - manager.sessionsMu.Unlock() - return types.ErrSessionNotFound - } - - session.profile = profile - manager.sessionsMu.Unlock() - - manager.emmiter.Emit("profile_changed", session) - manager.save() - - session.profileChanged() - return nil -} - -func (manager *SessionManagerCtx) Delete(id string) error { - manager.sessionsMu.Lock() - session, ok := manager.sessions[id] - if !ok { - manager.sessionsMu.Unlock() - return types.ErrSessionNotFound - } - - delete(manager.tokens, session.token) - delete(manager.sessions, id) - manager.sessionsMu.Unlock() - - if session.State().IsConnected { - session.DestroyWebSocketPeer("session deleted") - } - - if session.State().IsWatching { - session.GetWebRTCPeer().Destroy() - } - - manager.emmiter.Emit("deleted", session) - manager.save() - - return nil -} - -func (manager *SessionManagerCtx) Get(id string) (types.Session, bool) { - manager.sessionsMu.Lock() - defer manager.sessionsMu.Unlock() - - session, ok := manager.sessions[id] - return session, ok -} - -func (manager *SessionManagerCtx) GetByToken(token string) (types.Session, bool) { - manager.sessionsMu.Lock() - id, ok := manager.tokens[token] - manager.sessionsMu.Unlock() - - if ok { - return manager.Get(id) - } - - // is API session - if manager.apiSession != nil && manager.apiSession.token == token { - return manager.apiSession, true - } - - return nil, false -} - -func (manager *SessionManagerCtx) List() []types.Session { - manager.sessionsMu.Lock() - defer manager.sessionsMu.Unlock() - - var sessions []types.Session - for _, session := range manager.sessions { - sessions = append(sessions, session) - } - - return sessions -} - -// --- -// host -// --- - -func (manager *SessionManagerCtx) SetHost(host types.Session) { - var hostId string - if host != nil { - hostId = host.ID() - } - - manager.hostId.Store(hostId) - manager.emmiter.Emit("host_changed", host) -} - -func (manager *SessionManagerCtx) GetHost() (types.Session, bool) { - hostId, ok := manager.hostId.Load().(string) - if !ok || hostId == "" { - return nil, false - } - - return manager.Get(hostId) -} - -func (manager *SessionManagerCtx) ClearHost() { - manager.SetHost(nil) -} - -func (manager *SessionManagerCtx) isHost(host types.Session) bool { - hostId, ok := manager.hostId.Load().(string) - return ok && hostId == host.ID() -} - -// --- -// cursors -// --- - -func (manager *SessionManagerCtx) SetCursor(cursor types.Cursor, session types.Session) { - manager.cursorsMu.Lock() - defer manager.cursorsMu.Unlock() - - list, ok := manager.cursors[session] - if !ok { - list = []types.Cursor{} - } - - list = append(list, cursor) - manager.cursors[session] = list -} - -func (manager *SessionManagerCtx) PopCursors() map[types.Session][]types.Cursor { - manager.cursorsMu.Lock() - defer manager.cursorsMu.Unlock() - - cursors := manager.cursors - manager.cursors = make(map[types.Session][]types.Cursor) - - return cursors -} - -// --- -// broadcasts -// --- - -func (manager *SessionManagerCtx) Broadcast(event string, payload any, exclude ...string) { - for _, session := range manager.List() { - if !session.State().IsConnected { - continue - } - - if len(exclude) > 0 { - if in, _ := utils.ArrayIn(session.ID(), exclude); in { - continue - } - } - - session.Send(event, payload) - } -} - -func (manager *SessionManagerCtx) AdminBroadcast(event string, payload any, exclude ...string) { - for _, session := range manager.List() { - if !session.State().IsConnected || !session.Profile().IsAdmin { - continue - } - - if len(exclude) > 0 { - if in, _ := utils.ArrayIn(session.ID(), exclude); in { - continue - } - } - - session.Send(event, payload) - } -} - -func (manager *SessionManagerCtx) InactiveCursorsBroadcast(event string, payload any, exclude ...string) { - for _, session := range manager.List() { - if !session.State().IsConnected || !session.Profile().CanSeeInactiveCursors { - continue - } - - if len(exclude) > 0 { - if in, _ := utils.ArrayIn(session.ID(), exclude); in { - continue - } - } - - session.Send(event, payload) - } -} - -// --- -// events -// --- - -func (manager *SessionManagerCtx) OnCreated(listener func(session types.Session)) { - manager.emmiter.On("created", func(payload ...any) { - listener(payload[0].(*SessionCtx)) - }) -} - -func (manager *SessionManagerCtx) OnDeleted(listener func(session types.Session)) { - manager.emmiter.On("deleted", func(payload ...any) { - listener(payload[0].(*SessionCtx)) - }) -} - -func (manager *SessionManagerCtx) OnConnected(listener func(session types.Session)) { - manager.emmiter.On("connected", func(payload ...any) { - listener(payload[0].(*SessionCtx)) - }) -} - -func (manager *SessionManagerCtx) OnDisconnected(listener func(session types.Session)) { - manager.emmiter.On("disconnected", func(payload ...any) { - listener(payload[0].(*SessionCtx)) - }) -} - -func (manager *SessionManagerCtx) OnProfileChanged(listener func(session types.Session)) { - manager.emmiter.On("profile_changed", func(payload ...any) { - listener(payload[0].(*SessionCtx)) - }) -} - -func (manager *SessionManagerCtx) OnStateChanged(listener func(session types.Session)) { - manager.emmiter.On("state_changed", func(payload ...any) { - listener(payload[0].(*SessionCtx)) - }) -} - -func (manager *SessionManagerCtx) OnHostChanged(listener func(session types.Session)) { - manager.emmiter.On("host_changed", func(payload ...any) { - if payload[0] == nil { - listener(nil) - } else { - listener(payload[0].(*SessionCtx)) - } - }) -} - -func (manager *SessionManagerCtx) OnSettingsChanged(listener func(new types.Settings, old types.Settings)) { - manager.emmiter.On("settings_changed", func(payload ...any) { - listener(payload[0].(types.Settings), payload[1].(types.Settings)) - }) -} - -// --- -// settings -// --- - -func (manager *SessionManagerCtx) UpdateSettings(new types.Settings) { - manager.settingsMu.Lock() - old := manager.settings - manager.settings = new - manager.settingsMu.Unlock() - - // if private mode changed - if old.PrivateMode != new.PrivateMode { - // update webrtc paused state for all sessions - for _, session := range manager.List() { - enabled := session.PrivateModeEnabled() - - // if session had control, it must release it - if enabled && session.IsHost() { - manager.ClearHost() - } - - // its webrtc connection will be paused or unpaused - if webrtcPeer := session.GetWebRTCPeer(); webrtcPeer != nil { - webrtcPeer.SetPaused(enabled) - } - } - } - - // if contols have been locked - if old.LockedControls != new.LockedControls && new.LockedControls { - // if the host is not admin, it must release controls - host, hasHost := manager.GetHost() - if hasHost && !host.Profile().IsAdmin { - manager.ClearHost() - } - } - - manager.emmiter.Emit("settings_changed", new, old) -} - -func (manager *SessionManagerCtx) Settings() types.Settings { - manager.settingsMu.Lock() - defer manager.settingsMu.Unlock() - - return manager.settings -} - -func (manager *SessionManagerCtx) CookieEnabled() bool { - return manager.config.CookieEnabled -} diff --git a/internal/session/session.go b/internal/session/session.go deleted file mode 100644 index 716f1115..00000000 --- a/internal/session/session.go +++ /dev/null @@ -1,285 +0,0 @@ -package session - -import ( - "sync" - "time" - - "github.com/rs/zerolog" - - "github.com/demodesk/neko/pkg/types" - "github.com/demodesk/neko/pkg/types/event" -) - -// client is expected to reconnect within 5 second -// if some unexpected websocket disconnect happens -const WS_DELAYED_DURATION = 5 * time.Second - -type SessionCtx struct { - id string - token string - logger zerolog.Logger - manager *SessionManagerCtx - profile types.MemberProfile - state types.SessionState - - websocketPeer types.WebSocketPeer - websocketMu sync.Mutex - - // websocket delayed set connected events - wsDelayedMu sync.Mutex - wsDelayedTimer *time.Timer - - webrtcPeer types.WebRTCPeer - webrtcMu sync.Mutex -} - -func (session *SessionCtx) ID() string { - return session.id -} - -func (session *SessionCtx) Profile() types.MemberProfile { - return session.profile -} - -func (session *SessionCtx) profileChanged() { - if !session.profile.CanHost && session.IsHost() { - session.manager.ClearHost() - } - - if (!session.profile.CanConnect || !session.profile.CanLogin || !session.profile.CanWatch) && session.state.IsWatching { - session.GetWebRTCPeer().Destroy() - } - - if (!session.profile.CanConnect || !session.profile.CanLogin) && session.state.IsConnected { - session.DestroyWebSocketPeer("profile changed") - } - - // update webrtc paused state - if webrtcPeer := session.GetWebRTCPeer(); webrtcPeer != nil { - webrtcPeer.SetPaused(session.PrivateModeEnabled()) - } -} - -func (session *SessionCtx) State() types.SessionState { - return session.state -} - -func (session *SessionCtx) IsHost() bool { - return session.manager.isHost(session) -} - -func (session *SessionCtx) PrivateModeEnabled() bool { - return session.manager.Settings().PrivateMode && !session.profile.IsAdmin -} - -func (session *SessionCtx) SetCursor(cursor types.Cursor) { - if session.manager.Settings().InactiveCursors && session.profile.SendsInactiveCursor { - session.manager.SetCursor(cursor, session) - } -} - -// --- -// websocket -// --- - -// -// Connect WebSocket peer sets current peer and emits connected event. It also destroys the -// previous peer, if there was one. If the peer is already set, it will be ignored. -// -func (session *SessionCtx) ConnectWebSocketPeer(websocketPeer types.WebSocketPeer) { - session.websocketMu.Lock() - isCurrentPeer := websocketPeer == session.websocketPeer - session.websocketPeer, websocketPeer = websocketPeer, session.websocketPeer - session.websocketMu.Unlock() - - // ignore if already set - if isCurrentPeer { - return - } - - session.logger.Info().Msg("set websocket connected") - - // update state - now := time.Now() - session.state.IsConnected = true - session.state.ConnectedSince = &now - session.state.NotConnectedSince = nil - - session.manager.emmiter.Emit("connected", session) - - // if there is a previous peer, destroy it - if websocketPeer != nil { - websocketPeer.Destroy("connection replaced") - } -} - -// -// Disconnect WebSocket peer sets current peer to nil and emits disconnected event. It also -// allows for a delayed disconnect. That means, the peer will not be disconnected immediately, -// but after a delay. If the peer is connected again before the delay, the disconnect will be -// cancelled. -// -// If the peer is not the current peer or the peer is nil, it will be ignored. -// -func (session *SessionCtx) DisconnectWebSocketPeer(websocketPeer types.WebSocketPeer, delayed bool) { - session.websocketMu.Lock() - isCurrentPeer := websocketPeer == session.websocketPeer && websocketPeer != nil - session.websocketMu.Unlock() - - // ignore if not current peer - if !isCurrentPeer { - return - } - - // - // ws delayed - // - - var wsDelayedTimer *time.Timer - - if delayed { - wsDelayedTimer = time.AfterFunc(WS_DELAYED_DURATION, func() { - session.DisconnectWebSocketPeer(websocketPeer, false) - }) - } - - session.wsDelayedMu.Lock() - if session.wsDelayedTimer != nil { - session.wsDelayedTimer.Stop() - } - session.wsDelayedTimer = wsDelayedTimer - session.wsDelayedMu.Unlock() - - if delayed { - session.logger.Info().Msg("delayed websocket disconnected") - return - } - - // - // not delayed - // - - session.logger.Info().Msg("set websocket disconnected") - - now := time.Now() - session.state.IsConnected = false - session.state.ConnectedSince = nil - session.state.NotConnectedSince = &now - - session.manager.emmiter.Emit("disconnected", session) - - session.websocketMu.Lock() - if websocketPeer == session.websocketPeer { - session.websocketPeer = nil - } - session.websocketMu.Unlock() -} - -// -// Destroy WebSocket peer disconnects the peer and destroys it. It ensures that the peer is -// disconnected immediately even though normal flow would be to disconnect it delayed. -// -func (session *SessionCtx) DestroyWebSocketPeer(reason string) { - session.websocketMu.Lock() - peer := session.websocketPeer - session.websocketMu.Unlock() - - if peer == nil { - return - } - - // disconnect peer first, so that it is not used anymore - session.DisconnectWebSocketPeer(peer, false) - - // destroy it afterwards - peer.Destroy(reason) -} - -// -// Send event to websocket peer. -// -func (session *SessionCtx) Send(event string, payload any) { - session.websocketMu.Lock() - peer := session.websocketPeer - session.websocketMu.Unlock() - - if peer != nil { - peer.Send(event, payload) - } -} - -// --- -// webrtc -// --- - -// -// Set webrtc peer and destroy the old one, if there is old one. -// -func (session *SessionCtx) SetWebRTCPeer(webrtcPeer types.WebRTCPeer) { - session.webrtcMu.Lock() - session.webrtcPeer, webrtcPeer = webrtcPeer, session.webrtcPeer - session.webrtcMu.Unlock() - - if webrtcPeer != nil && webrtcPeer != session.webrtcPeer { - webrtcPeer.Destroy() - } -} - -// -// Set if current webrtc peer is connected or not. Since there might be lefover calls from -// webrtc peer, that are not used anymore, we need to check if the webrtc peer is still the -// same as the one we are setting the connected state for. -// -// If webrtc peer is disconnected, we don't expect it to be reconnected, so we set it to nil -// and send a signal close to the client. New connection is expected to use a new webrtc peer. -// -func (session *SessionCtx) SetWebRTCConnected(webrtcPeer types.WebRTCPeer, connected bool) { - session.webrtcMu.Lock() - isCurrentPeer := webrtcPeer == session.webrtcPeer - session.webrtcMu.Unlock() - - if !isCurrentPeer { - return - } - - session.logger.Info(). - Bool("connected", connected). - Msg("set webrtc connected") - - // update state - session.state.IsWatching = connected - if now := time.Now(); connected { - session.state.WatchingSince = &now - session.state.NotWatchingSince = nil - } else { - session.state.WatchingSince = nil - session.state.NotWatchingSince = &now - } - - session.manager.emmiter.Emit("state_changed", session) - - if connected { - return - } - - session.webrtcMu.Lock() - isCurrentPeer = webrtcPeer == session.webrtcPeer - if isCurrentPeer { - session.webrtcPeer = nil - } - session.webrtcMu.Unlock() - - if isCurrentPeer { - session.Send(event.SIGNAL_CLOSE, nil) - } -} - -// -// Get current WebRTC peer. Nil if not connected. -// -func (session *SessionCtx) GetWebRTCPeer() types.WebRTCPeer { - session.webrtcMu.Lock() - defer session.webrtcMu.Unlock() - - return session.webrtcPeer -} diff --git a/internal/webrtc/peer.go b/internal/webrtc/peer.go deleted file mode 100644 index 6abd4952..00000000 --- a/internal/webrtc/peer.go +++ /dev/null @@ -1,543 +0,0 @@ -package webrtc - -import ( - "bytes" - "encoding/binary" - "sync" - "time" - - "github.com/pion/interceptor/pkg/cc" - "github.com/pion/rtcp" - "github.com/pion/webrtc/v3" - "github.com/rs/zerolog" - - "github.com/demodesk/neko/internal/config" - "github.com/demodesk/neko/internal/webrtc/payload" - "github.com/demodesk/neko/pkg/types" - "github.com/demodesk/neko/pkg/types/event" - "github.com/demodesk/neko/pkg/utils" -) - -type WebRTCPeerCtx struct { - mu sync.Mutex - logger zerolog.Logger - session types.Session - metrics *metrics - connection *webrtc.PeerConnection - // bandwidth estimator - estimator cc.BandwidthEstimator - estimateTrend *utils.TrendDetector - // stream selectors - video types.StreamSelectorManager - audio types.StreamSinkManager - // tracks & channels - audioTrack *Track - videoTrack *Track - dataChannel *webrtc.DataChannel - rtcpChannel chan []rtcp.Packet - // config - iceTrickle bool - estimatorConfig config.WebRTCEstimator - paused bool - videoAuto bool - videoDisabled bool - audioDisabled bool -} - -// -// connection -// - -func (peer *WebRTCPeerCtx) CreateOffer(ICERestart bool) (*webrtc.SessionDescription, error) { - peer.mu.Lock() - defer peer.mu.Unlock() - - offer, err := peer.connection.CreateOffer(&webrtc.OfferOptions{ - ICERestart: ICERestart, - }) - if err != nil { - return nil, err - } - - return peer.setLocalDescription(offer) -} - -func (peer *WebRTCPeerCtx) CreateAnswer() (*webrtc.SessionDescription, error) { - peer.mu.Lock() - defer peer.mu.Unlock() - - answer, err := peer.connection.CreateAnswer(nil) - if err != nil { - return nil, err - } - - return peer.setLocalDescription(answer) -} - -func (peer *WebRTCPeerCtx) setLocalDescription(description webrtc.SessionDescription) (*webrtc.SessionDescription, error) { - if !peer.iceTrickle { - // Create channel that is blocked until ICE Gathering is complete - gatherComplete := webrtc.GatheringCompletePromise(peer.connection) - - if err := peer.connection.SetLocalDescription(description); err != nil { - return nil, err - } - - <-gatherComplete - } else { - if err := peer.connection.SetLocalDescription(description); err != nil { - return nil, err - } - } - - return peer.connection.LocalDescription(), nil -} - -func (peer *WebRTCPeerCtx) SetRemoteDescription(desc webrtc.SessionDescription) error { - peer.mu.Lock() - defer peer.mu.Unlock() - - return peer.connection.SetRemoteDescription(desc) -} - -func (peer *WebRTCPeerCtx) SetCandidate(candidate webrtc.ICECandidateInit) error { - peer.mu.Lock() - defer peer.mu.Unlock() - - return peer.connection.AddICECandidate(candidate) -} - -// TODO: Add shutdown function? -func (peer *WebRTCPeerCtx) Destroy() { - peer.mu.Lock() - defer peer.mu.Unlock() - - var err error - - // if peer connection is not closed, close it - if peer.connection.ConnectionState() != webrtc.PeerConnectionStateClosed { - err = peer.connection.Close() - } - - peer.logger.Err(err).Msg("peer connection destroyed") -} - -func (peer *WebRTCPeerCtx) estimatorReader() { - conf := peer.estimatorConfig - - // if estimator is not in debug mode, use a nop logger - var debugLogger zerolog.Logger - if conf.Debug { - debugLogger = peer.logger.With().Str("component", "estimator").Logger().Level(zerolog.DebugLevel) - } else { - debugLogger = zerolog.Nop() - } - - // if estimator is disabled, do nothing - if peer.estimator == nil { - return - } - - // use a ticker to get current client target bitrate - ticker := time.NewTicker(conf.ReadInterval) - defer ticker.Stop() - - // since when is the estimate stable/unstable - stableSince := time.Now() // we asume stable at start - unstableSince := time.Time{} - // since when are we neutral but cannot accomodate current bitrate - // we migt be stalled or estimator just reached zer (very bad connection) - stalledSince := time.Time{} - // when was the last upgrade/downgrade - lastUpgradeTime := time.Time{} - lastDowngradeTime := time.Time{} - - for range ticker.C { - targetBitrate := peer.estimator.GetTargetBitrate() - peer.metrics.SetReceiverEstimatedTargetBitrate(float64(targetBitrate)) - - // if peer connection is closed, stop reading - if peer.connection.ConnectionState() == webrtc.PeerConnectionStateClosed { - break - } - - // if estimation or video is disabled, do nothing - if !peer.videoAuto || peer.videoDisabled || peer.paused || conf.Passive { - continue - } - - // get trend direction to decide if we should upgrade or downgrade - peer.estimateTrend.AddValue(int64(targetBitrate)) - direction := peer.estimateTrend.GetDirection() - - // get current stream bitrate - stream, ok := peer.videoTrack.Stream() - if !ok { - debugLogger.Warn().Msg("looks like we don't have a stream yet, skipping bitrate estimation") - continue - } - - // if stream bitrate is 0, we need to wait for some time until we get a valid value - streamId, streamBitrate := stream.ID(), stream.Bitrate() - if streamBitrate == 0 { - debugLogger.Warn().Msg("looks like stream bitrate is 0, we need to wait for some time") - continue - } - - // check whats the difference between target and stream bitrate - diff := float64(targetBitrate) / float64(streamBitrate) - - debugLogger.Info(). - Float64("diff", diff). - Int("target_bitrate", targetBitrate). - Uint64("stream_bitrate", streamBitrate). - Str("direction", direction.String()). - Msg("got bitrate from estimator") - - // if we can accomodate current stream or we are not netural anymore, - // we are not stalled so we reset the stalled time - if direction != utils.TrendDirectionNeutral || diff > 1+conf.DiffThreshold { - stalledSince = time.Now() - } - - // if we are neutral and stalled for too long, we might be congesting - stalled := direction == utils.TrendDirectionNeutral && time.Since(stalledSince) > conf.StalledDuration - if stalled { - debugLogger.Warn(). - Time("stalled_since", stalledSince). - Msgf("it looks like we are stalled") - } - - // if we have an downward trend or are stalled, we might be congesting - if direction == utils.TrendDirectionDownward || stalled { - // we reset the stable time because we are congesting - stableSince = time.Now() - - // if we downgraded recently, we wait for some more time - if time.Since(lastDowngradeTime) < conf.DowngradeBackoff { - debugLogger.Debug(). - Time("last_downgrade", lastDowngradeTime). - Msgf("downgraded recently, waiting for at least %v", conf.DowngradeBackoff) - continue - } - - // if we are not unstable but we fluctuate we should wait for some more time - if time.Since(unstableSince) < conf.UnstableDuration { - debugLogger.Debug(). - Time("unstable_since", unstableSince). - Msgf("we are not unstable long enough, waiting for at least %v", conf.UnstableDuration) - continue - } - - // if we still have a big difference between target and stream bitrate, we wait for some more time - if conf.DiffThreshold >= 0 && diff > 1+conf.DiffThreshold { - debugLogger.Debug(). - Float64("diff", diff). - Float64("threshold", conf.DiffThreshold). - Msgf("we still have a big difference between target and stream bitrate, " + - "therefore we still should be able to accomodate current stream") - continue - } - - err := peer.SetVideo(types.PeerVideoRequest{ - Selector: &types.StreamSelector{ - ID: streamId, - Type: types.StreamSelectorTypeLower, - }, - }) - if err != nil && err != types.ErrWebRTCStreamNotFound { - peer.logger.Warn().Err(err).Msg("failed to downgrade video stream") - } - lastDowngradeTime = time.Now() - - if err == types.ErrWebRTCStreamNotFound { - debugLogger.Info().Msg("looks like we are already on the lowest stream") - } else { - debugLogger.Info().Msg("downgraded video stream") - } - continue - } - - // we reset the unstable time because we are not congesting - unstableSince = time.Now() - - // if we have a neutral or upward trend, that means our estimate is stable - // if we are on the highest stream, we don't need to do anything - // but if there is a higher stream, we should try to upgrade and see if it works - - // if we upgraded recently, we wait for some more time - if time.Since(lastUpgradeTime) < conf.UpgradeBackoff { - debugLogger.Debug(). - Time("last_upgrade", lastUpgradeTime). - Msgf("upgraded recently, waiting for at least %v", conf.UpgradeBackoff) - continue - } - - // if we are not stable for long enough, we wait for some more time - // because bandwidth estimation might fluctuate - if time.Since(stableSince) < conf.StableDuration { - debugLogger.Debug(). - Time("stable_since", stableSince). - Msgf("we are not stable long enough, waiting for at least %v", conf.StableDuration) - continue - } - - // upgrade only if estimated bitrate passed the threshold - if conf.DiffThreshold >= 0 && diff < 1+conf.DiffThreshold { - debugLogger.Debug(). - Float64("diff", diff). - Float64("threshold", conf.DiffThreshold). - Msgf("looks like we don't have enough bitrate to accomodate higher stream, " + - "therefore we should wait for some more time") - continue - } - - err := peer.SetVideo(types.PeerVideoRequest{ - Selector: &types.StreamSelector{ - ID: streamId, - Type: types.StreamSelectorTypeHigher, - }, - }) - if err != nil && err != types.ErrWebRTCStreamNotFound { - peer.logger.Warn().Err(err).Msg("failed to upgrade video stream") - } - lastUpgradeTime = time.Now() - - if err == types.ErrWebRTCStreamNotFound { - debugLogger.Info().Msg("looks like we are already on the highest stream") - } else { - debugLogger.Info().Msg("upgraded video stream") - } - } -} - -func (peer *WebRTCPeerCtx) SetPaused(isPaused bool) error { - peer.mu.Lock() - defer peer.mu.Unlock() - - peer.videoTrack.SetPaused(isPaused || peer.videoDisabled) - peer.audioTrack.SetPaused(isPaused || peer.audioDisabled) - - peer.logger.Info().Bool("is_paused", isPaused).Msg("set paused") - peer.paused = isPaused - - return nil -} - -func (peer *WebRTCPeerCtx) Paused() bool { - peer.mu.Lock() - defer peer.mu.Unlock() - - return peer.paused -} - -// -// video -// - -func (peer *WebRTCPeerCtx) SetVideo(r types.PeerVideoRequest) error { - peer.mu.Lock() - defer peer.mu.Unlock() - - modified := false - - // video disabled - if r.Disabled != nil { - disabled := *r.Disabled - - // update only if changed - if peer.videoDisabled != disabled { - peer.videoDisabled = disabled - peer.videoTrack.SetPaused(disabled || peer.paused) - - peer.logger.Info().Bool("disabled", disabled).Msg("set video disabled") - modified = true - } - } - - // video selector - if r.Selector != nil { - selector := *r.Selector - - // get requested video stream from selector - stream, ok := peer.video.GetStream(selector) - if !ok { - return types.ErrWebRTCStreamNotFound - } - - // set video stream to track - changed, err := peer.videoTrack.SetStream(stream) - if err != nil { - return err - } - - // update only if stream changed - if changed { - videoID := stream.ID() - peer.metrics.SetVideoID(videoID) - - peer.logger.Info().Str("video_id", videoID).Msg("set video") - modified = true - } - } - - // video auto - if r.Auto != nil { - videoAuto := *r.Auto - - if peer.estimator == nil || peer.estimatorConfig.Passive { - peer.logger.Warn().Msg("estimator is disabled or in passive mode, cannot change video auto") - videoAuto = false // ensure video auto is disabled - } - - // update only if video auto changed - if peer.videoAuto != videoAuto { - peer.videoAuto = videoAuto - - peer.logger.Info().Bool("video_auto", videoAuto).Msg("set video auto") - modified = true - } - } - - // send video signal if modified - if modified { - go func() { - // in goroutine because of mutex and we don't want to block - peer.session.Send(event.SIGNAL_VIDEO, peer.Video()) - }() - } - - return nil -} - -func (peer *WebRTCPeerCtx) Video() types.PeerVideo { - peer.mu.Lock() - defer peer.mu.Unlock() - - // get current video stream ID - ID := "" - stream, ok := peer.videoTrack.Stream() - if ok { - ID = stream.ID() - } - - return types.PeerVideo{ - Disabled: peer.videoDisabled, - ID: ID, - Video: ID, // TODO: Remove, used for backward compatibility - Auto: peer.videoAuto, - } -} - -// -// audio -// - -func (peer *WebRTCPeerCtx) SetAudio(r types.PeerAudioRequest) error { - peer.mu.Lock() - defer peer.mu.Unlock() - - modified := false - - // audio disabled - if r.Disabled != nil { - disabled := *r.Disabled - - // update only if changed - if peer.audioDisabled != disabled { - peer.audioDisabled = disabled - peer.audioTrack.SetPaused(disabled || peer.paused) - - peer.logger.Info().Bool("disabled", disabled).Msg("set audio disabled") - modified = true - } - } - - // send video signal if modified - if modified { - go func() { - // in goroutine because of mutex and we don't want to block - peer.session.Send(event.SIGNAL_AUDIO, peer.Audio()) - }() - } - - return nil -} - -func (peer *WebRTCPeerCtx) Audio() types.PeerAudio { - peer.mu.Lock() - defer peer.mu.Unlock() - - return types.PeerAudio{ - Disabled: peer.audioDisabled, - } -} - -// -// data channel -// - -func (peer *WebRTCPeerCtx) SendCursorPosition(x, y int) error { - peer.mu.Lock() - defer peer.mu.Unlock() - - // do not send cursor position to host - if peer.session.IsHost() { - return nil - } - - header := payload.Header{ - Event: payload.OP_CURSOR_POSITION, - Length: 7, - } - - data := payload.CursorPosition{ - X: uint16(x), - Y: uint16(y), - } - - buffer := &bytes.Buffer{} - - if err := binary.Write(buffer, binary.BigEndian, header); err != nil { - return err - } - - if err := binary.Write(buffer, binary.BigEndian, data); err != nil { - return err - } - - return peer.dataChannel.Send(buffer.Bytes()) -} - -func (peer *WebRTCPeerCtx) SendCursorImage(cur *types.CursorImage, img []byte) error { - peer.mu.Lock() - defer peer.mu.Unlock() - - header := payload.Header{ - Event: payload.OP_CURSOR_IMAGE, - Length: uint16(11 + len(img)), - } - - data := payload.CursorImage{ - Width: cur.Width, - Height: cur.Height, - Xhot: cur.Xhot, - Yhot: cur.Yhot, - } - - buffer := &bytes.Buffer{} - - if err := binary.Write(buffer, binary.BigEndian, header); err != nil { - return err - } - - if err := binary.Write(buffer, binary.BigEndian, data); err != nil { - return err - } - - if err := binary.Write(buffer, binary.BigEndian, img); err != nil { - return err - } - - return peer.dataChannel.Send(buffer.Bytes()) -} diff --git a/internal/webrtc/pionlog/factory.go b/internal/webrtc/pionlog/factory.go deleted file mode 100644 index 5a3a6beb..00000000 --- a/internal/webrtc/pionlog/factory.go +++ /dev/null @@ -1,27 +0,0 @@ -package pionlog - -import ( - "github.com/pion/logging" - "github.com/rs/zerolog" -) - -func New(logger zerolog.Logger) Factory { - return Factory{ - Logger: logger.With().Str("submodule", "pion").Logger(), - } -} - -type Factory struct { - Logger zerolog.Logger -} - -func (l Factory) NewLogger(subsystem string) logging.LeveledLogger { - if subsystem == "sctp" { - return nulllog{} - } - - return logger{ - subsystem: subsystem, - logger: l.Logger.With().Str("subsystem", subsystem).Logger(), - } -} diff --git a/internal/webrtc/pionlog/logger.go b/internal/webrtc/pionlog/logger.go deleted file mode 100644 index 0bb0c292..00000000 --- a/internal/webrtc/pionlog/logger.go +++ /dev/null @@ -1,66 +0,0 @@ -package pionlog - -import ( - "fmt" - "strings" - - "github.com/rs/zerolog" -) - -type logger struct { - logger zerolog.Logger - subsystem string -} - -func (l logger) Trace(msg string) { - l.logger.Trace().Msg(strings.TrimSpace(msg)) -} - -func (l logger) Tracef(format string, args ...any) { - msg := fmt.Sprintf(format, args...) - l.logger.Trace().Msg(strings.TrimSpace(msg)) -} - -func (l logger) Debug(msg string) { - l.logger.Debug().Msg(strings.TrimSpace(msg)) -} - -func (l logger) Debugf(format string, args ...any) { - msg := fmt.Sprintf(format, args...) - l.logger.Debug().Msg(strings.TrimSpace(msg)) -} - -func (l logger) Info(msg string) { - if strings.Contains(msg, "duplicated packet") { - return - } - - l.logger.Info().Msg(strings.TrimSpace(msg)) -} - -func (l logger) Infof(format string, args ...any) { - msg := fmt.Sprintf(format, args...) - if strings.Contains(msg, "duplicated packet") { - return - } - - l.logger.Info().Msg(strings.TrimSpace(msg)) -} - -func (l logger) Warn(msg string) { - l.logger.Warn().Msg(strings.TrimSpace(msg)) -} - -func (l logger) Warnf(format string, args ...any) { - msg := fmt.Sprintf(format, args...) - l.logger.Warn().Msg(strings.TrimSpace(msg)) -} - -func (l logger) Error(msg string) { - l.logger.Error().Msg(strings.TrimSpace(msg)) -} - -func (l logger) Errorf(format string, args ...any) { - msg := fmt.Sprintf(format, args...) - l.logger.Error().Msg(strings.TrimSpace(msg)) -} diff --git a/internal/webrtc/pionlog/nullog.go b/internal/webrtc/pionlog/nullog.go deleted file mode 100644 index 2d136d04..00000000 --- a/internal/webrtc/pionlog/nullog.go +++ /dev/null @@ -1,14 +0,0 @@ -package pionlog - -type nulllog struct{} - -func (l nulllog) Trace(msg string) {} -func (l nulllog) Tracef(format string, args ...any) {} -func (l nulllog) Debug(msg string) {} -func (l nulllog) Debugf(format string, args ...any) {} -func (l nulllog) Info(msg string) {} -func (l nulllog) Infof(format string, args ...any) {} -func (l nulllog) Warn(msg string) {} -func (l nulllog) Warnf(format string, args ...any) {} -func (l nulllog) Error(msg string) {} -func (l nulllog) Errorf(format string, args ...any) {} diff --git a/internal/websocket/handler/control.go b/internal/websocket/handler/control.go deleted file mode 100644 index e92add15..00000000 --- a/internal/websocket/handler/control.go +++ /dev/null @@ -1,222 +0,0 @@ -package handler - -import ( - "errors" - - "github.com/demodesk/neko/pkg/types" - "github.com/demodesk/neko/pkg/types/event" - "github.com/demodesk/neko/pkg/types/message" - "github.com/demodesk/neko/pkg/xorg" -) - -var ( - ErrIsNotAllowedToHost = errors.New("is not allowed to host") - ErrIsNotTheHost = errors.New("is not the host") - ErrIsAlreadyTheHost = errors.New("is already the host") - ErrIsAlreadyHosted = errors.New("is already hosted") -) - -func (h *MessageHandlerCtx) controlRelease(session types.Session) error { - if !session.Profile().CanHost || session.PrivateModeEnabled() { - return ErrIsNotAllowedToHost - } - - if !session.IsHost() { - return ErrIsNotTheHost - } - - h.desktop.ResetKeys() - h.sessions.ClearHost() - - return nil -} - -func (h *MessageHandlerCtx) controlRequest(session types.Session) error { - if !session.Profile().CanHost || session.PrivateModeEnabled() { - return ErrIsNotAllowedToHost - } - - if session.IsHost() { - return ErrIsAlreadyTheHost - } - - if h.sessions.Settings().LockedControls && !session.Profile().IsAdmin { - return ErrIsNotAllowedToHost - } - - if !h.sessions.Settings().ImplicitHosting { - // tell session if there is a host - if host, hasHost := h.sessions.GetHost(); hasHost { - session.Send( - event.CONTROL_HOST, - message.ControlHost{ - HasHost: true, - HostID: host.ID(), - }) - - return ErrIsAlreadyHosted - } - } - - h.sessions.SetHost(session) - - return nil -} - -func (h *MessageHandlerCtx) controlMove(session types.Session, payload *message.ControlPos) error { - if err := h.controlRequest(session); err != nil && !errors.Is(err, ErrIsAlreadyTheHost) { - return err - } - - // handle active cursor movement - h.desktop.Move(payload.X, payload.Y) - h.webrtc.SetCursorPosition(payload.X, payload.Y) - return nil -} - -func (h *MessageHandlerCtx) controlScroll(session types.Session, payload *message.ControlScroll) error { - if err := h.controlRequest(session); err != nil && !errors.Is(err, ErrIsAlreadyTheHost) { - return err - } - - // TOOD: remove this once the client is fixed - if payload.DeltaX == 0 && payload.DeltaY == 0 { - payload.DeltaX = payload.X - payload.DeltaY = payload.Y - } - - h.desktop.Scroll(payload.DeltaX, payload.DeltaY, payload.ControlKey) - return nil -} - -func (h *MessageHandlerCtx) controlButtonPress(session types.Session, payload *message.ControlButton) error { - if payload.ControlPos != nil { - if err := h.controlMove(session, payload.ControlPos); err != nil { - return err - } - } else if err := h.controlRequest(session); err != nil && !errors.Is(err, ErrIsAlreadyTheHost) { - return err - } - - return h.desktop.ButtonPress(payload.Code) -} - -func (h *MessageHandlerCtx) controlButtonDown(session types.Session, payload *message.ControlButton) error { - if payload.ControlPos != nil { - if err := h.controlMove(session, payload.ControlPos); err != nil { - return err - } - } else if err := h.controlRequest(session); err != nil && !errors.Is(err, ErrIsAlreadyTheHost) { - return err - } - - return h.desktop.ButtonDown(payload.Code) -} - -func (h *MessageHandlerCtx) controlButtonUp(session types.Session, payload *message.ControlButton) error { - if payload.ControlPos != nil { - if err := h.controlMove(session, payload.ControlPos); err != nil { - return err - } - } else if err := h.controlRequest(session); err != nil && !errors.Is(err, ErrIsAlreadyTheHost) { - return err - } - - return h.desktop.ButtonUp(payload.Code) -} - -func (h *MessageHandlerCtx) controlKeyPress(session types.Session, payload *message.ControlKey) error { - if payload.ControlPos != nil { - if err := h.controlMove(session, payload.ControlPos); err != nil { - return err - } - } else if err := h.controlRequest(session); err != nil && !errors.Is(err, ErrIsAlreadyTheHost) { - return err - } - - return h.desktop.KeyPress(payload.Keysym) -} - -func (h *MessageHandlerCtx) controlKeyDown(session types.Session, payload *message.ControlKey) error { - if payload.ControlPos != nil { - if err := h.controlMove(session, payload.ControlPos); err != nil { - return err - } - } else if err := h.controlRequest(session); err != nil && !errors.Is(err, ErrIsAlreadyTheHost) { - return err - } - - return h.desktop.KeyDown(payload.Keysym) -} - -func (h *MessageHandlerCtx) controlKeyUp(session types.Session, payload *message.ControlKey) error { - if payload.ControlPos != nil { - if err := h.controlMove(session, payload.ControlPos); err != nil { - return err - } - } else if err := h.controlRequest(session); err != nil && !errors.Is(err, ErrIsAlreadyTheHost) { - return err - } - - return h.desktop.KeyUp(payload.Keysym) -} - -func (h *MessageHandlerCtx) controlTouchBegin(session types.Session, payload *message.ControlTouch) error { - if err := h.controlRequest(session); err != nil && !errors.Is(err, ErrIsAlreadyTheHost) { - return err - } - return h.desktop.TouchBegin(payload.TouchId, payload.X, payload.Y, payload.Pressure) -} - -func (h *MessageHandlerCtx) controlTouchUpdate(session types.Session, payload *message.ControlTouch) error { - if err := h.controlRequest(session); err != nil && !errors.Is(err, ErrIsAlreadyTheHost) { - return err - } - return h.desktop.TouchUpdate(payload.TouchId, payload.X, payload.Y, payload.Pressure) -} - -func (h *MessageHandlerCtx) controlTouchEnd(session types.Session, payload *message.ControlTouch) error { - if err := h.controlRequest(session); err != nil && !errors.Is(err, ErrIsAlreadyTheHost) { - return err - } - return h.desktop.TouchEnd(payload.TouchId, payload.X, payload.Y, payload.Pressure) -} - -func (h *MessageHandlerCtx) controlCut(session types.Session) error { - if err := h.controlRequest(session); err != nil && !errors.Is(err, ErrIsAlreadyTheHost) { - return err - } - - return h.desktop.KeyPress(xorg.XK_Control_L, xorg.XK_x) -} - -func (h *MessageHandlerCtx) controlCopy(session types.Session) error { - if err := h.controlRequest(session); err != nil && !errors.Is(err, ErrIsAlreadyTheHost) { - return err - } - - return h.desktop.KeyPress(xorg.XK_Control_L, xorg.XK_c) -} - -func (h *MessageHandlerCtx) controlPaste(session types.Session, payload *message.ClipboardData) error { - if err := h.controlRequest(session); err != nil && !errors.Is(err, ErrIsAlreadyTheHost) { - return err - } - - // if there have been set clipboard data, set them first - if payload != nil && payload.Text != "" { - if err := h.clipboardSet(session, payload); err != nil { - return err - } - } - - return h.desktop.KeyPress(xorg.XK_Control_L, xorg.XK_v) -} - -func (h *MessageHandlerCtx) controlSelectAll(session types.Session) error { - if err := h.controlRequest(session); err != nil && !errors.Is(err, ErrIsAlreadyTheHost) { - return err - } - - return h.desktop.KeyPress(xorg.XK_Control_L, xorg.XK_a) -} diff --git a/internal/websocket/handler/handler.go b/internal/websocket/handler/handler.go deleted file mode 100644 index 43ea2a17..00000000 --- a/internal/websocket/handler/handler.go +++ /dev/null @@ -1,203 +0,0 @@ -package handler - -import ( - "github.com/rs/zerolog" - "github.com/rs/zerolog/log" - - "github.com/demodesk/neko/pkg/types" - "github.com/demodesk/neko/pkg/types/event" - "github.com/demodesk/neko/pkg/types/message" - "github.com/demodesk/neko/pkg/utils" -) - -func New( - sessions types.SessionManager, - desktop types.DesktopManager, - capture types.CaptureManager, - webrtc types.WebRTCManager, -) *MessageHandlerCtx { - return &MessageHandlerCtx{ - logger: log.With().Str("module", "websocket").Str("submodule", "handler").Logger(), - sessions: sessions, - desktop: desktop, - capture: capture, - webrtc: webrtc, - } -} - -type MessageHandlerCtx struct { - logger zerolog.Logger - sessions types.SessionManager - webrtc types.WebRTCManager - desktop types.DesktopManager - capture types.CaptureManager -} - -func (h *MessageHandlerCtx) Message(session types.Session, data types.WebSocketMessage) bool { - var err error - switch data.Event { - // System Events - case event.SYSTEM_LOGS: - payload := &message.SystemLogs{} - err = utils.Unmarshal(payload, data.Payload, func() error { - return h.systemLogs(session, payload) - }) - - // Signal Events - case event.SIGNAL_REQUEST: - payload := &message.SignalRequest{} - err = utils.Unmarshal(payload, data.Payload, func() error { - return h.signalRequest(session, payload) - }) - case event.SIGNAL_RESTART: - err = h.signalRestart(session) - case event.SIGNAL_OFFER: - payload := &message.SignalDescription{} - err = utils.Unmarshal(payload, data.Payload, func() error { - return h.signalOffer(session, payload) - }) - case event.SIGNAL_ANSWER: - payload := &message.SignalDescription{} - err = utils.Unmarshal(payload, data.Payload, func() error { - return h.signalAnswer(session, payload) - }) - case event.SIGNAL_CANDIDATE: - payload := &message.SignalCandidate{} - err = utils.Unmarshal(payload, data.Payload, func() error { - return h.signalCandidate(session, payload) - }) - case event.SIGNAL_VIDEO: - payload := &message.SignalVideo{} - err = utils.Unmarshal(payload, data.Payload, func() error { - return h.signalVideo(session, payload) - }) - case event.SIGNAL_AUDIO: - payload := &message.SignalAudio{} - err = utils.Unmarshal(payload, data.Payload, func() error { - return h.signalAudio(session, payload) - }) - - // Control Events - case event.CONTROL_RELEASE: - err = h.controlRelease(session) - case event.CONTROL_REQUEST: - err = h.controlRequest(session) - case event.CONTROL_MOVE: - payload := &message.ControlPos{} - err = utils.Unmarshal(payload, data.Payload, func() error { - return h.controlMove(session, payload) - }) - case event.CONTROL_SCROLL: - payload := &message.ControlScroll{} - err = utils.Unmarshal(payload, data.Payload, func() error { - return h.controlScroll(session, payload) - }) - case event.CONTROL_BUTTONPRESS: - payload := &message.ControlButton{} - err = utils.Unmarshal(payload, data.Payload, func() error { - return h.controlButtonPress(session, payload) - }) - case event.CONTROL_BUTTONDOWN: - payload := &message.ControlButton{} - err = utils.Unmarshal(payload, data.Payload, func() error { - return h.controlButtonDown(session, payload) - }) - case event.CONTROL_BUTTONUP: - payload := &message.ControlButton{} - err = utils.Unmarshal(payload, data.Payload, func() error { - return h.controlButtonUp(session, payload) - }) - case event.CONTROL_KEYPRESS: - payload := &message.ControlKey{} - err = utils.Unmarshal(payload, data.Payload, func() error { - return h.controlKeyPress(session, payload) - }) - case event.CONTROL_KEYDOWN: - payload := &message.ControlKey{} - err = utils.Unmarshal(payload, data.Payload, func() error { - return h.controlKeyDown(session, payload) - }) - case event.CONTROL_KEYUP: - payload := &message.ControlKey{} - err = utils.Unmarshal(payload, data.Payload, func() error { - return h.controlKeyUp(session, payload) - }) - // touch - case event.CONTROL_TOUCHBEGIN: - payload := &message.ControlTouch{} - err = utils.Unmarshal(payload, data.Payload, func() error { - return h.controlTouchBegin(session, payload) - }) - case event.CONTROL_TOUCHUPDATE: - payload := &message.ControlTouch{} - err = utils.Unmarshal(payload, data.Payload, func() error { - return h.controlTouchUpdate(session, payload) - }) - case event.CONTROL_TOUCHEND: - payload := &message.ControlTouch{} - err = utils.Unmarshal(payload, data.Payload, func() error { - return h.controlTouchEnd(session, payload) - }) - // actions - case event.CONTROL_CUT: - err = h.controlCut(session) - case event.CONTROL_COPY: - err = h.controlCopy(session) - case event.CONTROL_PASTE: - payload := &message.ClipboardData{} - err = utils.Unmarshal(payload, data.Payload, func() error { - return h.controlPaste(session, payload) - }) - case event.CONTROL_SELECT_ALL: - err = h.controlSelectAll(session) - - // Screen Events - case event.SCREEN_SET: - payload := &message.ScreenSize{} - err = utils.Unmarshal(payload, data.Payload, func() error { - return h.screenSet(session, payload) - }) - - // Clipboard Events - case event.CLIPBOARD_SET: - payload := &message.ClipboardData{} - err = utils.Unmarshal(payload, data.Payload, func() error { - return h.clipboardSet(session, payload) - }) - - // Keyboard Events - case event.KEYBOARD_MAP: - payload := &message.KeyboardMap{} - err = utils.Unmarshal(payload, data.Payload, func() error { - return h.keyboardMap(session, payload) - }) - case event.KEYBOARD_MODIFIERS: - payload := &message.KeyboardModifiers{} - err = utils.Unmarshal(payload, data.Payload, func() error { - return h.keyboardModifiers(session, payload) - }) - - // Send Events - case event.SEND_UNICAST: - payload := &message.SendUnicast{} - err = utils.Unmarshal(payload, data.Payload, func() error { - return h.sendUnicast(session, payload) - }) - case event.SEND_BROADCAST: - payload := &message.SendBroadcast{} - err = utils.Unmarshal(payload, data.Payload, func() error { - return h.sendBroadcast(session, payload) - }) - default: - return false - } - - if err != nil { - h.logger.Warn().Err(err). - Str("event", data.Event). - Str("session_id", session.ID()). - Msg("message handler has failed") - } - - return true -} diff --git a/internal/websocket/handler/screen.go b/internal/websocket/handler/screen.go deleted file mode 100644 index 8d88ac9d..00000000 --- a/internal/websocket/handler/screen.go +++ /dev/null @@ -1,32 +0,0 @@ -package handler - -import ( - "errors" - - "github.com/demodesk/neko/pkg/types" - "github.com/demodesk/neko/pkg/types/event" - "github.com/demodesk/neko/pkg/types/message" -) - -func (h *MessageHandlerCtx) screenSet(session types.Session, payload *message.ScreenSize) error { - if !session.Profile().IsAdmin { - return errors.New("is not the admin") - } - - size, err := h.desktop.SetScreenSize(types.ScreenSize{ - Width: payload.Width, - Height: payload.Height, - Rate: payload.Rate, - }) - - if err != nil { - return err - } - - h.sessions.Broadcast(event.SCREEN_UPDATED, message.ScreenSize{ - Width: size.Width, - Height: size.Height, - Rate: size.Rate, - }) - return nil -} diff --git a/internal/websocket/handler/session.go b/internal/websocket/handler/session.go deleted file mode 100644 index 8d621026..00000000 --- a/internal/websocket/handler/session.go +++ /dev/null @@ -1,75 +0,0 @@ -package handler - -import ( - "github.com/demodesk/neko/pkg/types" - "github.com/demodesk/neko/pkg/types/event" - "github.com/demodesk/neko/pkg/types/message" -) - -func (h *MessageHandlerCtx) SessionCreated(session types.Session) error { - h.sessions.Broadcast( - event.SESSION_CREATED, - message.SessionData{ - ID: session.ID(), - Profile: session.Profile(), - State: session.State(), - }) - - return nil -} - -func (h *MessageHandlerCtx) SessionDeleted(session types.Session) error { - h.sessions.Broadcast( - event.SESSION_DELETED, - message.SessionID{ - ID: session.ID(), - }) - - return nil -} - -func (h *MessageHandlerCtx) SessionConnected(session types.Session) error { - if err := h.systemInit(session); err != nil { - return err - } - - if session.Profile().IsAdmin { - if err := h.systemAdmin(session); err != nil { - return err - } - } - - return h.SessionStateChanged(session) -} - -func (h *MessageHandlerCtx) SessionDisconnected(session types.Session) error { - // clear host if exists - if session.IsHost() { - h.desktop.ResetKeys() - h.sessions.ClearHost() - } - - return h.SessionStateChanged(session) -} - -func (h *MessageHandlerCtx) SessionProfileChanged(session types.Session) error { - h.sessions.Broadcast( - event.SESSION_PROFILE, - message.MemberProfile{ - ID: session.ID(), - MemberProfile: session.Profile(), - }) - - return nil -} - -func (h *MessageHandlerCtx) SessionStateChanged(session types.Session) error { - h.sessions.Broadcast( - event.SESSION_STATE, - message.SessionState{ - ID: session.ID(), - SessionState: session.State(), - }) - - return nil -} diff --git a/internal/websocket/handler/signal.go b/internal/websocket/handler/signal.go deleted file mode 100644 index e0e5fb88..00000000 --- a/internal/websocket/handler/signal.go +++ /dev/null @@ -1,162 +0,0 @@ -package handler - -import ( - "errors" - - "github.com/demodesk/neko/pkg/types" - "github.com/demodesk/neko/pkg/types/event" - "github.com/demodesk/neko/pkg/types/message" - "github.com/pion/webrtc/v3" -) - -func (h *MessageHandlerCtx) signalRequest(session types.Session, payload *message.SignalRequest) error { - if !session.Profile().CanWatch { - return errors.New("not allowed to watch") - } - - offer, peer, err := h.webrtc.CreatePeer(session) - if err != nil { - return err - } - - // set webrtc as paused if session has private mode enabled - if session.PrivateModeEnabled() { - peer.SetPaused(true) - } - - video := payload.Video - - // use default first video, if not provided - if video.Selector == nil { - videos := h.capture.Video().IDs() - video.Selector = &types.StreamSelector{ - ID: videos[0], - Type: types.StreamSelectorTypeExact, - } - } - - // TODO: Remove, used for compatibility with old clients. - if video.Auto == nil { - video.Auto = &payload.Auto - } - - // set video stream - err = peer.SetVideo(video) - if err != nil { - return err - } - - audio := payload.Audio - - // enable by default if not requested otherwise - if audio.Disabled == nil { - disabled := false - audio.Disabled = &disabled - } - - // set audio stream - err = peer.SetAudio(audio) - if err != nil { - return err - } - - session.Send( - event.SIGNAL_PROVIDE, - message.SignalProvide{ - SDP: offer.SDP, - ICEServers: h.webrtc.ICEServers(), - - Video: peer.Video(), - Audio: peer.Audio(), - }) - - return nil -} - -func (h *MessageHandlerCtx) signalRestart(session types.Session) error { - peer := session.GetWebRTCPeer() - if peer == nil { - return errors.New("webRTC peer does not exist") - } - - offer, err := peer.CreateOffer(true) - if err != nil { - return err - } - - // TODO: Use offer event instead. - session.Send( - event.SIGNAL_RESTART, - message.SignalDescription{ - SDP: offer.SDP, - }) - - return nil -} - -func (h *MessageHandlerCtx) signalOffer(session types.Session, payload *message.SignalDescription) error { - peer := session.GetWebRTCPeer() - if peer == nil { - return errors.New("webRTC peer does not exist") - } - - err := peer.SetRemoteDescription(webrtc.SessionDescription{ - SDP: payload.SDP, - Type: webrtc.SDPTypeOffer, - }) - if err != nil { - return err - } - - answer, err := peer.CreateAnswer() - if err != nil { - return err - } - - session.Send( - event.SIGNAL_ANSWER, - message.SignalDescription{ - SDP: answer.SDP, - }) - - return nil -} - -func (h *MessageHandlerCtx) signalAnswer(session types.Session, payload *message.SignalDescription) error { - peer := session.GetWebRTCPeer() - if peer == nil { - return errors.New("webRTC peer does not exist") - } - - return peer.SetRemoteDescription(webrtc.SessionDescription{ - SDP: payload.SDP, - Type: webrtc.SDPTypeAnswer, - }) -} - -func (h *MessageHandlerCtx) signalCandidate(session types.Session, payload *message.SignalCandidate) error { - peer := session.GetWebRTCPeer() - if peer == nil { - return errors.New("webRTC peer does not exist") - } - - return peer.SetCandidate(payload.ICECandidateInit) -} - -func (h *MessageHandlerCtx) signalVideo(session types.Session, payload *message.SignalVideo) error { - peer := session.GetWebRTCPeer() - if peer == nil { - return errors.New("webRTC peer does not exist") - } - - return peer.SetVideo(payload.PeerVideoRequest) -} - -func (h *MessageHandlerCtx) signalAudio(session types.Session, payload *message.SignalAudio) error { - peer := session.GetWebRTCPeer() - if peer == nil { - return errors.New("webRTC peer does not exist") - } - - return peer.SetAudio(payload.PeerAudioRequest) -} diff --git a/neko.go b/neko.go deleted file mode 100644 index d25ee6a7..00000000 --- a/neko.go +++ /dev/null @@ -1,69 +0,0 @@ -package neko - -import ( - "fmt" - "runtime" - "strings" -) - -const Header = `&34 - _ __ __ - / | / /__ / /______ \ /\ - / |/ / _ \/ //_/ __ \ ) ( ') - / /| / __/ ,< / /_/ / ( / ) -/_/ |_/\___/_/|_|\____/ \(__)| -&1&37 nurdism/m1k1o &33%s %s&0 -` - -var ( - // - buildDate = "dev" - // - gitCommit = "dev" - // - gitBranch = "dev" - // - gitTag = "dev" -) - -var Version = &version{ - GitCommit: gitCommit, - GitBranch: gitBranch, - GitTag: gitTag, - BuildDate: buildDate, - GoVersion: runtime.Version(), - Compiler: runtime.Compiler, - Platform: fmt.Sprintf("%s/%s", runtime.GOOS, runtime.GOARCH), -} - -type version struct { - GitCommit string - GitBranch string - GitTag string - BuildDate string - GoVersion string - Compiler string - Platform string -} - -func (i *version) String() string { - version := i.GitTag - if version == "" || version == "dev" { - version = i.GitBranch - } - - return fmt.Sprintf("%s@%s", version, i.GitCommit) -} - -func (i *version) Details() string { - return "\n" + strings.Join([]string{ - fmt.Sprintf("Version %s", i.String()), - fmt.Sprintf("GitCommit %s", i.GitCommit), - fmt.Sprintf("GitBranch %s", i.GitBranch), - fmt.Sprintf("GitTag %s", i.GitTag), - fmt.Sprintf("BuildDate %s", i.BuildDate), - fmt.Sprintf("GoVersion %s", i.GoVersion), - fmt.Sprintf("Compiler %s", i.Compiler), - fmt.Sprintf("Platform %s", i.Platform), - }, "\n") + "\n" -} diff --git a/server/.editorconfig b/server/.editorconfig deleted file mode 100644 index 3dce4145..00000000 --- a/server/.editorconfig +++ /dev/null @@ -1,9 +0,0 @@ -root = true - -[*] -charset = utf-8 -indent_style = space -indent_size = 2 -end_of_line = lf -insert_final_newline = true -trim_trailing_whitespace = true \ No newline at end of file diff --git a/server/.env.development b/server/.env.development deleted file mode 100644 index b5884926..00000000 --- a/server/.env.development +++ /dev/null @@ -1,2 +0,0 @@ -DISPLAY=:99.0 -PION_LOG_TRACE=all diff --git a/server/.gitignore b/server/.gitignore new file mode 100644 index 00000000..6784a149 --- /dev/null +++ b/server/.gitignore @@ -0,0 +1,8 @@ +runtime/fonts/* +!runtime/fonts/.gitkeep + +runtime/icon-theme/* +!runtime/icon-theme/.gitkeep + +plugins/* +!plugins/.gitkeep diff --git a/server/.vscode/launch.json b/server/.vscode/launch.json deleted file mode 100644 index 02c7b7db..00000000 --- a/server/.vscode/launch.json +++ /dev/null @@ -1,16 +0,0 @@ -{ - "version": "0.2.0", - "configurations": [ - { - "name": "launch", - "type": "go", - "request": "launch", - "mode": "debug", - "program": "${workspaceFolder}/cmd/neko", - "envFile": "${workspaceFolder}/.env.development", - "output": "${workspaceFolder}/bin/debug/neko", - "cwd": "${workspaceFolder}/", - "args": ["serve", "-d", "--bind", ":3000", "--static", "../client/dist", "--password", "neko", "--password_admin", "admin"] - } - ] -} diff --git a/server/.vscode/settings.json b/server/.vscode/settings.json deleted file mode 100644 index 452ccec1..00000000 --- a/server/.vscode/settings.json +++ /dev/null @@ -1,22 +0,0 @@ - -{ - "go.formatTool": "goformat", - "go.inferGopath": false, - "go.autocompleteUnimportedPackages": true, - "go.delveConfig": { - "useApiV1": false, - "dlvLoadConfig": { - "followPointers": true, - "maxVariableRecurse": 3, - "maxStringLen": 400, - "maxArrayValues": 400, - "maxStructFields": -1 - } - }, - "[go]": { - "editor.formatOnSave": true, - "editor.codeActionsOnSave": { - "source.organizeImports": true - } - } - } diff --git a/Dockerfile b/server/Dockerfile similarity index 100% rename from Dockerfile rename to server/Dockerfile diff --git a/Dockerfile.bookworm b/server/Dockerfile.bookworm similarity index 100% rename from Dockerfile.bookworm rename to server/Dockerfile.bookworm diff --git a/Dockerfile.nvidia b/server/Dockerfile.nvidia similarity index 100% rename from Dockerfile.nvidia rename to server/Dockerfile.nvidia diff --git a/Dockerfile.nvidia.bookworm b/server/Dockerfile.nvidia.bookworm similarity index 100% rename from Dockerfile.nvidia.bookworm rename to server/Dockerfile.nvidia.bookworm diff --git a/server/build b/server/build index 6c3a2543..ee092a8c 100755 --- a/server/build +++ b/server/build @@ -4,7 +4,12 @@ # aborting if any command returns a non-zero value set -e -BUILD_TIME=`date -u +'%Y-%m-%dT%H:%M:%SZ'` +# +# do not build plugins when passing "core" as first argument +if [ "$1" = "core" ]; +then + skip_plugins="true" +fi # # set git build variables if git exists @@ -13,12 +18,10 @@ then GIT_COMMIT=`git rev-parse --short HEAD` GIT_BRANCH=`git rev-parse --symbolic-full-name --abbrev-ref HEAD` GIT_TAG=`git tag --points-at $GIT_COMMIT | head -n 1` - GIT_DIRTY=`git diff-index --quiet HEAD -- || echo "✗-"` - GIT_COMMIT="${GIT_DIRTY}${GIT_COMMIT}" fi # -# load dependencies +# load server dependencies go get -v -t -d . # @@ -27,9 +30,58 @@ go build \ -o bin/neko \ -ldflags " -s -w - -X 'm1k1o/neko.buildDate=${BUILD_TIME}' - -X 'm1k1o/neko.gitCommit=${GIT_COMMIT}' - -X 'm1k1o/neko.gitBranch=${GIT_BRANCH}' - -X 'm1k1o/neko.gitTag=${GIT_TAG}' + -X 'github.com/demodesk/neko.buildDate=`date -u +'%Y-%m-%dT%H:%M:%SZ'`' + -X 'github.com/demodesk/neko.gitCommit=${GIT_COMMIT}' + -X 'github.com/demodesk/neko.gitBranch=${GIT_BRANCH}' + -X 'github.com/demodesk/neko.gitTag=${GIT_TAG}' " \ cmd/neko/main.go; + +# +# ensure plugins folder exists +mkdir -p bin/plugins + +# +# if plugins are ignored +if [ "$skip_plugins" = "true" ]; +then + echo "Not building plugins..." + exit 0 +fi + +# +# if plugins directory does not exist +if [ ! -d "./plugins" ]; +then + echo "No plugins directory found, skipping..." + exit 0 +fi + +# +# remove old plugins +rm -f bin/plugins/* + +# +# build plugins +for plugPath in ./plugins/*; do + if [ ! -d $plugPath ]; + then + continue + fi + + pushd $plugPath + + echo "Building plugin: $plugPath" + + if [ ! -f "go.plug.mod" ]; + then + echo "go.plug.mod not found, skipping..." + popd + continue + fi + + # build plugin + go build -modfile=go.plug.mod -buildmode=plugin -buildvcs=false -o "../../bin/plugins/${plugPath##*/}.so" + + popd +done diff --git a/server/cmd/neko/main.go b/server/cmd/neko/main.go index a1a2206f..dec9df74 100644 --- a/server/cmd/neko/main.go +++ b/server/cmd/neko/main.go @@ -5,13 +5,13 @@ import ( "github.com/rs/zerolog/log" - "m1k1o/neko" - "m1k1o/neko/cmd" - "m1k1o/neko/internal/utils" + "github.com/demodesk/neko" + "github.com/demodesk/neko/cmd" + "github.com/demodesk/neko/pkg/utils" ) func main() { - fmt.Print(utils.Colorf(neko.Header, "server", neko.Service.Version)) + fmt.Print(utils.Colorf(neko.Header, "server", neko.Version)) if err := cmd.Execute(); err != nil { log.Panic().Err(err).Msg("failed to execute command") } diff --git a/cmd/plugins.go b/server/cmd/plugins.go similarity index 100% rename from cmd/plugins.go rename to server/cmd/plugins.go diff --git a/server/cmd/root.go b/server/cmd/root.go index d402f3ce..fb5b5d0e 100644 --- a/server/cmd/root.go +++ b/server/cmd/root.go @@ -6,6 +6,7 @@ import ( "os" "path/filepath" "runtime" + "strings" "time" "github.com/rs/zerolog" @@ -14,10 +15,19 @@ import ( "github.com/spf13/cobra" "github.com/spf13/viper" - "m1k1o/neko" + "github.com/demodesk/neko" + "github.com/demodesk/neko/internal/config" ) func Execute() error { + // properly log unhandled panics + defer func() { + panicVal := recover() + if panicVal != nil { + log.Panic().Msgf("%v", panicVal) + } + }() + return root.Execute() } @@ -25,58 +35,24 @@ var root = &cobra.Command{ Use: "neko", Short: "neko streaming server", Long: `neko streaming server`, - Version: neko.Service.Version.String(), + Version: neko.Version.String(), } func init() { + rootConfig := config.Root{} + cobra.OnInitialize(func() { - ////// - // logs - ////// - zerolog.TimeFieldFormat = "" - zerolog.SetGlobalLevel(zerolog.InfoLevel) - - console := zerolog.ConsoleWriter{Out: os.Stdout} - - if !viper.GetBool("logs") { - log.Logger = log.Output(console) - } else { - logs := filepath.Join(".", "logs") - if runtime.GOOS == "linux" { - logs = "/var/log/neko" - } - - if _, err := os.Stat(logs); os.IsNotExist(err) { - _ = os.Mkdir(logs, os.ModePerm) - } - - latest := filepath.Join(logs, "neko-latest.log") - _, err := os.Stat(latest) - if err == nil { - err = os.Rename(latest, filepath.Join(logs, "neko."+time.Now().Format("2006-01-02T15-04-05Z07-00")+".log")) - if err != nil { - log.Panic().Err(err).Msg("failed to rotate log file") - } - } - - logf, err := os.OpenFile(latest, os.O_RDWR|os.O_CREATE, 0666) - if err != nil { - log.Panic().Err(err).Msg("failed to create log file") - } - - logger := diode.NewWriter(logf, 1000, 10*time.Millisecond, func(missed int) { - fmt.Printf("logger dropped %d messages", missed) - }) - - log.Logger = log.Output(io.MultiWriter(console, logger)) - } - ////// // configs ////// - config := viper.GetString("config") + + config := viper.GetString("config") // Use config file from the flag. + if config == "" { + config = os.Getenv("NEKO_CONFIG") // Use config file from the environment variable. + } + if config != "" { - viper.SetConfigFile(config) // Use config file from the flag. + viper.SetConfigFile(config) } else { if runtime.GOOS == "linux" { viper.AddConfigPath("/etc/neko/") @@ -87,41 +63,103 @@ func init() { } viper.SetEnvPrefix("NEKO") + viper.SetEnvKeyReplacer(strings.NewReplacer(".", "_")) viper.AutomaticEnv() // read in environment variables that match - if err := viper.ReadInConfig(); err != nil { - if _, ok := err.(viper.ConfigFileNotFoundError); !ok { - log.Error().Err(err) - } - if config != "" { - log.Error().Err(err) + // read config values + err := viper.ReadInConfig() + if err != nil { + _, notFound := err.(viper.ConfigFileNotFoundError) + if !notFound { + log.Fatal().Err(err).Msg("unable to read config file") } } - debug := viper.GetBool("debug") - if debug { - zerolog.SetGlobalLevel(zerolog.DebugLevel) + // get full config file path + config = viper.ConfigFileUsed() + + // set root config values + rootConfig.Set() + + ////// + // logs + ////// + var logWriter io.Writer + + // log to a directory instead of stderr + if rootConfig.LogDir != "" { + if _, err := os.Stat(rootConfig.LogDir); os.IsNotExist(err) { + _ = os.Mkdir(rootConfig.LogDir, os.ModePerm) + } + + latest := filepath.Join(rootConfig.LogDir, "neko-latest.log") + if _, err := os.Stat(latest); err == nil { + err = os.Rename(latest, filepath.Join(rootConfig.LogDir, "neko."+time.Now().Format("2006-01-02T15-04-05Z07-00")+".log")) + if err != nil { + log.Fatal().Err(err).Msg("failed to rotate log file") + } + } + + logf, err := os.OpenFile(latest, os.O_RDWR|os.O_CREATE, 0666) + if err != nil { + log.Fatal().Err(err).Msg("failed to open log file") + } + + logWriter = diode.NewWriter(logf, 1000, 10*time.Millisecond, func(missed int) { + fmt.Printf("logger dropped %d messages", missed) + }) + } else { + logWriter = os.Stderr + } + + // log console output instead of json + if !rootConfig.LogJson { + logWriter = zerolog.ConsoleWriter{ + Out: logWriter, + NoColor: rootConfig.LogNocolor, + } + } + + // save new logger output + log.Logger = log.Output(logWriter) + + // set custom log level + if rootConfig.LogLevel != zerolog.NoLevel { + zerolog.SetGlobalLevel(rootConfig.LogLevel) + } + + // set custom log tiem format + if rootConfig.LogTime != "" { + zerolog.TimeFieldFormat = rootConfig.LogTime + } + + timeFormat := rootConfig.LogTime + if rootConfig.LogTime == zerolog.TimeFormatUnix { + timeFormat = "UNIX" } - file := viper.ConfigFileUsed() logger := log.With(). - Bool("debug", debug). - Str("logging", viper.GetString("logs")). - Str("config", file). + Str("config", config). + Str("log-level", zerolog.GlobalLevel().String()). + Bool("log-json", rootConfig.LogJson). + Str("log-time", timeFormat). + Str("log-dir", rootConfig.LogDir). Logger() - if file == "" { + if config == "" { logger.Warn().Msg("preflight complete without config file") } else { - logger.Info().Msg("preflight complete") + if _, err := os.Stat(config); os.IsNotExist(err) { + logger.Err(err).Msg("preflight complete with nonexistent config file") + } else { + logger.Info().Msg("preflight complete with config file") + } } - - neko.Service.Root.Set() }) - if err := neko.Service.Root.Init(root); err != nil { + if err := rootConfig.Init(root); err != nil { log.Panic().Err(err).Msg("unable to run root command") } - root.SetVersionTemplate(neko.Service.Version.Details()) + root.SetVersionTemplate(neko.Version.Details()) } diff --git a/server/cmd/serve.go b/server/cmd/serve.go index d8c4f605..13f5de23 100644 --- a/server/cmd/serve.go +++ b/server/cmd/serve.go @@ -1,41 +1,240 @@ package cmd import ( + "os" + "os/signal" + + "github.com/rs/zerolog" "github.com/rs/zerolog/log" "github.com/spf13/cobra" - "m1k1o/neko" - "m1k1o/neko/internal/config" + "github.com/demodesk/neko/internal/api" + "github.com/demodesk/neko/internal/capture" + "github.com/demodesk/neko/internal/config" + "github.com/demodesk/neko/internal/desktop" + "github.com/demodesk/neko/internal/http" + "github.com/demodesk/neko/internal/member" + "github.com/demodesk/neko/internal/plugins" + "github.com/demodesk/neko/internal/session" + "github.com/demodesk/neko/internal/webrtc" + "github.com/demodesk/neko/internal/websocket" ) func init() { + service := serve{} + command := &cobra.Command{ - Use: "serve", - Short: "serve neko streaming server", - Long: `serve neko streaming server`, - Run: neko.Service.ServeCommand, + Use: "serve", + Short: "serve neko streaming server", + Long: `serve neko streaming server`, + PreRun: service.PreRun, + Run: service.Run, } - configs := []config.Config{ - neko.Service.Server, - neko.Service.WebRTC, - neko.Service.Capture, - neko.Service.Desktop, - neko.Service.WebSocket, - } - - cobra.OnInitialize(func() { - for _, cfg := range configs { - cfg.Set() - } - neko.Service.Preflight() - }) - - for _, cfg := range configs { - if err := cfg.Init(command); err != nil { - log.Panic().Err(err).Msg("unable to run serve command") - } + if err := service.Init(command); err != nil { + log.Panic().Err(err).Msg("unable to initialize configuration") } root.AddCommand(command) } + +type serve struct { + logger zerolog.Logger + + configs struct { + Desktop config.Desktop + Capture config.Capture + WebRTC config.WebRTC + Member config.Member + Session config.Session + Plugins config.Plugins + Server config.Server + } + + managers struct { + desktop *desktop.DesktopManagerCtx + capture *capture.CaptureManagerCtx + webRTC *webrtc.WebRTCManagerCtx + member *member.MemberManagerCtx + session *session.SessionManagerCtx + webSocket *websocket.WebSocketManagerCtx + plugins *plugins.ManagerCtx + api *api.ApiManagerCtx + http *http.HttpManagerCtx + } +} + +func (c *serve) Init(cmd *cobra.Command) error { + if err := c.configs.Desktop.Init(cmd); err != nil { + return err + } + if err := c.configs.Capture.Init(cmd); err != nil { + return err + } + if err := c.configs.WebRTC.Init(cmd); err != nil { + return err + } + if err := c.configs.Member.Init(cmd); err != nil { + return err + } + if err := c.configs.Session.Init(cmd); err != nil { + return err + } + if err := c.configs.Plugins.Init(cmd); err != nil { + return err + } + if err := c.configs.Server.Init(cmd); err != nil { + return err + } + + // V2 configuration + + 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 +} + +func (c *serve) PreRun(cmd *cobra.Command, args []string) { + c.logger = log.With().Str("service", "neko").Logger() + + c.configs.Desktop.Set() + c.configs.Capture.Set() + c.configs.WebRTC.Set() + c.configs.Member.Set() + c.configs.Session.Set() + c.configs.Plugins.Set() + c.configs.Server.Set() + + 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) { + c.managers.session = session.New( + &c.configs.Session, + ) + + c.managers.member = member.New( + c.managers.session, + &c.configs.Member, + ) + + if err := c.managers.member.Connect(); err != nil { + c.logger.Panic().Err(err).Msg("unable to connect to member manager") + } + + c.managers.desktop = desktop.New( + &c.configs.Desktop, + ) + c.managers.desktop.Start() + + c.managers.capture = capture.New( + c.managers.desktop, + &c.configs.Capture, + ) + c.managers.capture.Start() + + c.managers.webRTC = webrtc.New( + c.managers.desktop, + c.managers.capture, + &c.configs.WebRTC, + ) + c.managers.webRTC.Start() + + c.managers.webSocket = websocket.New( + c.managers.session, + c.managers.desktop, + c.managers.capture, + c.managers.webRTC, + ) + c.managers.webSocket.Start() + + c.managers.api = api.New( + c.managers.session, + c.managers.member, + c.managers.desktop, + c.managers.capture, + ) + + c.managers.plugins = plugins.New( + &c.configs.Plugins, + ) + + // init and set configuration now + // this means it won't be in --help + c.managers.plugins.InitConfigs(cmd) + c.managers.plugins.SetConfigs() + + c.managers.plugins.Start( + c.managers.session, + c.managers.webSocket, + c.managers.api, + ) + + c.managers.http = http.New( + c.managers.webSocket, + c.managers.api, + &c.configs.Server, + ) + c.managers.http.Start() +} + +func (c *serve) Shutdown() { + var err error + + err = c.managers.http.Shutdown() + c.logger.Err(err).Msg("http manager shutdown") + + err = c.managers.plugins.Shutdown() + c.logger.Err(err).Msg("plugins manager shutdown") + + err = c.managers.webSocket.Shutdown() + c.logger.Err(err).Msg("websocket manager shutdown") + + err = c.managers.webRTC.Shutdown() + c.logger.Err(err).Msg("webrtc manager shutdown") + + err = c.managers.capture.Shutdown() + c.logger.Err(err).Msg("capture manager shutdown") + + err = c.managers.desktop.Shutdown() + c.logger.Err(err).Msg("desktop manager shutdown") + + err = c.managers.member.Disconnect() + c.logger.Err(err).Msg("member manager disconnect") +} + +func (c *serve) Run(cmd *cobra.Command, args []string) { + c.logger.Info().Msg("starting neko server") + c.Start(cmd) + c.logger.Info().Msg("neko ready") + + quit := make(chan os.Signal, 1) + signal.Notify(quit, os.Interrupt) + sig := <-quit + + c.logger.Warn().Msgf("received %s, attempting graceful shutdown", sig) + c.Shutdown() + c.logger.Info().Msg("shutdown complete") +} diff --git a/dev/build b/server/dev/build similarity index 100% rename from dev/build rename to server/dev/build diff --git a/dev/exec b/server/dev/exec similarity index 100% rename from dev/exec rename to server/dev/exec diff --git a/dev/fmt b/server/dev/fmt similarity index 73% rename from dev/fmt rename to server/dev/fmt index 977f8802..69c24eef 100755 --- a/dev/fmt +++ b/server/dev/fmt @@ -2,8 +2,8 @@ cd "$(dirname "$0")" if [ "$(docker images -q neko_server_build 2> /dev/null)" == "" ]; then - echo "Image 'neko_server_build' not found. Run ./build first." - exit 1 + echo "Image 'neko_server_build' not found. Run ./build first." + exit 1 fi docker run -it --rm \ diff --git a/dev/go b/server/dev/go similarity index 86% rename from dev/go rename to server/dev/go index 731d1d14..ae0acf12 100755 --- a/dev/go +++ b/server/dev/go @@ -2,8 +2,8 @@ cd "$(dirname "$0")" if [ "$(docker images -q neko_server_build 2> /dev/null)" == "" ]; then - echo "Image 'neko_server_build' not found. Run ./build first." - exit 1 + echo "Image 'neko_server_build' not found. Run ./build first." + exit 1 fi docker run -it \ diff --git a/dev/lint b/server/dev/lint similarity index 83% rename from dev/lint rename to server/dev/lint index a7aa6203..4124fda4 100755 --- a/dev/lint +++ b/server/dev/lint @@ -2,8 +2,8 @@ cd "$(dirname "$0")" if [ "$(docker images -q neko_server_build 2> /dev/null)" == "" ]; then - echo "Image 'neko_server_build' not found. Run ./build first." - exit 1 + echo "Image 'neko_server_build' not found. Run ./build first." + exit 1 fi # diff --git a/dev/rebuild b/server/dev/rebuild similarity index 100% rename from dev/rebuild rename to server/dev/rebuild diff --git a/dev/rebuild.input b/server/dev/rebuild.input similarity index 100% rename from dev/rebuild.input rename to server/dev/rebuild.input diff --git a/dev/runtime/Dockerfile b/server/dev/runtime/Dockerfile similarity index 100% rename from dev/runtime/Dockerfile rename to server/dev/runtime/Dockerfile diff --git a/dev/runtime/config.nvidia.yml b/server/dev/runtime/config.nvidia.yml similarity index 100% rename from dev/runtime/config.nvidia.yml rename to server/dev/runtime/config.nvidia.yml diff --git a/dev/runtime/config.yml b/server/dev/runtime/config.yml similarity index 100% rename from dev/runtime/config.yml rename to server/dev/runtime/config.yml diff --git a/dev/runtime/supervisord.conf b/server/dev/runtime/supervisord.conf similarity index 100% rename from dev/runtime/supervisord.conf rename to server/dev/runtime/supervisord.conf diff --git a/dev/start b/server/dev/start similarity index 88% rename from dev/start rename to server/dev/start index 514bd2f2..210e61c0 100755 --- a/dev/start +++ b/server/dev/start @@ -2,8 +2,8 @@ cd "$(dirname "$0")" if [ -z "$(docker images -q neko_server_app 2> /dev/null)" ]; then - echo "Image 'neko_server_app' not found. Running ./build first." - ./build + echo "Image 'neko_server_app' not found. Running ./build first." + ./build fi if [ -z $NEKO_PORT ]; then @@ -22,6 +22,10 @@ if [ -z $NEKO_NAT1TO1 ]; then fi done + if [ -z $NEKO_NAT1TO1 ]; then + NEKO_NAT1TO1=$(hostname -I 2>/dev/null | awk '{print $1}') + fi + if [ -z $NEKO_NAT1TO1 ]; then NEKO_NAT1TO1=$(hostname -i 2>/dev/null) fi diff --git a/server/go.mod b/server/go.mod index 98e16d2a..eb971ff6 100644 --- a/server/go.mod +++ b/server/go.mod @@ -1,55 +1,68 @@ -module m1k1o/neko +module github.com/demodesk/neko -go 1.20 +go 1.21 require ( - github.com/fsnotify/fsnotify v1.6.0 - github.com/go-chi/chi/v5 v5.0.10 + github.com/PaesslerAG/gval v1.2.2 + github.com/go-chi/chi v1.5.5 github.com/go-chi/cors v1.2.1 - github.com/gorilla/websocket v1.5.0 - github.com/pion/ice/v2 v2.3.0 - github.com/pion/interceptor v0.1.12 + github.com/gorilla/websocket v1.5.1 + github.com/kataras/go-events v0.0.3 + github.com/pion/ice/v2 v2.3.12 + github.com/pion/interceptor v0.1.25 github.com/pion/logging v0.2.2 - github.com/pion/rtp v1.7.13 // indirect - github.com/pion/srtp/v2 v2.0.12 // indirect - github.com/pion/webrtc/v3 v3.1.55 - github.com/pkg/errors v0.9.1 - github.com/rs/zerolog v1.29.0 - github.com/spf13/cast v1.5.0 // indirect - github.com/spf13/cobra v1.6.1 - github.com/spf13/viper v1.15.0 - golang.org/x/crypto v0.6.0 // indirect - golang.org/x/net v0.7.0 // indirect - golang.org/x/sys v0.5.0 // indirect - golang.org/x/text v0.7.0 // indirect - gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c // indirect + github.com/pion/rtcp v1.2.13 + github.com/pion/webrtc/v3 v3.2.24 + github.com/prometheus/client_golang v1.18.0 + github.com/rs/zerolog v1.31.0 + github.com/spf13/cobra v1.8.0 + github.com/spf13/viper v1.18.2 ) require ( - github.com/google/uuid v1.3.0 // indirect + github.com/beorn7/perks v1.0.1 // indirect + github.com/cespare/xxhash/v2 v2.2.0 // indirect + github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect + github.com/fsnotify/fsnotify v1.7.0 // indirect + github.com/google/uuid v1.6.0 // indirect github.com/hashicorp/hcl v1.0.0 // indirect github.com/inconshreveable/mousetrap v1.1.0 // indirect - github.com/kr/pretty v0.3.1 // indirect github.com/magiconair/properties v1.8.7 // indirect github.com/mattn/go-colorable v0.1.13 // indirect - github.com/mattn/go-isatty v0.0.17 // indirect + github.com/mattn/go-isatty v0.0.20 // indirect github.com/mitchellh/mapstructure v1.5.0 // indirect - github.com/pelletier/go-toml/v2 v2.0.6 // indirect + github.com/pelletier/go-toml/v2 v2.1.1 // indirect github.com/pion/datachannel v1.5.5 // indirect - github.com/pion/dtls/v2 v2.2.6 // indirect - github.com/pion/mdns v0.0.7 // indirect + github.com/pion/dtls/v2 v2.2.9 // indirect + github.com/pion/mdns v0.0.9 // indirect github.com/pion/randutil v0.1.0 // indirect - github.com/pion/rtcp v1.2.10 // indirect - github.com/pion/sctp v1.8.6 // indirect + github.com/pion/rtp v1.8.3 // indirect + github.com/pion/sctp v1.8.9 // indirect github.com/pion/sdp/v3 v3.0.6 // indirect - github.com/pion/stun v0.4.0 // indirect - github.com/pion/transport/v2 v2.0.2 // indirect - github.com/pion/turn/v2 v2.1.0 // indirect - github.com/pion/udp/v2 v2.0.1 // indirect - github.com/spf13/afero v1.9.4 // indirect - github.com/spf13/jwalterweatherman v1.1.0 // indirect + github.com/pion/srtp/v2 v2.0.18 // indirect + github.com/pion/stun v0.6.1 // indirect + github.com/pion/transport/v2 v2.2.4 // indirect + github.com/pion/turn/v2 v2.1.4 // indirect + github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect + github.com/prometheus/client_model v0.5.0 // indirect + github.com/prometheus/common v0.46.0 // indirect + github.com/prometheus/procfs v0.12.0 // indirect + github.com/sagikazarmark/locafero v0.4.0 // indirect + github.com/sagikazarmark/slog-shim v0.1.0 // indirect + github.com/shopspring/decimal v1.3.1 // indirect + github.com/sourcegraph/conc v0.3.0 // indirect + github.com/spf13/afero v1.11.0 // indirect + github.com/spf13/cast v1.6.0 // indirect github.com/spf13/pflag v1.0.5 // indirect - github.com/subosito/gotenv v1.4.2 // indirect + github.com/stretchr/testify v1.8.4 // indirect + github.com/subosito/gotenv v1.6.0 // indirect + go.uber.org/multierr v1.11.0 // indirect + golang.org/x/crypto v0.18.0 // indirect + golang.org/x/exp v0.0.0-20240119083558-1b970713d09a // indirect + golang.org/x/net v0.20.0 // indirect + golang.org/x/sys v0.16.0 // indirect + golang.org/x/text v0.14.0 // indirect + google.golang.org/protobuf v1.32.0 // indirect gopkg.in/ini.v1 v1.67.0 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect ) diff --git a/server/go.sum b/server/go.sum index a16b653f..84008d03 100644 --- a/server/go.sum +++ b/server/go.sum @@ -1,155 +1,57 @@ -cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= -cloud.google.com/go v0.34.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= -cloud.google.com/go v0.38.0/go.mod h1:990N+gfupTy94rShfmMCWGDn0LpTmnzTp2qbd1dvSRU= -cloud.google.com/go v0.44.1/go.mod h1:iSa0KzasP4Uvy3f1mN/7PiObzGgflwredwwASm/v6AU= -cloud.google.com/go v0.44.2/go.mod h1:60680Gw3Yr4ikxnPRS/oxxkBccT6SA1yMk63TGekxKY= -cloud.google.com/go v0.44.3/go.mod h1:60680Gw3Yr4ikxnPRS/oxxkBccT6SA1yMk63TGekxKY= -cloud.google.com/go v0.45.1/go.mod h1:RpBamKRgapWJb87xiFSdk4g1CME7QZg3uwTez+TSTjc= -cloud.google.com/go v0.46.3/go.mod h1:a6bKKbmY7er1mI7TEI4lsAkts/mkhTSZK8w33B4RAg0= -cloud.google.com/go v0.50.0/go.mod h1:r9sluTvynVuxRIOHXQEHMFffphuXHOMZMycpNR5e6To= -cloud.google.com/go v0.52.0/go.mod h1:pXajvRH/6o3+F9jDHZWQ5PbGhn+o8w9qiu/CffaVdO4= -cloud.google.com/go v0.53.0/go.mod h1:fp/UouUEsRkN6ryDKNW/Upv/JBKnv6WDthjR6+vze6M= -cloud.google.com/go v0.54.0/go.mod h1:1rq2OEkV3YMf6n/9ZvGWI3GWw0VoqH/1x2nd8Is/bPc= -cloud.google.com/go v0.56.0/go.mod h1:jr7tqZxxKOVYizybht9+26Z/gUq7tiRzu+ACVAMbKVk= -cloud.google.com/go v0.57.0/go.mod h1:oXiQ6Rzq3RAkkY7N6t3TcE6jE+CIBBbA36lwQ1JyzZs= -cloud.google.com/go v0.62.0/go.mod h1:jmCYTdRCQuc1PHIIJ/maLInMho30T/Y0M4hTdTShOYc= -cloud.google.com/go v0.65.0/go.mod h1:O5N8zS7uWy9vkA9vayVHs65eM1ubvY4h553ofrNHObY= -cloud.google.com/go v0.72.0/go.mod h1:M+5Vjvlc2wnp6tjzE102Dw08nGShTscUx2nZMufOKPI= -cloud.google.com/go v0.74.0/go.mod h1:VV1xSbzvo+9QJOxLDaJfTjx5e+MePCpCWwvftOeQmWk= -cloud.google.com/go v0.75.0/go.mod h1:VGuuCn7PG0dwsd5XPVm2Mm3wlh3EL55/79EKB6hlPTY= -cloud.google.com/go/bigquery v1.0.1/go.mod h1:i/xbL2UlR5RvWAURpBYZTtm/cXjCha9lbfbpx4poX+o= -cloud.google.com/go/bigquery v1.3.0/go.mod h1:PjpwJnslEMmckchkHFfq+HTD2DmtT67aNFKH1/VBDHE= -cloud.google.com/go/bigquery v1.4.0/go.mod h1:S8dzgnTigyfTmLBfrtrhyYhwRxG72rYxvftPBK2Dvzc= -cloud.google.com/go/bigquery v1.5.0/go.mod h1:snEHRnqQbz117VIFhE8bmtwIDY80NLUZUMb4Nv6dBIg= -cloud.google.com/go/bigquery v1.7.0/go.mod h1://okPTzCYNXSlb24MZs83e2Do+h+VXtc4gLoIoXIAPc= -cloud.google.com/go/bigquery v1.8.0/go.mod h1:J5hqkt3O0uAFnINi6JXValWIb1v0goeZM77hZzJN/fQ= -cloud.google.com/go/datastore v1.0.0/go.mod h1:LXYbyblFSglQ5pkeyhO+Qmw7ukd3C+pD7TKLgZqpHYE= -cloud.google.com/go/datastore v1.1.0/go.mod h1:umbIZjpQpHh4hmRpGhH4tLFup+FVzqBi1b3c64qFpCk= -cloud.google.com/go/pubsub v1.0.1/go.mod h1:R0Gpsv3s54REJCy4fxDixWD93lHJMoZTyQ2kNxGRt3I= -cloud.google.com/go/pubsub v1.1.0/go.mod h1:EwwdRX2sKPjnvnqCa270oGRyludottCI76h+R3AArQw= -cloud.google.com/go/pubsub v1.2.0/go.mod h1:jhfEVHT8odbXTkndysNHCcx0awwzvfOlguIAii9o8iA= -cloud.google.com/go/pubsub v1.3.1/go.mod h1:i+ucay31+CNRpDW4Lu78I4xXG+O1r/MAHgjpRVR+TSU= -cloud.google.com/go/storage v1.0.0/go.mod h1:IhtSnM/ZTZV8YYJWCY8RULGVqBDmpoyjwiyrjsg+URw= -cloud.google.com/go/storage v1.5.0/go.mod h1:tpKbwo567HUNpVclU5sGELwQWBDZ8gh0ZeosJ0Rtdos= -cloud.google.com/go/storage v1.6.0/go.mod h1:N7U0C8pVQ/+NIKOBQyamJIeKQKkZ+mxpohlUTyfDhBk= -cloud.google.com/go/storage v1.8.0/go.mod h1:Wv1Oy7z6Yz3DshWRJFhqM/UCfaWIRTdp0RXyy7KQOVs= -cloud.google.com/go/storage v1.10.0/go.mod h1:FLPqc6j+Ki4BU591ie1oL6qBQGu2Bl/tZ9ullr3+Kg0= -cloud.google.com/go/storage v1.14.0/go.mod h1:GrKmX003DSIwi9o29oFT7YDnHYwZoctc3fOKtUw0Xmo= -dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU= -github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= -github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo= -github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU= -github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI= -github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI= -github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU= -github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw= -github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc= -github.com/cncf/udpa/go v0.0.0-20200629203442-efcf912fb354/go.mod h1:WmhPx2Nbnhtbo57+VJT5O0JRkEi1Wbu0z5j0R8u5Hbk= -github.com/cncf/udpa/go v0.0.0-20201120205902-5459f2c99403/go.mod h1:WmhPx2Nbnhtbo57+VJT5O0JRkEi1Wbu0z5j0R8u5Hbk= -github.com/coreos/go-systemd/v22 v22.3.3-0.20220203105225-a9a7ef127534/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc= -github.com/cpuguy83/go-md2man/v2 v2.0.2/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= -github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= +github.com/PaesslerAG/gval v1.2.2 h1:Y7iBzhgE09IGTt5QgGQ2IdaYYYOU134YGHBThD+wm9E= +github.com/PaesslerAG/gval v1.2.2/go.mod h1:XRFLwvmkTEdYziLdaCeCa5ImcGVrfQbeNUbVR+C6xac= +github.com/PaesslerAG/jsonpath v0.1.0 h1:gADYeifvlqK3R3i2cR5B4DGgxLXIPb3TRTH1mGi0jPI= +github.com/PaesslerAG/jsonpath v0.1.0/go.mod h1:4BzmtoM/PI8fPO4aQGIusjGxGir2BzcV0grWtFzq1Y8= +github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM= +github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw= +github.com/cespare/xxhash/v2 v2.2.0 h1:DC2CZ1Ep5Y4k3ZQ899DldepgrayRUGE6BBZ/cd9Cj44= +github.com/cespare/xxhash/v2 v2.2.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= +github.com/coreos/go-systemd/v22 v22.5.0/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc= +github.com/cpuguy83/go-md2man/v2 v2.0.3/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= -github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= -github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= -github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= -github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1mIlRU8Am5FuJP05cCM98= -github.com/envoyproxy/go-control-plane v0.9.7/go.mod h1:cwu0lG7PUMfa9snN8LXBig5ynNVH9qI8YYLbd1fK2po= -github.com/envoyproxy/go-control-plane v0.9.9-0.20201210154907-fd9021fe5dad/go.mod h1:cXg6YxExXjJnVBQHBLXeUAgxn2UodCpnH306RInaBQk= -github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c= -github.com/frankban/quicktest v1.14.3 h1:FJKSZTDHjyhriyC81FLQ0LY93eSai0ZyR/ZIkd3ZUKE= +github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM= +github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHkI4W8= +github.com/frankban/quicktest v1.14.6/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7zb5vbUoiM6w0= github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo= github.com/fsnotify/fsnotify v1.4.9/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4IgpuI1SZQ= -github.com/fsnotify/fsnotify v1.6.0 h1:n+5WquG0fcWoWp6xPWfHdbskMCQaFnG6PfBrh1Ky4HY= -github.com/fsnotify/fsnotify v1.6.0/go.mod h1:sl3t1tCWJFWoRz9R8WJCbQihKKwmorjAbSClcnxKAGw= -github.com/go-chi/chi/v5 v5.0.10 h1:rLz5avzKpjqxrYwXNfmjkrYYXOyLJd37pz53UFHC6vk= -github.com/go-chi/chi/v5 v5.0.10/go.mod h1:DslCQbL2OYiznFReuXYUmQ2hGd1aDpCnlMNITLSKoi8= +github.com/fsnotify/fsnotify v1.7.0 h1:8JEhPFa5W2WU7YfeZzPNqzMP6Lwt7L2715Ggo0nosvA= +github.com/fsnotify/fsnotify v1.7.0/go.mod h1:40Bi/Hjc2AVfZrqy+aj+yEI+/bRxZnMJyTJwOpGvigM= +github.com/go-chi/chi v1.5.5 h1:vOB/HbEMt9QqBqErz07QehcOKHaWFtuj87tTDVz2qXE= +github.com/go-chi/chi v1.5.5/go.mod h1:C9JqLr3tIYjDOZpzn+BCuxY8z8vmca43EeMgyZt7irw= github.com/go-chi/cors v1.2.1 h1:xEC8UT3Rlp2QuWNEr4Fs/c2EAGVKBwy/1vHx3bppil4= github.com/go-chi/cors v1.2.1/go.mod h1:sSbTewc+6wYHBBCW7ytsFSn836hqM7JxpglAy2Vzc58= -github.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1/go.mod h1:vR7hzQXu2zJy9AVAgeJqvqgH9Q5CA+iKCZ2gyEVpxRU= -github.com/go-gl/glfw/v3.3/glfw v0.0.0-20191125211704-12ad95a8df72/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8= -github.com/go-gl/glfw/v3.3/glfw v0.0.0-20200222043503-6f7a984d4dc4/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8= github.com/go-task/slim-sprig v0.0.0-20210107165309-348f09dbbbc0/go.mod h1:fyg7847qk6SyHyPtNmDHnmrv/HOrqktSC+C9fM+CJOE= github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA= -github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q= -github.com/golang/groupcache v0.0.0-20190702054246-869f871628b6/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= -github.com/golang/groupcache v0.0.0-20191227052852-215e87163ea7/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= -github.com/golang/groupcache v0.0.0-20200121045136-8c9f03a8e57e/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= -github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= -github.com/golang/mock v1.2.0/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= -github.com/golang/mock v1.3.1/go.mod h1:sBzyDLLjw3U8JLTeZvSv8jJB+tU5PVekmnlKIyFUx0Y= -github.com/golang/mock v1.4.0/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw= -github.com/golang/mock v1.4.1/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw= -github.com/golang/mock v1.4.3/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw= -github.com/golang/mock v1.4.4/go.mod h1:l3mdAwkq5BuhzHwde/uurv3sEJeZMXNpwsxVWU71h+4= github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= -github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= -github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= -github.com/golang/protobuf v1.3.3/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw= -github.com/golang/protobuf v1.3.4/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw= -github.com/golang/protobuf v1.3.5/go.mod h1:6O5/vntMXwX2lRkT1hjjk0nAC1IDOTvTlVgjlRvqsdk= github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8= github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA= github.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrUpVNzEA03Pprs= github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:WU3c8KckQ9AFe+yFwt9sWVRKCVIyN9cPHBJSNnbL67w= github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0= -github.com/golang/protobuf v1.4.1/go.mod h1:U8fpvMrcmy5pZrNK1lt4xCsGvpyWQ/VVv6QDs8UjoX8= github.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI= -github.com/golang/protobuf v1.4.3/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI= github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk= github.com/golang/protobuf v1.5.2/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY= -github.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= -github.com/google/btree v1.0.0/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= -github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M= github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= -github.com/google/go-cmp v0.4.1/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= -github.com/google/go-cmp v0.5.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= -github.com/google/go-cmp v0.5.1/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= -github.com/google/go-cmp v0.5.2/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= -github.com/google/go-cmp v0.5.4/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38= -github.com/google/martian v2.1.0+incompatible/go.mod h1:9I4somxYTbIHy5NJKHRl3wXiIaQGbYVAs8BPL6v8lEs= -github.com/google/martian/v3 v3.0.0/go.mod h1:y5Zk1BBys9G+gd6Jrk0W3cC1+ELVxBWuIGO+w/tUAp0= -github.com/google/martian/v3 v3.1.0/go.mod h1:y5Zk1BBys9G+gd6Jrk0W3cC1+ELVxBWuIGO+w/tUAp0= -github.com/google/pprof v0.0.0-20181206194817-3ea8567a2e57/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc= -github.com/google/pprof v0.0.0-20190515194954-54271f7e092f/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc= -github.com/google/pprof v0.0.0-20191218002539-d4f498aebedc/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= -github.com/google/pprof v0.0.0-20200212024743-f11f1df84d12/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= -github.com/google/pprof v0.0.0-20200229191704-1ebb73c60ed3/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= -github.com/google/pprof v0.0.0-20200430221834-fc25d7d30c6d/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= -github.com/google/pprof v0.0.0-20200708004538-1a94d8640e99/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= -github.com/google/pprof v0.0.0-20201023163331-3e6fc7fc9c4c/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= -github.com/google/pprof v0.0.0-20201203190320-1bf35d6f28c2/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= -github.com/google/pprof v0.0.0-20201218002935-b9804c9f04c2/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= -github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI= -github.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= -github.com/google/uuid v1.3.0 h1:t6JiXgmwXMjEs8VusXIJk2BXHsn+wx8BZdTaoZ5fu7I= -github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= -github.com/googleapis/gax-go/v2 v2.0.4/go.mod h1:0Wqv26UfaUD9n4G6kQubkQ+KchISgw+vpHVxEJEs9eg= -github.com/googleapis/gax-go/v2 v2.0.5/go.mod h1:DWXyrwAJ9X0FpwwEdw+IPEYBICEFu5mhpdKc/us6bOk= -github.com/googleapis/google-cloud-go-testing v0.0.0-20200911160855-bcd43fbb19e8/go.mod h1:dvDLG8qkwmyD9a/MJJN3XJcT3xFxOKAvTZGvuZmac9g= -github.com/gorilla/websocket v1.5.0 h1:PPwGk2jz7EePpoHN/+ClbZu8SPxiqlu12wZP/3sWmnc= -github.com/gorilla/websocket v1.5.0/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= -github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= -github.com/hashicorp/golang-lru v0.5.1/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= +github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= +github.com/google/uuid v1.3.1/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= +github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/gorilla/websocket v1.5.1 h1:gmztn0JnHVt9JZquRuzLw3g4wouNVzKL15iLr/zn/QY= +github.com/gorilla/websocket v1.5.1/go.mod h1:x3kM2JMyaluk02fnUJpQuwD2dCS5NDG2ZHL0uE0tcaY= github.com/hashicorp/hcl v1.0.0 h1:0Anlzjpi4vEasTeNFn2mLJgTSwt0+6sfsiTG8qcWGx4= github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ= github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU= -github.com/ianlancetaylor/demangle v0.0.0-20181102032728-5e5cf60278f6/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc= -github.com/ianlancetaylor/demangle v0.0.0-20200824232613-28f6c0f3b639/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc= -github.com/inconshreveable/mousetrap v1.0.1/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw= github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8= github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw= -github.com/jstemmer/go-junit-report v0.0.0-20190106144839-af01ea7f8024/go.mod h1:6v2b51hI/fHJwM22ozAgKL4VKDeJcHhJFhtBdhmNjmU= -github.com/jstemmer/go-junit-report v0.9.1/go.mod h1:Brl9GWCQeLvo8nXZwPNNblvFj/XSXhF0NWZEnDohbsk= -github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= -github.com/kr/fs v0.1.0/go.mod h1:FFnZGqtBN9Gxj7eW1uZ42v5BccTP0vu6NEaFoC2HwRg= +github.com/kataras/go-events v0.0.3 h1:o5YK53uURXtrlg7qE/vovxd/yKOJcLuFtPQbf1rYMC4= +github.com/kataras/go-events v0.0.3/go.mod h1:bFBgtzwwzrag7kQmGuU1ZaVxhK2qseYPQomXoVEMsj4= github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= -github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI= github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= @@ -158,13 +60,12 @@ github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= github.com/magiconair/properties v1.8.7 h1:IeQXZAiQcpL9mgcAe1Nu6cX9LLw6ExEHKjN0VQdvPDY= github.com/magiconair/properties v1.8.7/go.mod h1:Dhd985XPs7jluiymwWYZ0G4Z61jb3vdS329zhj2hYo0= -github.com/mattn/go-colorable v0.1.12/go.mod h1:u5H1YNBxpqRaxsYJYSkiCWKzEfiAb1Gb520KVy5xxl4= github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA= github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg= -github.com/mattn/go-isatty v0.0.14/go.mod h1:7GGIvUiUoEMVVmxf/4nioHXj79iQHKdU27kJ6hsGG94= github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM= -github.com/mattn/go-isatty v0.0.17 h1:BTarxUcIeDqL27Mc+vyvdWYSL28zpIhv3RoTdsLMPng= -github.com/mattn/go-isatty v0.0.17/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM= +github.com/mattn/go-isatty v0.0.19/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= +github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= +github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= github.com/mitchellh/mapstructure v1.5.0 h1:jeMsZIYE/09sWLaz43PL7Gy6RuMjD2eJVyuac5Z2hdY= github.com/mitchellh/mapstructure v1.5.0/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo= github.com/nxadm/tail v1.4.4/go.mod h1:kenIhsEOeOJmVchQTgglprH7qJGnHDVpk1VPCcaMI8A= @@ -176,425 +77,221 @@ github.com/onsi/ginkgo v1.16.5/go.mod h1:+E8gABHa3K6zRBolWtd+ROzc/U5bkGt0FwiG042 github.com/onsi/gomega v1.7.1/go.mod h1:XdKZgCCFLUoM/7CFJVPcG8C1xQ1AJ0vpAezJrB7JYyY= github.com/onsi/gomega v1.10.1/go.mod h1:iN09h71vgCQne3DLsj+A5owkum+a2tYe+TOCB1ybHNo= github.com/onsi/gomega v1.17.0/go.mod h1:HnhC7FXeEQY45zxNK3PPoIUhzk/80Xly9PcubAlGdZY= -github.com/pelletier/go-toml/v2 v2.0.6 h1:nrzqCb7j9cDFj2coyLNLaZuJTLjWjlaz6nvTvIwycIU= -github.com/pelletier/go-toml/v2 v2.0.6/go.mod h1:eumQOmlWiOPt5WriQQqoM5y18pDHwha2N+QD+EUNTek= +github.com/pelletier/go-toml/v2 v2.1.1 h1:LWAJwfNvjQZCFIDKWYQaM62NcYeYViCmWIwmOStowAI= +github.com/pelletier/go-toml/v2 v2.1.1/go.mod h1:tJU2Z3ZkXwnxa4DPO899bsyIoywizdUvyaeZurnPPDc= github.com/pion/datachannel v1.5.5 h1:10ef4kwdjije+M9d7Xm9im2Y3O6A6ccQb0zcqZcJew8= github.com/pion/datachannel v1.5.5/go.mod h1:iMz+lECmfdCMqFRhXhcA/219B0SQlbpoR2V118yimL0= -github.com/pion/dtls/v2 v2.2.4/go.mod h1:WGKfxqhrddne4Kg3p11FUMJrynkOY4lb25zHNO49wuw= -github.com/pion/dtls/v2 v2.2.6 h1:yXMxKr0Skd+Ub6A8UqXTRLSywskx93ooMRHsQUtd+Z4= -github.com/pion/dtls/v2 v2.2.6/go.mod h1:t8fWJCIquY5rlQZwA2yWxUS1+OCrAdXrhVKXB5oD/wY= -github.com/pion/ice/v2 v2.3.0 h1:G+ysriabk1p9wbySDpdsnlD+6ZspLlDLagRduRfzJPk= -github.com/pion/ice/v2 v2.3.0/go.mod h1:+xO/cXVnnVUr6D2ZJcCT5g9LngucUkkTvfnTMqUxKRM= -github.com/pion/interceptor v0.1.12 h1:CslaNriCFUItiXS5o+hh5lpL0t0ytQkFnUcbbCs2Zq8= -github.com/pion/interceptor v0.1.12/go.mod h1:bDtgAD9dRkBZpWHGKaoKb42FhDHTG2rX8Ii9LRALLVA= +github.com/pion/dtls/v2 v2.2.7/go.mod h1:8WiMkebSHFD0T+dIU+UeBaoV7kDhOW5oDCzZ7WZ/F9s= +github.com/pion/dtls/v2 v2.2.9 h1:K+D/aVf9/REahQvqk6G5JavdrD8W1PWDKC11UlwN7ts= +github.com/pion/dtls/v2 v2.2.9/go.mod h1:8WiMkebSHFD0T+dIU+UeBaoV7kDhOW5oDCzZ7WZ/F9s= +github.com/pion/ice/v2 v2.3.11/go.mod h1:hPcLC3kxMa+JGRzMHqQzjoSj3xtE9F+eoncmXLlCL4E= +github.com/pion/ice/v2 v2.3.12 h1:NWKW2b3+oSZS3klbQMIEWQ0i52Kuo0KBg505a5kQv4s= +github.com/pion/ice/v2 v2.3.12/go.mod h1:hPcLC3kxMa+JGRzMHqQzjoSj3xtE9F+eoncmXLlCL4E= +github.com/pion/interceptor v0.1.25 h1:pwY9r7P6ToQ3+IF0bajN0xmk/fNw/suTgaTdlwTDmhc= +github.com/pion/interceptor v0.1.25/go.mod h1:wkbPYAak5zKsfpVDYMtEfWEy8D4zL+rpxCxPImLOg3Y= github.com/pion/logging v0.2.2 h1:M9+AIj/+pxNsDfAT64+MAVgJO0rsyLnoJKCqf//DoeY= github.com/pion/logging v0.2.2/go.mod h1:k0/tDVsRCX2Mb2ZEmTqNa7CWsQPc+YYCB7Q+5pahoms= -github.com/pion/mdns v0.0.7 h1:P0UB4Sr6xDWEox0kTVxF0LmQihtCbSAdW0H2nEgkA3U= -github.com/pion/mdns v0.0.7/go.mod h1:4iP2UbeFhLI/vWju/bw6ZfwjJzk0z8DNValjGxR/dD8= +github.com/pion/mdns v0.0.8/go.mod h1:hYE72WX8WDveIhg7fmXgMKivD3Puklk0Ymzog0lSyaI= +github.com/pion/mdns v0.0.9 h1:7Ue5KZsqq8EuqStnpPWV33vYYEH0+skdDN5L7EiEsI4= +github.com/pion/mdns v0.0.9/go.mod h1:2JA5exfxwzXiCihmxpTKgFUpiQws2MnipoPK09vecIc= github.com/pion/randutil v0.1.0 h1:CFG1UdESneORglEsnimhUjf33Rwjubwj6xfiOXBa3mA= github.com/pion/randutil v0.1.0/go.mod h1:XcJrSMMbbMRhASFVOlj/5hQial/Y8oH/HVo7TBZq+j8= -github.com/pion/rtcp v1.2.10 h1:nkr3uj+8Sp97zyItdN60tE/S6vk4al5CPRR6Gejsdjc= github.com/pion/rtcp v1.2.10/go.mod h1:ztfEwXZNLGyF1oQDttz/ZKIBaeeg/oWbRYqzBM9TL1I= -github.com/pion/rtp v1.7.13 h1:qcHwlmtiI50t1XivvoawdCGTP4Uiypzfrsap+bijcoA= -github.com/pion/rtp v1.7.13/go.mod h1:bDb5n+BFZxXx0Ea7E5qe+klMuqiBrP+w8XSjiWtCUko= +github.com/pion/rtcp v1.2.12/go.mod h1:sn6qjxvnwyAkkPzPULIbVqSKI5Dv54Rv7VG0kNxh9L4= +github.com/pion/rtcp v1.2.13 h1:+EQijuisKwm/8VBs8nWllr0bIndR7Lf7cZG200mpbNo= +github.com/pion/rtcp v1.2.13/go.mod h1:sn6qjxvnwyAkkPzPULIbVqSKI5Dv54Rv7VG0kNxh9L4= +github.com/pion/rtp v1.8.2/go.mod h1:pBGHaFt/yW7bf1jjWAoUjpSNoDnw98KTMg+jWWvziqU= +github.com/pion/rtp v1.8.3 h1:VEHxqzSVQxCkKDSHro5/4IUUG1ea+MFdqR2R3xSpNU8= +github.com/pion/rtp v1.8.3/go.mod h1:pBGHaFt/yW7bf1jjWAoUjpSNoDnw98KTMg+jWWvziqU= github.com/pion/sctp v1.8.5/go.mod h1:SUFFfDpViyKejTAdwD1d/HQsCu+V/40cCs2nZIvC3s0= -github.com/pion/sctp v1.8.6 h1:CUex11Vkt9YS++VhLf8b55O3VqKrWL6W3SDwX4jAqsI= -github.com/pion/sctp v1.8.6/go.mod h1:SUFFfDpViyKejTAdwD1d/HQsCu+V/40cCs2nZIvC3s0= +github.com/pion/sctp v1.8.8/go.mod h1:igF9nZBrjh5AtmKc7U30jXltsFHicFCXSmWA2GWRaWs= +github.com/pion/sctp v1.8.9 h1:TP5ZVxV5J7rz7uZmbyvnUvsn7EJ2x/5q9uhsTtXbI3g= +github.com/pion/sctp v1.8.9/go.mod h1:cMLT45jqw3+jiJCrtHVwfQLnfR0MGZ4rgOJwUOIqLkI= github.com/pion/sdp/v3 v3.0.6 h1:WuDLhtuFUUVpTfus9ILC4HRyHsW6TdugjEX/QY9OiUw= github.com/pion/sdp/v3 v3.0.6/go.mod h1:iiFWFpQO8Fy3S5ldclBkpXqmWy02ns78NOKoLLL0YQw= -github.com/pion/srtp/v2 v2.0.12 h1:WrmiVCubGMOAObBU1vwWjG0H3VSyQHawKeer2PVA5rY= -github.com/pion/srtp/v2 v2.0.12/go.mod h1:C3Ep44hlOo2qEYaq4ddsmK5dL63eLehXFbHaZ9F5V9Y= -github.com/pion/stun v0.4.0 h1:vgRrbBE2htWHy7l3Zsxckk7rkjnjOsSM7PHZnBwo8rk= -github.com/pion/stun v0.4.0/go.mod h1:QPsh1/SbXASntw3zkkrIk3ZJVKz4saBY2G7S10P3wCw= +github.com/pion/srtp/v2 v2.0.18 h1:vKpAXfawO9RtTRKZJbG4y0v1b11NZxQnxRl85kGuUlo= +github.com/pion/srtp/v2 v2.0.18/go.mod h1:0KJQjA99A6/a0DOVTu1PhDSw0CXF2jTkqOoMg3ODqdA= +github.com/pion/stun v0.6.1 h1:8lp6YejULeHBF8NmV8e2787BogQhduZugh5PdhDyyN4= +github.com/pion/stun v0.6.1/go.mod h1:/hO7APkX4hZKu/D0f2lHzNyvdkTGtIy3NDmLR7kSz/8= github.com/pion/transport v0.14.1 h1:XSM6olwW+o8J4SCmOBb/BpwZypkHeyM0PGFCxNQBr40= github.com/pion/transport v0.14.1/go.mod h1:4tGmbk00NeYA3rUa9+n+dzCCoKkcy3YlYb99Jn2fNnI= -github.com/pion/transport/v2 v2.0.0/go.mod h1:HS2MEBJTwD+1ZI2eSXSvHJx/HnzQqRy2/LXxt6eVMHc= -github.com/pion/transport/v2 v2.0.1/go.mod h1:93OYg91+mrGxKW+Jrgzmqr80kgXqD7J0yybOrdr7w0Y= -github.com/pion/transport/v2 v2.0.2 h1:St+8o+1PEzPT51O9bv+tH/KYYLMNR5Vwm5Z3Qkjsywg= -github.com/pion/transport/v2 v2.0.2/go.mod h1:vrz6bUbFr/cjdwbnxq8OdDDzHf7JJfGsIRkxfpZoTA0= -github.com/pion/turn/v2 v2.1.0 h1:5wGHSgGhJhP/RpabkUb/T9PdsAjkGLS6toYz5HNzoSI= -github.com/pion/turn/v2 v2.1.0/go.mod h1:yrT5XbXSGX1VFSF31A3c1kCNB5bBZgk/uu5LET162qs= -github.com/pion/udp v0.1.4/go.mod h1:G8LDo56HsFwC24LIcnT4YIDU5qcB6NepqqjP0keL2us= -github.com/pion/udp/v2 v2.0.1 h1:xP0z6WNux1zWEjhC7onRA3EwwSliXqu1ElUZAQhUP54= -github.com/pion/udp/v2 v2.0.1/go.mod h1:B7uvTMP00lzWdyMr/1PVZXtV3wpPIxBRd4Wl6AksXn8= -github.com/pion/webrtc/v3 v3.1.55 h1:jQt98hZ8DUi/l/s/rtogthBdsKKvKekFgZCX9hMEqRo= -github.com/pion/webrtc/v3 v3.1.55/go.mod h1:M1gU5mnvvo4e1nnLvF23esYz0nZAFOtbU/wq44MSfbc= -github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsKMEsNbx1VGcRFpLqf3715MtcvvzbA= -github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= +github.com/pion/transport/v2 v2.2.1/go.mod h1:cXXWavvCnFF6McHTft3DWS9iic2Mftcz1Aq29pGcU5g= +github.com/pion/transport/v2 v2.2.2/go.mod h1:OJg3ojoBJopjEeECq2yJdXH9YVrUJ1uQ++NjXLOUorc= +github.com/pion/transport/v2 v2.2.3/go.mod h1:q2U/tf9FEfnSBGSW6w5Qp5PFWRLRj3NjLhCCgpRK4p0= +github.com/pion/transport/v2 v2.2.4 h1:41JJK6DZQYSeVLxILA2+F4ZkKb4Xd/tFJZRFZQ9QAlo= +github.com/pion/transport/v2 v2.2.4/go.mod h1:q2U/tf9FEfnSBGSW6w5Qp5PFWRLRj3NjLhCCgpRK4p0= +github.com/pion/transport/v3 v3.0.1 h1:gDTlPJwROfSfz6QfSi0ZmeCSkFcnWWiiR9ES0ouANiM= +github.com/pion/transport/v3 v3.0.1/go.mod h1:UY7kiITrlMv7/IKgd5eTUcaahZx5oUN3l9SzK5f5xE0= +github.com/pion/turn/v2 v2.1.3/go.mod h1:huEpByKKHix2/b9kmTAM3YoX6MKP+/D//0ClgUYR2fY= +github.com/pion/turn/v2 v2.1.4 h1:2xn8rduI5W6sCZQkEnIUDAkrBQNl2eYIBCHMZ3QMmP8= +github.com/pion/turn/v2 v2.1.4/go.mod h1:huEpByKKHix2/b9kmTAM3YoX6MKP+/D//0ClgUYR2fY= +github.com/pion/webrtc/v3 v3.2.24 h1:MiFL5DMo2bDaaIFWr0DDpwiV/L4EGbLZb+xoRvfEo1Y= +github.com/pion/webrtc/v3 v3.2.24/go.mod h1:1CaT2fcZzZ6VZA+O1i9yK2DU4EOcXVvSbWG9pr5jefs= github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= -github.com/pkg/sftp v1.13.1/go.mod h1:3HaPG6Dq1ILlpPZRO0HVMrsydcdLt6HRDccSgb87qRg= -github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= -github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= -github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4= -github.com/rogpeppe/go-internal v1.9.0 h1:73kH8U+JUqXU8lRuOHeVHaa/SZPifC7BkcraZVejAe8= -github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs= -github.com/rs/xid v1.4.0/go.mod h1:trrq9SKmegXys3aeAKXMUTdJsYXVwGY3RLcfgqegfbg= -github.com/rs/zerolog v1.29.0 h1:Zes4hju04hjbvkVkOhdl2HpZa+0PmVwigmo8XoORE5w= -github.com/rs/zerolog v1.29.0/go.mod h1:NILgTygv/Uej1ra5XxGf82ZFSLk58MFGAUS2o6usyD0= +github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U= +github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/prometheus/client_golang v1.18.0 h1:HzFfmkOzH5Q8L8G+kSJKUx5dtG87sewO+FoDDqP5Tbk= +github.com/prometheus/client_golang v1.18.0/go.mod h1:T+GXkCk5wSJyOqMIzVgvvjFDlkOQntgjkJWKrN5txjA= +github.com/prometheus/client_model v0.5.0 h1:VQw1hfvPvk3Uv6Qf29VrPF32JB6rtbgI6cYPYQjL0Qw= +github.com/prometheus/client_model v0.5.0/go.mod h1:dTiFglRmd66nLR9Pv9f0mZi7B7fk5Pm3gvsjB5tr+kI= +github.com/prometheus/common v0.46.0 h1:doXzt5ybi1HBKpsZOL0sSkaNHJJqkyfEWZGGqqScV0Y= +github.com/prometheus/common v0.46.0/go.mod h1:Tp0qkxpb9Jsg54QMe+EAmqXkSV7Evdy1BTn+g2pa/hQ= +github.com/prometheus/procfs v0.12.0 h1:jluTpSng7V9hY0O2R9DzzJHYb2xULk9VTR1V1R/k6Bo= +github.com/prometheus/procfs v0.12.0/go.mod h1:pcuDEFsWDnvcgNzo4EEweacyhjeA9Zk3cnaOZAZEfOo= +github.com/rogpeppe/go-internal v1.10.0 h1:TMyTOH3F/DB16zRVcYyreMH6GnZZrwQVAoYjRBZyWFQ= +github.com/rogpeppe/go-internal v1.10.0/go.mod h1:UQnix2H7Ngw/k4C5ijL5+65zddjncjaFoBhdsK/akog= +github.com/rs/xid v1.5.0/go.mod h1:trrq9SKmegXys3aeAKXMUTdJsYXVwGY3RLcfgqegfbg= +github.com/rs/zerolog v1.31.0 h1:FcTR3NnLWW+NnTwwhFWiJSZr4ECLpqCm6QsEnyvbV4A= +github.com/rs/zerolog v1.31.0/go.mod h1:/7mN4D5sKwJLZQ2b/znpjC3/GQWY/xaDXUM0kKWRHss= github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= +github.com/sagikazarmark/locafero v0.4.0 h1:HApY1R9zGo4DBgr7dqsTH/JJxLTTsOt7u6keLGt6kNQ= +github.com/sagikazarmark/locafero v0.4.0/go.mod h1:Pe1W6UlPYUk/+wc/6KFhbORCfqzgYEpgQ3O5fPuL3H4= +github.com/sagikazarmark/slog-shim v0.1.0 h1:diDBnUNK9N/354PgrxMywXnAwEr1QZcOr6gto+ugjYE= +github.com/sagikazarmark/slog-shim v0.1.0/go.mod h1:SrcSrq8aKtyuqEI1uvTDTK1arOWRIczQRv+GVI1AkeQ= github.com/sclevine/agouti v3.0.0+incompatible/go.mod h1:b4WX9W9L1sfQKXeJf1mUTLZKJ48R1S7H23Ji7oFO5Bw= -github.com/spf13/afero v1.9.4 h1:Sd43wM1IWz/s1aVXdOBkjJvuP8UdyqioeE4AmM0QsBs= -github.com/spf13/afero v1.9.4/go.mod h1:iUV7ddyEEZPO5gA3zD4fJt6iStLlL+Lg4m2cihcDf8Y= -github.com/spf13/cast v1.5.0 h1:rj3WzYc11XZaIZMPKmwP96zkFEnnAmV8s6XbB2aY32w= -github.com/spf13/cast v1.5.0/go.mod h1:SpXXQ5YoyJw6s3/6cMTQuxvgRl3PCJiyaX9p6b155UU= -github.com/spf13/cobra v1.6.1 h1:o94oiPyS4KD1mPy2fmcYYHHfCxLqYjJOhGsCHFZtEzA= -github.com/spf13/cobra v1.6.1/go.mod h1:IOw/AERYS7UzyrGinqmz6HLUo219MORXGxhbaJUqzrY= -github.com/spf13/jwalterweatherman v1.1.0 h1:ue6voC5bR5F8YxI5S67j9i582FU4Qvo2bmqnqMYADFk= -github.com/spf13/jwalterweatherman v1.1.0/go.mod h1:aNWZUN0dPAAO/Ljvb5BEdw96iTZ0EXowPYD95IqWIGo= +github.com/shopspring/decimal v1.3.1 h1:2Usl1nmF/WZucqkFZhnfFYxxxu8LG21F6nPQBE5gKV8= +github.com/shopspring/decimal v1.3.1/go.mod h1:DKyhrW/HYNuLGql+MJL6WCR6knT2jwCFRcu2hWCYk4o= +github.com/sourcegraph/conc v0.3.0 h1:OQTbbt6P72L20UqAkXXuLOj79LfEanQ+YQFNpLA9ySo= +github.com/sourcegraph/conc v0.3.0/go.mod h1:Sdozi7LEKbFPqYX2/J+iBAM6HpqSLTASQIKqDmF7Mt0= +github.com/spf13/afero v1.11.0 h1:WJQKhtpdm3v2IzqG8VMqrr6Rf3UYpEF239Jy9wNepM8= +github.com/spf13/afero v1.11.0/go.mod h1:GH9Y3pIexgf1MTIWtNGyogA5MwRIDXGUr+hbWNoBjkY= +github.com/spf13/cast v1.6.0 h1:GEiTHELF+vaR5dhz3VqZfFSzZjYbgeKDpBxQVS4GYJ0= +github.com/spf13/cast v1.6.0/go.mod h1:ancEpBxwJDODSW/UG4rDrAqiKolqNNh2DX3mk86cAdo= +github.com/spf13/cobra v1.8.0 h1:7aJaZx1B85qltLMc546zn58BxxfZdR/W22ej9CFoEf0= +github.com/spf13/cobra v1.8.0/go.mod h1:WXLWApfZ71AjXPya3WOlMsY9yMs7YeiHhFVlvLyhcho= github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA= github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= -github.com/spf13/viper v1.15.0 h1:js3yy885G8xwJa6iOISGFwd+qlUo5AvyXb7CiihdtiU= -github.com/spf13/viper v1.15.0/go.mod h1:fFcTBJxvhhzSJiZy8n+PeW6t8l+KeT/uTARa0jHOQLA= +github.com/spf13/viper v1.18.2 h1:LUXCnvUvSM6FXAsj6nnfc8Q2tp1dIgUfY9Kc8GsSOiQ= +github.com/spf13/viper v1.18.2/go.mod h1:EKmWIqdnk5lOcmR72yw6hS+8OPYcwD0jteitLMVB+yk= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= -github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= -github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA= -github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= -github.com/stretchr/testify v1.8.1 h1:w7B6lhMri9wdJUVmEZPGGhZzrYTPvgJArz7wNPgYKsk= github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= -github.com/subosito/gotenv v1.4.2 h1:X1TuBLAMDFbaTAChgCBLu3DU3UPyELpnF2jjJ2cz/S8= -github.com/subosito/gotenv v1.4.2/go.mod h1:ayKnFf/c6rvx/2iiLrJUk1e6plDbT3edrFNGqEflhK0= -github.com/yuin/goldmark v1.1.25/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= -github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= -github.com/yuin/goldmark v1.1.32/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= +github.com/stretchr/testify v1.8.3/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= +github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk= +github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= +github.com/subosito/gotenv v1.6.0 h1:9NlTDc1FTs4qu0DDq7AEtTPNw6SVm7uBMsUCUjABIf8= +github.com/subosito/gotenv v1.6.0/go.mod h1:Dk4QP5c2W3ibzajGcXpNraDfq2IrhjMIvMSWPKKo0FU= github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= -go.opencensus.io v0.21.0/go.mod h1:mSImk1erAIZhrmZN+AvHh14ztQfjbGwt4TtuofqLduU= -go.opencensus.io v0.22.0/go.mod h1:+kGneAE2xo2IficOXnaByMWTGM9T73dGwxeWcUqIpI8= -go.opencensus.io v0.22.2/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= -go.opencensus.io v0.22.3/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= -go.opencensus.io v0.22.4/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= -go.opencensus.io v0.22.5/go.mod h1:5pWMHQbX5EPX2/62yrJeAkowc+lfs/XD7Uxpq3pI6kk= +go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0= +go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= -golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= -golang.org/x/crypto v0.0.0-20190605123033-f99c8df09eb5/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= -golang.org/x/crypto v0.0.0-20210421170649-83a5a9bb288b/go.mod h1:T9bdIzuCu7OtxOm1hfPfRQxPLYneinmdGuTeoZ9dtd4= golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= -golang.org/x/crypto v0.0.0-20211108221036-ceb1ce70b4fa/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= -golang.org/x/crypto v0.5.0/go.mod h1:NK/OQwhpMQP3MwtdjgLlYHnH9ebylxKWv3e0fK+mkQU= -golang.org/x/crypto v0.6.0 h1:qfktjS5LUO+fFKeJXZ+ikTRijMmljikvG68fpMMruSc= -golang.org/x/crypto v0.6.0/go.mod h1:OFC/31mSvZgRz0V1QTNCzfAI1aIRzbiufJtkMIlEp58= -golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= -golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= -golang.org/x/exp v0.0.0-20190510132918-efd6b22b2522/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8= -golang.org/x/exp v0.0.0-20190829153037-c13cbed26979/go.mod h1:86+5VVa7VpoJ4kLfm080zCjGlMRFzhUhsZKEZO7MGek= -golang.org/x/exp v0.0.0-20191030013958-a1ab85dbe136/go.mod h1:JXzH8nQsPlswgeRAPE3MuO9GYsAcnJvJ4vnMwN/5qkY= -golang.org/x/exp v0.0.0-20191129062945-2f5052295587/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4= -golang.org/x/exp v0.0.0-20191227195350-da58074b4299/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4= -golang.org/x/exp v0.0.0-20200119233911-0405dc783f0a/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4= -golang.org/x/exp v0.0.0-20200207192155-f17229e696bd/go.mod h1:J/WKrq2StrnmMY6+EHIKF9dgMWnmCNThgcyBT1FY9mM= -golang.org/x/exp v0.0.0-20200224162631-6cc2880d07d6/go.mod h1:3jZMyOhIsHpP37uCMkUooju7aAi5cS1Q23tOzKc+0MU= -golang.org/x/image v0.0.0-20190227222117-0694c2d4d067/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js= -golang.org/x/image v0.0.0-20190802002840-cff245a6509b/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0= -golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= -golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU= -golang.org/x/lint v0.0.0-20190301231843-5614ed5bae6f/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= -golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= -golang.org/x/lint v0.0.0-20190409202823-959b441ac422/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= -golang.org/x/lint v0.0.0-20190909230951-414d861bb4ac/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= -golang.org/x/lint v0.0.0-20190930215403-16217165b5de/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= -golang.org/x/lint v0.0.0-20191125180803-fdd1cda4f05f/go.mod h1:5qLYkcX4OjUUV8bRuDixDT3tpyyb+LUpUlRWLxfhWrs= -golang.org/x/lint v0.0.0-20200130185559-910be7a94367/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY= -golang.org/x/lint v0.0.0-20200302205851-738671d3881b/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY= -golang.org/x/lint v0.0.0-20201208152925-83fdc39ff7b5/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY= -golang.org/x/mobile v0.0.0-20190312151609-d3739f865fa6/go.mod h1:z+o9i4GpDbdi3rU15maQ/Ox0txvL9dWGYEHz965HBQE= -golang.org/x/mobile v0.0.0-20190719004257-d2bd2a29d028/go.mod h1:E/iHnbuqvinMTCcRqshq8CkpyQDoeVncDDYHnLhea+o= -golang.org/x/mod v0.0.0-20190513183733-4bf6d317e70e/go.mod h1:mXi4GBBbnImb6dmsKGUJ2LatrhH/nqhxcFungHvyanc= -golang.org/x/mod v0.1.0/go.mod h1:0QHyrYULN0/3qlju5TqG8bIK38QM8yzMo5ekMj3DlcY= -golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg= -golang.org/x/mod v0.1.1-0.20191107180719-034126e5016b/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg= -golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= +golang.org/x/crypto v0.8.0/go.mod h1:mRqEX+O9/h5TFCrQhkgjo2yKi0yYA+9ecGkdQoHrywE= +golang.org/x/crypto v0.10.0/go.mod h1:o4eNf7Ede1fv+hwOwZsTHl9EsPFO6q6ZvYR8vYfY45I= +golang.org/x/crypto v0.11.0/go.mod h1:xgJhtzW8F9jGdVFWZESrid1U1bjeNy4zgy5cRr/CIio= +golang.org/x/crypto v0.12.0/go.mod h1:NF0Gs7EO5K4qLn+Ylc+fih8BSTeIjAP05siRnAh98yw= +golang.org/x/crypto v0.13.0/go.mod h1:y6Z2r+Rw4iayiXXAIxJIDAJ1zMW4yaTpebo8fPOliYc= +golang.org/x/crypto v0.18.0 h1:PGVlW0xEltQnzFZ55hkuX5+KLyrMYhHld1YHO4AKcdc= +golang.org/x/crypto v0.18.0/go.mod h1:R0j02AL6hcrfOiy9T4ZYp/rcWeMxM3L6QYxlOuEG1mg= +golang.org/x/exp v0.0.0-20240119083558-1b970713d09a h1:Q8/wZp0KX97QFTc2ywcOE0YRjZPVIx+MXInMzdvQqcA= +golang.org/x/exp v0.0.0-20240119083558-1b970713d09a/go.mod h1:idGWGoKP1toJGkd5/ig9ZLuPcZBC3ewk7SzmH0uou08= golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= -golang.org/x/mod v0.4.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= -golang.org/x/mod v0.4.1/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= -golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= -golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= -golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= -golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= -golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= -golang.org/x/net v0.0.0-20190501004415-9ce7a6920f09/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= -golang.org/x/net v0.0.0-20190503192946-f4e77d36d62c/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= -golang.org/x/net v0.0.0-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks= golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= -golang.org/x/net v0.0.0-20190628185345-da137c7871d7/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= -golang.org/x/net v0.0.0-20190724013045-ca1201d0de80/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= -golang.org/x/net v0.0.0-20191209160850-c0dbc17a3553/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= -golang.org/x/net v0.0.0-20200114155413-6afb5195e5aa/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= -golang.org/x/net v0.0.0-20200202094626-16171245cfb2/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= -golang.org/x/net v0.0.0-20200222125558-5a598a2470a0/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= -golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= -golang.org/x/net v0.0.0-20200301022130-244492dfa37a/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= -golang.org/x/net v0.0.0-20200324143707-d3edc9973b7e/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= -golang.org/x/net v0.0.0-20200501053045-e0ff5e5a1de5/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= -golang.org/x/net v0.0.0-20200506145744-7e3656a0809f/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= -golang.org/x/net v0.0.0-20200513185701-a91f0712d120/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= golang.org/x/net v0.0.0-20200520004742-59133d7f0dd7/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= -golang.org/x/net v0.0.0-20200520182314-0ba52f642ac2/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= -golang.org/x/net v0.0.0-20200625001655-4c5254603344/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= -golang.org/x/net v0.0.0-20200707034311-ab3426394381/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= -golang.org/x/net v0.0.0-20200822124328-c89045814202/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= -golang.org/x/net v0.0.0-20201031054903-ff519b6c9102/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= -golang.org/x/net v0.0.0-20201209123823-ac852fbbde11/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= -golang.org/x/net v0.0.0-20201224014010-6772e930b67b/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= golang.org/x/net v0.0.0-20210428140749-89ef3d95e781/go.mod h1:OJAsFXCWl8Ukc7SiCT/9KSuxbyM7479/AVlXFRxuMCk= golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= golang.org/x/net v0.1.0/go.mod h1:Cx3nUiGt4eDBEyega/BKRp+/AlGL8hYe7U9odMt2Cco= -golang.org/x/net v0.5.0/go.mod h1:DivGGAXEgPSlEBzxGzZI+ZLohi+xUj054jfeKui00ws= golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= -golang.org/x/net v0.7.0 h1:rJrUqqhjsgNp7KqAIc25s9pZnjU7TUcSY7HcVZjdn1g= -golang.org/x/net v0.7.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= -golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= -golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= -golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= -golang.org/x/oauth2 v0.0.0-20191202225959-858c2ad4c8b6/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= -golang.org/x/oauth2 v0.0.0-20200107190931-bf48bf16ab8d/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= -golang.org/x/oauth2 v0.0.0-20200902213428-5d25da1a8d43/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= -golang.org/x/oauth2 v0.0.0-20201109201403-9fd604954f58/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= -golang.org/x/oauth2 v0.0.0-20201208152858-08078c50e5b5/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= -golang.org/x/oauth2 v0.0.0-20210218202405-ba52d332ba99/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= +golang.org/x/net v0.9.0/go.mod h1:d48xBJpPfHeWQsugry2m+kC02ZBRGRgulfHnEXEuWns= +golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg= +golang.org/x/net v0.11.0/go.mod h1:2L/ixqYpgIVXmeoSA/4Lu7BzTG4KIyPIryS4IsOd1oQ= +golang.org/x/net v0.13.0/go.mod h1:zEVYFnQC7m/vmpQFELhcD1EWkZlX69l4oqgmer6hfKA= +golang.org/x/net v0.14.0/go.mod h1:PpSgVXXLK0OxS0F31C1/tv6XNguvCrnXIDrFMspZIUI= +golang.org/x/net v0.15.0/go.mod h1:idbUs1IY1+zTqbi8yxTbhexhEEk5ur9LInksu6HrEpk= +golang.org/x/net v0.20.0 h1:aCL9BSgETF1k+blQaYUBx9hJ9LOGP3gAVemcZlf1Kpo= +golang.org/x/net v0.20.0/go.mod h1:z8BVo6PvndSri0LbOE3hAn0apkU+1YvI6E70E9jsnvY= golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.0.0-20190227155943-e225da77a7e6/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.0.0-20200317015054-43a5402ce75a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.0.0-20200625203802-6e8e738ad208/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.0.0-20201207232520-09787c993a3a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= -golang.org/x/sys v0.0.0-20190312061237-fead79001313/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20190502145724-3ef323f4f1fd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20190507160741-ecd444e8653b/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20190606165138-5da285871e9c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20190624142023-c5567b49c5d0/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20190726091711-fc99dfbffb4e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190904154756-749cb33beabd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20191001151750-bb3f8db39f24/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20191005200804-aed5e4c7ecf9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20191120155948-bd437916bb0e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20191204072324-ce4227a45e2e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20191228213918-04cbcbbfeed8/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200113162924-86b910548bc1/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200122134326-e047566fdf82/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200202164722-d101bd2416d5/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200212091648-12a6c2dcc1e4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200302150141-5c8b2ff67527/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200331124033-c3d80250170d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200501052902-10377860bb8e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200511232937-7e40ca221e25/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200515095857-1151b9dac4a9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200523222454-059865788121/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200803210538-64077c9b5642/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200905004654-be1d3432aa8f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20201201145000-ef89a241ccb3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20210104204734-6f8348627aad/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210112080510-489259a85091/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20210119212857-b64e53b001e4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20210225134936-a50acf3fe073/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20210423185535-09eb48e85fd7/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20210927094055-39ccf1dd6fa6/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20220908164124-27713097b956/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.2.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.4.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.5.0 h1:MUK/U/4lj1t1oPg0HfuXDN/Z1wv31ZJ/YcPiGccS4DU= golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.7.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.9.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.10.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.11.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.16.0 h1:xWw16ngr6ZMtmxDyKyIgsE93KNKz5HKmMa3b8ALHidU= +golang.org/x/sys v0.16.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= golang.org/x/term v0.1.0/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= -golang.org/x/term v0.4.0/go.mod h1:9P2UbLfCdcvo3p/nzKvsmas4TnlujnuoV9hGgYzW1lQ= golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k= -golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/term v0.7.0/go.mod h1:P32HKFT3hSsZrRxla30E9HqToFYAQPCMs/zFMBUFqPY= +golang.org/x/term v0.8.0/go.mod h1:xPskH00ivmX89bAKVGSKKtLOWNx2+17Eiy94tnKShWo= +golang.org/x/term v0.9.0/go.mod h1:M6DEAAIenWoTxdKrOltXcmDY3rSplQUkrvaDU5FcQyo= +golang.org/x/term v0.10.0/go.mod h1:lpqdcUyK/oCiQxvxVrppt5ggO2KCZ5QblwqPnfZ6d5o= +golang.org/x/term v0.11.0/go.mod h1:zC9APTIj3jG3FdV/Ons+XE1riIZXG4aZ4GTHiPZJPIU= +golang.org/x/term v0.12.0/go.mod h1:owVbMEjm3cBLCHdkQu9b1opXd4ETQWc3BhuQGKgXgvU= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= -golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= -golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= -golang.org/x/text v0.3.4/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= golang.org/x/text v0.4.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= -golang.org/x/text v0.6.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= -golang.org/x/text v0.7.0 h1:4BRB4x83lYWy72KwLD/qYDuTu7q9PjSagHvijDw7cLo= golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= -golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= -golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= -golang.org/x/time v0.0.0-20191024005414-555d28b269f0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= +golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8= +golang.org/x/text v0.10.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE= +golang.org/x/text v0.11.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE= +golang.org/x/text v0.12.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE= +golang.org/x/text v0.13.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE= +golang.org/x/text v0.14.0 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ= +golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= -golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= -golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY= -golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= -golang.org/x/tools v0.0.0-20190312151545-0bb0c0a6e846/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= -golang.org/x/tools v0.0.0-20190312170243-e65039ee4138/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= -golang.org/x/tools v0.0.0-20190425150028-36563e24a262/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= -golang.org/x/tools v0.0.0-20190506145303-2d16b83fe98c/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= -golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= -golang.org/x/tools v0.0.0-20190606124116-d0a3d012864b/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= -golang.org/x/tools v0.0.0-20190621195816-6e04913cbbac/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= -golang.org/x/tools v0.0.0-20190628153133-6cdbf07be9d0/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= -golang.org/x/tools v0.0.0-20190816200558-6889da9d5479/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= -golang.org/x/tools v0.0.0-20190911174233-4f2ddba30aff/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= -golang.org/x/tools v0.0.0-20191012152004-8de300cfc20a/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= -golang.org/x/tools v0.0.0-20191113191852-77e3bb0ad9e7/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= -golang.org/x/tools v0.0.0-20191115202509-3a792d9c32b2/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= -golang.org/x/tools v0.0.0-20191125144606-a911d9008d1f/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= -golang.org/x/tools v0.0.0-20191130070609-6e064ea0cf2d/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= -golang.org/x/tools v0.0.0-20191216173652-a0e659d51361/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= -golang.org/x/tools v0.0.0-20191227053925-7b8e75db28f4/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= -golang.org/x/tools v0.0.0-20200117161641-43d50277825c/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= -golang.org/x/tools v0.0.0-20200122220014-bf1340f18c4a/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= -golang.org/x/tools v0.0.0-20200130002326-2f3ba24bd6e7/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= -golang.org/x/tools v0.0.0-20200204074204-1cc6d1ef6c74/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= -golang.org/x/tools v0.0.0-20200207183749-b753a1ba74fa/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= -golang.org/x/tools v0.0.0-20200212150539-ea181f53ac56/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= -golang.org/x/tools v0.0.0-20200224181240-023911ca70b2/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= -golang.org/x/tools v0.0.0-20200227222343-706bc42d1f0d/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= -golang.org/x/tools v0.0.0-20200304193943-95d2e580d8eb/go.mod h1:o4KQGtdN14AW+yjsvvwRTJJuXz8XRtIHtEnmAXLyFUw= -golang.org/x/tools v0.0.0-20200312045724-11d5b4c81c7d/go.mod h1:o4KQGtdN14AW+yjsvvwRTJJuXz8XRtIHtEnmAXLyFUw= -golang.org/x/tools v0.0.0-20200331025713-a30bf2db82d4/go.mod h1:Sl4aGygMT6LrqrWclx+PTx3U+LnKx/seiNR+3G19Ar8= -golang.org/x/tools v0.0.0-20200501065659-ab2804fb9c9d/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= -golang.org/x/tools v0.0.0-20200512131952-2bc93b1c0c88/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= -golang.org/x/tools v0.0.0-20200515010526-7d3b6ebf133d/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= -golang.org/x/tools v0.0.0-20200618134242-20370b0cb4b2/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= -golang.org/x/tools v0.0.0-20200729194436-6467de6f59a7/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA= -golang.org/x/tools v0.0.0-20200804011535-6c149bb5ef0d/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA= -golang.org/x/tools v0.0.0-20200825202427-b303f430e36d/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA= -golang.org/x/tools v0.0.0-20200904185747-39188db58858/go.mod h1:Cj7w3i3Rnn0Xh82ur9kSqwfTHTeVxaDqrfMjpcNT6bE= -golang.org/x/tools v0.0.0-20201110124207-079ba7bd75cd/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= -golang.org/x/tools v0.0.0-20201201161351-ac6f37ff4c2a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= -golang.org/x/tools v0.0.0-20201208233053-a543418bbed2/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= golang.org/x/tools v0.0.0-20201224043029-2b0845dc783e/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= -golang.org/x/tools v0.0.0-20210105154028-b0ab187a4818/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= -golang.org/x/tools v0.0.0-20210108195828-e2f9c7f1fc8e/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= -golang.org/x/tools v0.1.0/go.mod h1:xkSsbof2nBLbhDlRMhhhyNLN/zl3eTqcnHD5viDpcZ0= golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= +golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= -google.golang.org/api v0.4.0/go.mod h1:8k5glujaEP+g9n7WNsDg8QP6cUVNI86fCNMcbazEtwE= -google.golang.org/api v0.7.0/go.mod h1:WtwebWUNSVBH/HAw79HIFXZNqEvBhG+Ra+ax0hx3E3M= -google.golang.org/api v0.8.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg= -google.golang.org/api v0.9.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg= -google.golang.org/api v0.13.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI= -google.golang.org/api v0.14.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI= -google.golang.org/api v0.15.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI= -google.golang.org/api v0.17.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= -google.golang.org/api v0.18.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= -google.golang.org/api v0.19.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= -google.golang.org/api v0.20.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= -google.golang.org/api v0.22.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= -google.golang.org/api v0.24.0/go.mod h1:lIXQywCXRcnZPGlsd8NbLnOjtAoL6em04bJ9+z0MncE= -google.golang.org/api v0.28.0/go.mod h1:lIXQywCXRcnZPGlsd8NbLnOjtAoL6em04bJ9+z0MncE= -google.golang.org/api v0.29.0/go.mod h1:Lcubydp8VUV7KeIHD9z2Bys/sm/vGKnG1UHuDBSrHWM= -google.golang.org/api v0.30.0/go.mod h1:QGmEvQ87FHZNiUVJkT14jQNYJ4ZJjdRF23ZXz5138Fc= -google.golang.org/api v0.35.0/go.mod h1:/XrVsuzM0rZmrsbjJutiuftIzeuTQcEeaYcSk/mQ1dg= -google.golang.org/api v0.36.0/go.mod h1:+z5ficQTmoYpPn8LCUNVpK5I7hwkpjbcgqA7I34qYtE= -google.golang.org/api v0.40.0/go.mod h1:fYKFpnQN0DsDSKRVRcQSDQNtqWPfM9i+zNPxepjRCQ8= -google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM= -google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= -google.golang.org/appengine v1.5.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= -google.golang.org/appengine v1.6.1/go.mod h1:i06prIuMbXzDqacNJfV5OdTW448YApPu5ww/cMBSeb0= -google.golang.org/appengine v1.6.5/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc= -google.golang.org/appengine v1.6.6/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc= -google.golang.org/appengine v1.6.7/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc= -google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc= -google.golang.org/genproto v0.0.0-20190307195333-5fe7a883aa19/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= -google.golang.org/genproto v0.0.0-20190418145605-e7d98fc518a7/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= -google.golang.org/genproto v0.0.0-20190425155659-357c62f0e4bb/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= -google.golang.org/genproto v0.0.0-20190502173448-54afdca5d873/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= -google.golang.org/genproto v0.0.0-20190801165951-fa694d86fc64/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc= -google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc= -google.golang.org/genproto v0.0.0-20190911173649-1774047e7e51/go.mod h1:IbNlFCBrqXvoKpeg0TB2l7cyZUmoaFKYIwrEpbDKLA8= -google.golang.org/genproto v0.0.0-20191108220845-16a3f7862a1a/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= -google.golang.org/genproto v0.0.0-20191115194625-c23dd37a84c9/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= -google.golang.org/genproto v0.0.0-20191216164720-4f79533eabd1/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= -google.golang.org/genproto v0.0.0-20191230161307-f3c370f40bfb/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= -google.golang.org/genproto v0.0.0-20200115191322-ca5a22157cba/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= -google.golang.org/genproto v0.0.0-20200122232147-0452cf42e150/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= -google.golang.org/genproto v0.0.0-20200204135345-fa8e72b47b90/go.mod h1:GmwEX6Z4W5gMy59cAlVYjN9JhxgbQH6Gn+gFDQe2lzA= -google.golang.org/genproto v0.0.0-20200212174721-66ed5ce911ce/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= -google.golang.org/genproto v0.0.0-20200224152610-e50cd9704f63/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= -google.golang.org/genproto v0.0.0-20200228133532-8c2c7df3a383/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= -google.golang.org/genproto v0.0.0-20200305110556-506484158171/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= -google.golang.org/genproto v0.0.0-20200312145019-da6875a35672/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= -google.golang.org/genproto v0.0.0-20200331122359-1ee6d9798940/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= -google.golang.org/genproto v0.0.0-20200430143042-b979b6f78d84/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= -google.golang.org/genproto v0.0.0-20200511104702-f5ebc3bea380/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= -google.golang.org/genproto v0.0.0-20200515170657-fc4c6c6a6587/go.mod h1:YsZOwe1myG/8QRHRsmBRE1LrgQY60beZKjly0O1fX9U= -google.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013/go.mod h1:NbSheEEYHJ7i3ixzK3sjbqSGDJWnxyFXZblF3eUsNvo= -google.golang.org/genproto v0.0.0-20200618031413-b414f8b61790/go.mod h1:jDfRM7FcilCzHH/e9qn6dsT145K34l5v+OpcnNgKAAA= -google.golang.org/genproto v0.0.0-20200729003335-053ba62fc06f/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= -google.golang.org/genproto v0.0.0-20200804131852-c06518451d9c/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= -google.golang.org/genproto v0.0.0-20200825200019-8632dd797987/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= -google.golang.org/genproto v0.0.0-20200904004341-0bd0a958aa1d/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= -google.golang.org/genproto v0.0.0-20201109203340-2640f1f9cdfb/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= -google.golang.org/genproto v0.0.0-20201201144952-b05cb90ed32e/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= -google.golang.org/genproto v0.0.0-20201210142538-e3217bee35cc/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= -google.golang.org/genproto v0.0.0-20201214200347-8c77b98c765d/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= -google.golang.org/genproto v0.0.0-20210108203827-ffc7fda8c3d7/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= -google.golang.org/genproto v0.0.0-20210226172003-ab064af71705/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= -google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c= -google.golang.org/grpc v1.20.1/go.mod h1:10oTOabMzJvdu6/UiuZezV6QK5dSlG84ov/aaiqXj38= -google.golang.org/grpc v1.21.1/go.mod h1:oYelfM1adQP15Ek0mdvEgi9Df8B9CZIaU1084ijfRaM= -google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg= -google.golang.org/grpc v1.25.1/go.mod h1:c3i+UQWmh7LiEpx4sFZnkU36qjEYZ0imhYfXVyQciAY= -google.golang.org/grpc v1.26.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= -google.golang.org/grpc v1.27.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= -google.golang.org/grpc v1.27.1/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= -google.golang.org/grpc v1.28.0/go.mod h1:rpkK4SK4GF4Ach/+MFLZUBavHOvF2JJB5uozKKal+60= -google.golang.org/grpc v1.29.1/go.mod h1:itym6AZVZYACWQqET3MqgPpjcuV5QH3BxFS3IjizoKk= -google.golang.org/grpc v1.30.0/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak= -google.golang.org/grpc v1.31.0/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak= -google.golang.org/grpc v1.31.1/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak= -google.golang.org/grpc v1.33.2/go.mod h1:JMHMWHQWaTccqQQlmk3MJZS+GWXOdAesneDmEnv2fbc= -google.golang.org/grpc v1.34.0/go.mod h1:WotjhfgOW/POjDeRt8vscBtXq+2VjORFy659qA51WJ8= -google.golang.org/grpc v1.35.0/go.mod h1:qjiiYl8FncCW8feJPdyg3v6XW24KsRHe+dy9BAGRRjU= google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8= google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0= google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM= google.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miEFZTKqfCUM6K7xSMQL9OKL/b6hQv+e19PK+JZNE= google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo= -google.golang.org/protobuf v1.22.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= -google.golang.org/protobuf v1.23.1-0.20200526195155-81db48ad09cc/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= -google.golang.org/protobuf v1.24.0/go.mod h1:r/3tXBNzIEhYS9I1OUVjXDlt8tc493IdKGjtUeSXeh4= -google.golang.org/protobuf v1.25.0/go.mod h1:9JNX74DMeImyA3h4bdi1ymwjUzf21/xIlbajtzgsN7c= google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw= google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc= +google.golang.org/protobuf v1.32.0 h1:pPC6BG5ex8PDFnkbrGU3EixyhKcQ2aDuBS36lqK/C7I= +google.golang.org/protobuf v1.32.0/go.mod h1:c6P6GXX6sHbq/GpV6MGZEdwhWPcYBgnhAHhKbcUYpos= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= -gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= -gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI= gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys= gopkg.in/ini.v1 v1.67.0 h1:Dgnx+6+nfE+IfzjUEISNeydPJh9AXNNsWbGP9KzCsOA= gopkg.in/ini.v1 v1.67.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k= @@ -606,13 +303,3 @@ gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= -honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= -honnef.co/go/tools v0.0.0-20190106161140-3f1c8253044a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= -honnef.co/go/tools v0.0.0-20190418001031-e561f6794a2a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= -honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= -honnef.co/go/tools v0.0.1-2019.2.3/go.mod h1:a3bituU0lyd329TUQxRnasdCoJDkEUEAqEt0JzvZhAg= -honnef.co/go/tools v0.0.1-2020.1.3/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k= -honnef.co/go/tools v0.0.1-2020.1.4/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k= -rsc.io/binaryregexp v0.2.0/go.mod h1:qTv7/COck+e2FymRvadv62gMdZztPaShugOCi3I+8D8= -rsc.io/quote/v3 v3.1.0/go.mod h1:yEA65RcK8LyAZtP9Kv3t0HmxON59tX3rD+tICJqUlj0= -rsc.io/sampler v1.3.0/go.mod h1:T1hPZKmBbMNahiBKFy5HrXp6adAjACjK9JXDnKaTXpA= diff --git a/internal/api/members/bluk.go b/server/internal/api/members/bluk.go similarity index 100% rename from internal/api/members/bluk.go rename to server/internal/api/members/bluk.go diff --git a/internal/api/members/controler.go b/server/internal/api/members/controler.go similarity index 100% rename from internal/api/members/controler.go rename to server/internal/api/members/controler.go diff --git a/internal/api/members/handler.go b/server/internal/api/members/handler.go similarity index 100% rename from internal/api/members/handler.go rename to server/internal/api/members/handler.go diff --git a/internal/api/room/broadcast.go b/server/internal/api/room/broadcast.go similarity index 89% rename from internal/api/room/broadcast.go rename to server/internal/api/room/broadcast.go index 75e047c0..5f8319ee 100644 --- a/internal/api/room/broadcast.go +++ b/server/internal/api/room/broadcast.go @@ -22,7 +22,7 @@ func (h *RoomHandler) broadcastStatus(w http.ResponseWriter, r *http.Request) er }) } -func (h *RoomHandler) boradcastStart(w http.ResponseWriter, r *http.Request) error { +func (h *RoomHandler) broadcastStart(w http.ResponseWriter, r *http.Request) error { data := &BroadcastStatusPayload{} if err := utils.HttpJsonRequest(w, r, data); err != nil { return err @@ -42,7 +42,7 @@ func (h *RoomHandler) boradcastStart(w http.ResponseWriter, r *http.Request) err } h.sessions.AdminBroadcast( - event.BORADCAST_STATUS, + event.BROADCAST_STATUS, message.BroadcastStatus{ IsActive: broadcast.Started(), URL: broadcast.Url(), @@ -51,7 +51,7 @@ func (h *RoomHandler) boradcastStart(w http.ResponseWriter, r *http.Request) err return utils.HttpSuccess(w) } -func (h *RoomHandler) boradcastStop(w http.ResponseWriter, r *http.Request) error { +func (h *RoomHandler) broadcastStop(w http.ResponseWriter, r *http.Request) error { broadcast := h.capture.Broadcast() if !broadcast.Started() { return utils.HttpUnprocessableEntity("server is not broadcasting") @@ -60,7 +60,7 @@ func (h *RoomHandler) boradcastStop(w http.ResponseWriter, r *http.Request) erro broadcast.Stop() h.sessions.AdminBroadcast( - event.BORADCAST_STATUS, + event.BROADCAST_STATUS, message.BroadcastStatus{ IsActive: broadcast.Started(), URL: broadcast.Url(), diff --git a/internal/api/room/clipboard.go b/server/internal/api/room/clipboard.go similarity index 100% rename from internal/api/room/clipboard.go rename to server/internal/api/room/clipboard.go diff --git a/internal/api/room/control.go b/server/internal/api/room/control.go similarity index 76% rename from internal/api/room/control.go rename to server/internal/api/room/control.go index ddf04346..133af610 100644 --- a/internal/api/room/control.go +++ b/server/internal/api/room/control.go @@ -6,6 +6,8 @@ import ( "github.com/go-chi/chi" "github.com/demodesk/neko/pkg/auth" + "github.com/demodesk/neko/pkg/types/event" + "github.com/demodesk/neko/pkg/types/message" "github.com/demodesk/neko/pkg/utils" ) @@ -33,17 +35,26 @@ func (h *RoomHandler) controlStatus(w http.ResponseWriter, r *http.Request) erro } func (h *RoomHandler) controlRequest(w http.ResponseWriter, r *http.Request) error { - _, hasHost := h.sessions.GetHost() + session, _ := auth.GetSession(r) + host, hasHost := h.sessions.GetHost() if hasHost { - return utils.HttpUnprocessableEntity("there is already a host") + // TODO: Some throttling mechanism to prevent spamming. + + // let host know that someone wants to take control + host.Send( + event.CONTROL_REQUEST, + message.SessionID{ + ID: session.ID(), + }) + + return utils.HttpError(http.StatusAccepted, "control request sent") } - session, _ := auth.GetSession(r) if h.sessions.Settings().LockedControls && !session.Profile().IsAdmin { return utils.HttpForbidden("controls are locked") } - h.sessions.SetHost(session) + session.SetAsHost() return utils.HttpSuccess(w) } @@ -55,19 +66,20 @@ func (h *RoomHandler) controlRelease(w http.ResponseWriter, r *http.Request) err } h.desktop.ResetKeys() - h.sessions.ClearHost() + session.ClearHost() return utils.HttpSuccess(w) } func (h *RoomHandler) controlTake(w http.ResponseWriter, r *http.Request) error { session, _ := auth.GetSession(r) - h.sessions.SetHost(session) + session.SetAsHost() return utils.HttpSuccess(w) } func (h *RoomHandler) controlGive(w http.ResponseWriter, r *http.Request) error { + session, _ := auth.GetSession(r) sessionId := chi.URLParam(r, "sessionId") target, ok := h.sessions.Get(sessionId) @@ -79,17 +91,18 @@ func (h *RoomHandler) controlGive(w http.ResponseWriter, r *http.Request) error return utils.HttpBadRequest("target session is not allowed to host") } - h.sessions.SetHost(target) + target.SetAsHostBy(session) return utils.HttpSuccess(w) } func (h *RoomHandler) controlReset(w http.ResponseWriter, r *http.Request) error { + session, _ := auth.GetSession(r) _, hasHost := h.sessions.GetHost() if hasHost { h.desktop.ResetKeys() - h.sessions.ClearHost() + session.ClearHost() } return utils.HttpSuccess(w) diff --git a/internal/api/room/handler.go b/server/internal/api/room/handler.go similarity index 93% rename from internal/api/room/handler.go rename to server/internal/api/room/handler.go index ba33d915..75ca9a92 100644 --- a/internal/api/room/handler.go +++ b/server/internal/api/room/handler.go @@ -31,7 +31,7 @@ func New( } // generate fallback image for private mode when needed - sessions.OnSettingsChanged(func(new types.Settings, old types.Settings) { + sessions.OnSettingsChanged(func(session types.Session, new, old types.Settings) { if old.PrivateMode && !new.PrivateMode { log.Debug().Msg("clearing private mode fallback image") h.privateModeImage = nil @@ -62,8 +62,8 @@ func (h *RoomHandler) Route(r types.Router) { r.With(auth.AdminsOnly).Route("/broadcast", func(r types.Router) { r.Get("/", h.broadcastStatus) - r.Post("/start", h.boradcastStart) - r.Post("/stop", h.boradcastStop) + r.Post("/start", h.broadcastStart) + r.Post("/stop", h.broadcastStop) }) r.With(auth.CanAccessClipboardOnly).With(auth.HostsOnly).Route("/clipboard", func(r types.Router) { @@ -95,7 +95,7 @@ func (h *RoomHandler) Route(r types.Router) { r.Post("/release", h.controlRelease) r.With(auth.AdminsOnly).Post("/take", h.controlTake) - r.With(auth.AdminsOnly).Post("/give/{sessionId}", h.controlGive) + r.With(auth.HostsOrAdminsOnly).Post("/give/{sessionId}", h.controlGive) r.With(auth.AdminsOnly).Post("/reset", h.controlReset) }) diff --git a/internal/api/room/keyboard.go b/server/internal/api/room/keyboard.go similarity index 54% rename from internal/api/room/keyboard.go rename to server/internal/api/room/keyboard.go index a8115c08..f3765f56 100644 --- a/internal/api/room/keyboard.go +++ b/server/internal/api/room/keyboard.go @@ -7,21 +7,13 @@ import ( "github.com/demodesk/neko/pkg/utils" ) -type KeyboardMapData struct { - types.KeyboardMap -} - -type KeyboardModifiersData struct { - types.KeyboardModifiers -} - func (h *RoomHandler) keyboardMapSet(w http.ResponseWriter, r *http.Request) error { - data := &KeyboardMapData{} - if err := utils.HttpJsonRequest(w, r, data); err != nil { + keyboardMap := types.KeyboardMap{} + if err := utils.HttpJsonRequest(w, r, &keyboardMap); err != nil { return err } - err := h.desktop.SetKeyboardMap(data.KeyboardMap) + err := h.desktop.SetKeyboardMap(keyboardMap) if err != nil { return utils.HttpInternalServerError().WithInternalErr(err) } @@ -30,28 +22,26 @@ func (h *RoomHandler) keyboardMapSet(w http.ResponseWriter, r *http.Request) err } func (h *RoomHandler) keyboardMapGet(w http.ResponseWriter, r *http.Request) error { - data, err := h.desktop.GetKeyboardMap() + keyboardMap, err := h.desktop.GetKeyboardMap() if err != nil { return utils.HttpInternalServerError().WithInternalErr(err) } - return utils.HttpSuccess(w, KeyboardMapData{ - KeyboardMap: *data, - }) + return utils.HttpSuccess(w, keyboardMap) } func (h *RoomHandler) keyboardModifiersSet(w http.ResponseWriter, r *http.Request) error { - data := &KeyboardModifiersData{} - if err := utils.HttpJsonRequest(w, r, data); err != nil { + keyboardModifiers := types.KeyboardModifiers{} + if err := utils.HttpJsonRequest(w, r, &keyboardModifiers); err != nil { return err } - h.desktop.SetKeyboardModifiers(data.KeyboardModifiers) + h.desktop.SetKeyboardModifiers(keyboardModifiers) return utils.HttpSuccess(w) } func (h *RoomHandler) keyboardModifiersGet(w http.ResponseWriter, r *http.Request) error { - return utils.HttpSuccess(w, KeyboardModifiersData{ - KeyboardModifiers: h.desktop.GetKeyboardModifiers(), - }) + keyboardModifiers := h.desktop.GetKeyboardModifiers() + + return utils.HttpSuccess(w, keyboardModifiers) } diff --git a/internal/api/room/screen.go b/server/internal/api/room/screen.go similarity index 76% rename from internal/api/room/screen.go rename to server/internal/api/room/screen.go index 174f3477..12124bdc 100644 --- a/internal/api/room/screen.go +++ b/server/internal/api/room/screen.go @@ -11,24 +11,16 @@ import ( "github.com/demodesk/neko/pkg/utils" ) -type ScreenConfigurationPayload struct { - Width int `json:"width"` - Height int `json:"height"` - Rate int16 `json:"rate"` -} - func (h *RoomHandler) screenConfiguration(w http.ResponseWriter, r *http.Request) error { - size := h.desktop.GetScreenSize() + screenSize := h.desktop.GetScreenSize() - return utils.HttpSuccess(w, ScreenConfigurationPayload{ - Width: size.Width, - Height: size.Height, - Rate: size.Rate, - }) + return utils.HttpSuccess(w, screenSize) } func (h *RoomHandler) screenConfigurationChange(w http.ResponseWriter, r *http.Request) error { - data := &ScreenConfigurationPayload{} + auth, _ := auth.GetSession(r) + + data := &types.ScreenSize{} if err := utils.HttpJsonRequest(w, r, data); err != nil { return err } @@ -43,10 +35,9 @@ func (h *RoomHandler) screenConfigurationChange(w http.ResponseWriter, r *http.R return utils.HttpUnprocessableEntity("cannot set screen size").WithInternalErr(err) } - h.sessions.Broadcast(event.SCREEN_UPDATED, message.ScreenSize{ - Width: size.Width, - Height: size.Height, - Rate: size.Rate, + h.sessions.Broadcast(event.SCREEN_UPDATED, message.ScreenSizeUpdate{ + ID: auth.ID(), + ScreenSize: size, }) return utils.HttpSuccess(w, data) @@ -56,16 +47,7 @@ func (h *RoomHandler) screenConfigurationChange(w http.ResponseWriter, r *http.R func (h *RoomHandler) screenConfigurationsList(w http.ResponseWriter, r *http.Request) error { configurations := h.desktop.ScreenConfigurations() - list := make([]ScreenConfigurationPayload, 0, len(configurations)) - for _, conf := range configurations { - list = append(list, ScreenConfigurationPayload{ - Width: conf.Width, - Height: conf.Height, - Rate: conf.Rate, - }) - } - - return utils.HttpSuccess(w, list) + return utils.HttpSuccess(w, configurations) } func (h *RoomHandler) screenShotGet(w http.ResponseWriter, r *http.Request) error { diff --git a/server/internal/api/room/settings.go b/server/internal/api/room/settings.go new file mode 100644 index 00000000..c8e1816a --- /dev/null +++ b/server/internal/api/room/settings.go @@ -0,0 +1,38 @@ +package room + +import ( + "encoding/json" + "io" + "net/http" + + "github.com/demodesk/neko/pkg/auth" + "github.com/demodesk/neko/pkg/types" + "github.com/demodesk/neko/pkg/utils" +) + +func (h *RoomHandler) settingsGet(w http.ResponseWriter, r *http.Request) error { + settings := h.sessions.Settings() + return utils.HttpSuccess(w, settings) +} + +func (h *RoomHandler) settingsSet(w http.ResponseWriter, r *http.Request) error { + session, _ := auth.GetSession(r) + + // We read the request body first and unmashal it inside the UpdateSettingsFunc + // to ensure atomicity of the operation. + body, err := io.ReadAll(r.Body) + if err != nil { + return utils.HttpBadRequest("unable to read request body").WithInternalErr(err) + } + + h.sessions.UpdateSettingsFunc(session, func(settings *types.Settings) bool { + err = json.Unmarshal(body, settings) + return err == nil + }) + + if err != nil { + return utils.HttpBadRequest("unable to parse provided data").WithInternalErr(err) + } + + return utils.HttpSuccess(w) +} diff --git a/internal/api/room/upload.go b/server/internal/api/room/upload.go similarity index 100% rename from internal/api/room/upload.go rename to server/internal/api/room/upload.go diff --git a/internal/api/router.go b/server/internal/api/router.go similarity index 91% rename from internal/api/router.go rename to server/internal/api/router.go index dbe877a9..24ada419 100644 --- a/internal/api/router.go +++ b/server/internal/api/router.go @@ -7,6 +7,7 @@ import ( "github.com/demodesk/neko/internal/api/members" "github.com/demodesk/neko/internal/api/room" + "github.com/demodesk/neko/internal/api/sessions" "github.com/demodesk/neko/pkg/auth" "github.com/demodesk/neko/pkg/types" "github.com/demodesk/neko/pkg/utils" @@ -45,7 +46,10 @@ func (api *ApiManagerCtx) Route(r types.Router) { r.Post("/logout", api.Logout) r.Get("/whoami", api.Whoami) - r.Get("/sessions", api.Sessions) + r.Post("/profile", api.UpdateProfile) + + sessionsHandler := sessions.New(api.sessions) + r.Route("/sessions", sessionsHandler.Route) membersHandler := members.New(api.members) r.Route("/members", membersHandler.Route) diff --git a/internal/api/session.go b/server/internal/api/session.go similarity index 77% rename from internal/api/session.go rename to server/internal/api/session.go index e1991ad6..fbe85733 100644 --- a/internal/api/session.go +++ b/server/internal/api/session.go @@ -33,6 +33,8 @@ func (api *ApiManagerCtx) Login(w http.ResponseWriter, r *http.Request) error { return utils.HttpUnprocessableEntity("session already connected") } else if errors.Is(err, types.ErrMemberDoesNotExist) || errors.Is(err, types.ErrMemberInvalidPassword) { return utils.HttpUnauthorized().WithInternalErr(err) + } else if errors.Is(err, types.ErrSessionLoginsLocked) { + return utils.HttpForbidden("logins are locked").WithInternalErr(err) } else { return utils.HttpInternalServerError().WithInternalErr(err) } @@ -82,15 +84,22 @@ func (api *ApiManagerCtx) Whoami(w http.ResponseWriter, r *http.Request) error { }) } -func (api *ApiManagerCtx) Sessions(w http.ResponseWriter, r *http.Request) error { - sessions := []SessionDataPayload{} - for _, session := range api.sessions.List() { - sessions = append(sessions, SessionDataPayload{ - ID: session.ID(), - Profile: session.Profile(), - State: session.State(), - }) +func (api *ApiManagerCtx) UpdateProfile(w http.ResponseWriter, r *http.Request) error { + session, _ := auth.GetSession(r) + + data := session.Profile() + if err := utils.HttpJsonRequest(w, r, &data); err != nil { + return err } - return utils.HttpSuccess(w, sessions) + err := api.sessions.Update(session.ID(), data) + if err != nil { + if errors.Is(err, types.ErrSessionNotFound) { + return utils.HttpBadRequest("session does not exist") + } else { + return utils.HttpInternalServerError().WithInternalErr(err) + } + } + + return utils.HttpSuccess(w, true) } diff --git a/server/internal/api/sessions/controller.go b/server/internal/api/sessions/controller.go new file mode 100644 index 00000000..c4ce44c0 --- /dev/null +++ b/server/internal/api/sessions/controller.go @@ -0,0 +1,80 @@ +package sessions + +import ( + "errors" + "net/http" + + "github.com/demodesk/neko/pkg/auth" + "github.com/demodesk/neko/pkg/types" + "github.com/demodesk/neko/pkg/utils" + "github.com/go-chi/chi" +) + +type SessionDataPayload struct { + ID string `json:"id"` + Profile types.MemberProfile `json:"profile"` + State types.SessionState `json:"state"` +} + +func (h *SessionsHandler) sessionsList(w http.ResponseWriter, r *http.Request) error { + sessions := []SessionDataPayload{} + for _, session := range h.sessions.List() { + sessions = append(sessions, SessionDataPayload{ + ID: session.ID(), + Profile: session.Profile(), + State: session.State(), + }) + } + + return utils.HttpSuccess(w, sessions) +} + +func (h *SessionsHandler) sessionsRead(w http.ResponseWriter, r *http.Request) error { + sessionId := chi.URLParam(r, "sessionId") + + session, ok := h.sessions.Get(sessionId) + if !ok { + return utils.HttpNotFound("session not found") + } + + return utils.HttpSuccess(w, SessionDataPayload{ + ID: session.ID(), + Profile: session.Profile(), + State: session.State(), + }) +} + +func (h *SessionsHandler) sessionsDelete(w http.ResponseWriter, r *http.Request) error { + session, _ := auth.GetSession(r) + + sessionId := chi.URLParam(r, "sessionId") + if sessionId == session.ID() { + return utils.HttpBadRequest("cannot delete own session") + } + + err := h.sessions.Delete(sessionId) + if err != nil { + if errors.Is(err, types.ErrSessionNotFound) { + return utils.HttpBadRequest("session not found") + } else { + return utils.HttpInternalServerError().WithInternalErr(err) + } + } + + return utils.HttpSuccess(w) +} + +func (h *SessionsHandler) sessionsDisconnect(w http.ResponseWriter, r *http.Request) error { + sessionId := chi.URLParam(r, "sessionId") + + err := h.sessions.Disconnect(sessionId) + if err != nil { + if errors.Is(err, types.ErrSessionNotFound) { + return utils.HttpBadRequest("session not found") + } else { + return utils.HttpInternalServerError().WithInternalErr(err) + } + } + + return utils.HttpSuccess(w) +} diff --git a/server/internal/api/sessions/handler.go b/server/internal/api/sessions/handler.go new file mode 100644 index 00000000..5f5b7711 --- /dev/null +++ b/server/internal/api/sessions/handler.go @@ -0,0 +1,30 @@ +package sessions + +import ( + "github.com/demodesk/neko/pkg/auth" + "github.com/demodesk/neko/pkg/types" +) + +type SessionsHandler struct { + sessions types.SessionManager +} + +func New( + sessions types.SessionManager, +) *SessionsHandler { + // Init + + return &SessionsHandler{ + sessions: sessions, + } +} + +func (h *SessionsHandler) Route(r types.Router) { + r.Get("/", h.sessionsList) + + r.With(auth.AdminsOnly).Route("/{sessionId}", func(r types.Router) { + r.Get("/", h.sessionsRead) + r.Delete("/", h.sessionsDelete) + r.Post("/disconnect", h.sessionsDisconnect) + }) +} diff --git a/server/internal/capture/broadcast.go b/server/internal/capture/broadcast.go index fc6727e1..0b056c02 100644 --- a/server/internal/capture/broadcast.go +++ b/server/internal/capture/broadcast.go @@ -3,26 +3,32 @@ package capture import ( "sync" + "github.com/prometheus/client_golang/prometheus" + "github.com/prometheus/client_golang/prometheus/promauto" "github.com/rs/zerolog" "github.com/rs/zerolog/log" - "m1k1o/neko/internal/capture/gst" - "m1k1o/neko/internal/types" + "github.com/demodesk/neko/pkg/gst" + "github.com/demodesk/neko/pkg/types" ) type BroacastManagerCtx struct { logger zerolog.Logger mu sync.Mutex - pipeline *gst.Pipeline + pipeline gst.Pipeline pipelineMu sync.Mutex pipelineFn func(url string) (string, error) url string started bool + + // metrics + pipelinesCounter prometheus.Counter + pipelinesActive prometheus.Gauge } -func broadcastNew(pipelineFn func(url string) (string, error), url string, started bool) *BroacastManagerCtx { +func broadcastNew(pipelineFn func(url string) (string, error), defaultUrl string, autostart bool) *BroacastManagerCtx { logger := log.With(). Str("module", "capture"). Str("submodule", "broadcast"). @@ -31,8 +37,34 @@ func broadcastNew(pipelineFn func(url string) (string, error), url string, start return &BroacastManagerCtx{ logger: logger, pipelineFn: pipelineFn, - url: url, - started: started && url != "", + url: defaultUrl, + started: defaultUrl != "" && autostart, + + // metrics + pipelinesCounter: promauto.NewCounter(prometheus.CounterOpts{ + Name: "pipelines_total", + Namespace: "neko", + Subsystem: "capture", + Help: "Total number of created pipelines.", + ConstLabels: map[string]string{ + "submodule": "broadcast", + "video_id": "main", + "codec_name": "-", + "codec_type": "-", + }, + }), + pipelinesActive: promauto.NewGauge(prometheus.GaugeOpts{ + Name: "pipelines_active", + Namespace: "neko", + Subsystem: "capture", + Help: "Total number of active pipelines.", + ConstLabels: map[string]string{ + "submodule": "broadcast", + "video_id": "main", + "codec_name": "-", + "codec_type": "-", + }, + }), } } @@ -86,7 +118,6 @@ func (manager *BroacastManagerCtx) createPipeline() error { return types.ErrCapturePipelineAlreadyExists } - var err error pipelineStr, err := manager.pipelineFn(manager.url) if err != nil { return err @@ -103,6 +134,8 @@ func (manager *BroacastManagerCtx) createPipeline() error { } manager.pipeline.Play() + manager.pipelinesCounter.Inc() + manager.pipelinesActive.Set(1) return nil } @@ -118,4 +151,6 @@ func (manager *BroacastManagerCtx) destroyPipeline() { manager.pipeline.Destroy() manager.logger.Info().Msgf("destroying pipeline") manager.pipeline = nil + + manager.pipelinesActive.Set(0) } diff --git a/server/internal/capture/gst/gst.c b/server/internal/capture/gst/gst.c deleted file mode 100644 index 6f903234..00000000 --- a/server/internal/capture/gst/gst.c +++ /dev/null @@ -1,202 +0,0 @@ -#include "gst.h" - -static void gstreamer_pipeline_log(GstPipelineCtx *ctx, char* level, const char* format, ...) { - va_list argptr; - va_start(argptr, format); - char buffer[4096]; - vsnprintf(buffer, sizeof(buffer), format, argptr); - va_end(argptr); - goPipelineLog(level, buffer, ctx->pipelineId); -} - -static gboolean gstreamer_bus_call(GstBus *bus, GstMessage *msg, gpointer user_data) { - GstPipelineCtx *ctx = (GstPipelineCtx *)user_data; - - switch (GST_MESSAGE_TYPE(msg)) { - case GST_MESSAGE_EOS: { - gstreamer_pipeline_log(ctx, "fatal", "end of stream"); - break; - } - - case GST_MESSAGE_STATE_CHANGED: { - GstState old_state, new_state; - gst_message_parse_state_changed(msg, &old_state, &new_state, NULL); - - gstreamer_pipeline_log(ctx, "debug", - "element %s changed state from %s to %s", - GST_OBJECT_NAME(msg->src), - gst_element_state_get_name(old_state), - gst_element_state_get_name(new_state)); - break; - } - - case GST_MESSAGE_TAG: { - GstTagList *tags = NULL; - gst_message_parse_tag(msg, &tags); - - gstreamer_pipeline_log(ctx, "debug", - "got tags from element %s", - GST_OBJECT_NAME(msg->src)); - - gst_tag_list_unref(tags); - break; - } - - case GST_MESSAGE_ERROR: { - GError *err = NULL; - gchar *dbg_info = NULL; - gst_message_parse_error(msg, &err, &dbg_info); - - gstreamer_pipeline_log(ctx, "error", - "error from element %s: %s", - GST_OBJECT_NAME(msg->src), err->message); - gstreamer_pipeline_log(ctx, "warn", - "debugging info: %s", - (dbg_info) ? dbg_info : "none"); - - g_error_free(err); - g_free(dbg_info); - break; - } - - default: - gstreamer_pipeline_log(ctx, "trace", "unknown message"); - break; - } - - return TRUE; -} - -GstPipelineCtx *gstreamer_pipeline_create(char *pipelineStr, int pipelineId, GError **error) { - GstElement *pipeline = gst_parse_launch(pipelineStr, error); - if (pipeline == NULL) return NULL; - - // create gstreamer pipeline context - GstPipelineCtx *ctx = calloc(1, sizeof(GstPipelineCtx)); - ctx->pipelineId = pipelineId; - ctx->pipeline = pipeline; - - GstBus *bus = gst_pipeline_get_bus(GST_PIPELINE(pipeline)); - gst_bus_add_watch(bus, gstreamer_bus_call, ctx); - gst_object_unref(bus); - - return ctx; -} - -static GstFlowReturn gstreamer_send_new_sample_handler(GstElement *object, gpointer user_data) { - GstPipelineCtx *ctx = (GstPipelineCtx *)user_data; - GstSample *sample = NULL; - GstBuffer *buffer = NULL; - gpointer copy = NULL; - gsize copy_size = 0; - - g_signal_emit_by_name(object, "pull-sample", &sample); - if (sample) { - buffer = gst_sample_get_buffer(sample); - if (buffer) { - gst_buffer_extract_dup(buffer, 0, gst_buffer_get_size(buffer), ©, ©_size); - goHandlePipelineBuffer(copy, copy_size, GST_BUFFER_DURATION(buffer), ctx->pipelineId); - } - gst_sample_unref(sample); - } - - return GST_FLOW_OK; -} - -void gstreamer_pipeline_attach_appsink(GstPipelineCtx *ctx, char *sinkName) { - ctx->appsink = gst_bin_get_by_name(GST_BIN(ctx->pipeline), sinkName); - g_object_set(ctx->appsink, "emit-signals", TRUE, NULL); - g_signal_connect(ctx->appsink, "new-sample", G_CALLBACK(gstreamer_send_new_sample_handler), ctx); -} - -void gstreamer_pipeline_attach_appsrc(GstPipelineCtx *ctx, char *srcName) { - ctx->appsrc = gst_bin_get_by_name(GST_BIN(ctx->pipeline), srcName); -} - -void gstreamer_pipeline_play(GstPipelineCtx *ctx) { - gst_element_set_state(GST_ELEMENT(ctx->pipeline), GST_STATE_PLAYING); -} - -void gstreamer_pipeline_pause(GstPipelineCtx *ctx) { - gst_element_set_state(GST_ELEMENT(ctx->pipeline), GST_STATE_PAUSED); -} - -void gstreamer_pipeline_destory(GstPipelineCtx *ctx) { - // end appsrc, if exists - if (ctx->appsrc) { - gst_app_src_end_of_stream(GST_APP_SRC(ctx->appsrc)); - } - - // send pipeline eos - gst_element_send_event(GST_ELEMENT(ctx->pipeline), gst_event_new_eos()); - - // set null state - gst_element_set_state(GST_ELEMENT(ctx->pipeline), GST_STATE_NULL); - - if (ctx->appsink) { - gst_object_unref(ctx->appsink); - ctx->appsink = NULL; - } - - if (ctx->appsrc) { - gst_object_unref(ctx->appsrc); - ctx->appsrc = NULL; - } - - gst_object_unref(ctx->pipeline); -} - -void gstreamer_pipeline_push(GstPipelineCtx *ctx, void *buffer, int bufferLen) { - if (ctx->appsrc != NULL) { - gpointer p = g_memdup(buffer, bufferLen); - GstBuffer *buffer = gst_buffer_new_wrapped(p, bufferLen); - gst_app_src_push_buffer(GST_APP_SRC(ctx->appsrc), buffer); - } -} - -gboolean gstreamer_pipeline_set_prop_int(GstPipelineCtx *ctx, char *binName, char *prop, gint value) { - GstElement *el = gst_bin_get_by_name(GST_BIN(ctx->pipeline), binName); - if (el == NULL) return FALSE; - - g_object_set(G_OBJECT(el), - prop, value, - NULL); - - gst_object_unref(el); - return TRUE; -} - -gboolean gstreamer_pipeline_set_caps_framerate(GstPipelineCtx *ctx, const gchar* binName, gint numerator, gint denominator) { - GstElement *el = gst_bin_get_by_name(GST_BIN(ctx->pipeline), binName); - if (el == NULL) return FALSE; - - GstCaps *caps = gst_caps_new_simple("video/x-raw", - "framerate", GST_TYPE_FRACTION, numerator, denominator, - NULL); - - g_object_set(G_OBJECT(el), - "caps", caps, - NULL); - - gst_caps_unref(caps); - gst_object_unref(el); - return TRUE; -} - -gboolean gstreamer_pipeline_set_caps_resolution(GstPipelineCtx *ctx, const gchar* binName, gint width, gint height) { - GstElement *el = gst_bin_get_by_name(GST_BIN(ctx->pipeline), binName); - if (el == NULL) return FALSE; - - GstCaps *caps = gst_caps_new_simple("video/x-raw", - "width", G_TYPE_INT, width, - "height", G_TYPE_INT, height, - NULL); - - g_object_set(G_OBJECT(el), - "caps", caps, - NULL); - - gst_caps_unref(caps); - gst_object_unref(el); - return TRUE; -} diff --git a/server/internal/capture/gst/gst.go b/server/internal/capture/gst/gst.go deleted file mode 100644 index 7731d2e7..00000000 --- a/server/internal/capture/gst/gst.go +++ /dev/null @@ -1,221 +0,0 @@ -package gst - -/* -#cgo pkg-config: gstreamer-1.0 gstreamer-app-1.0 - -#include "gst.h" -*/ -import "C" -import ( - "fmt" - "sync" - "sync/atomic" - "time" - "unsafe" - - "github.com/rs/zerolog" - "github.com/rs/zerolog/log" - - "m1k1o/neko/internal/types" -) - -type Pipeline struct { - id int - logger zerolog.Logger - Src string - Ctx *C.GstPipelineCtx - Sample chan types.Sample -} - -var pSerial int32 -var pipelines = make(map[int]*Pipeline) -var pipelinesLock sync.Mutex -var registry *C.GstRegistry -var gMainLoop *C.GMainLoop - -func init() { - C.gst_init(nil, nil) - registry = C.gst_registry_get() -} - -func RunMainLoop() { - if gMainLoop != nil { - return - } - gMainLoop = C.g_main_loop_new(nil, C.int(0)) - C.g_main_loop_run(gMainLoop) -} - -func QuitMainLoop() { - if gMainLoop == nil { - return - } - C.g_main_loop_quit(gMainLoop) - gMainLoop = nil -} - -func CreatePipeline(pipelineStr string) (*Pipeline, error) { - id := atomic.AddInt32(&pSerial, 1) - - pipelineStrUnsafe := C.CString(pipelineStr) - defer C.free(unsafe.Pointer(pipelineStrUnsafe)) - - pipelinesLock.Lock() - defer pipelinesLock.Unlock() - - var gstError *C.GError - ctx := C.gstreamer_pipeline_create(pipelineStrUnsafe, C.int(id), &gstError) - - if gstError != nil { - defer C.g_error_free(gstError) - fmt.Printf("(pipeline error) %s", C.GoString(gstError.message)) - return nil, fmt.Errorf("(pipeline error) %s", C.GoString(gstError.message)) - } - - p := &Pipeline{ - id: int(id), - logger: log.With(). - Str("module", "capture"). - Str("submodule", "gstreamer"). - Int("pipeline_id", int(id)).Logger(), - Src: pipelineStr, - Ctx: ctx, - } - - pipelines[p.id] = p - return p, nil -} - -func (p *Pipeline) AttachAppsink(sinkName string, sampleChannel chan types.Sample) { - sinkNameUnsafe := C.CString(sinkName) - defer C.free(unsafe.Pointer(sinkNameUnsafe)) - - p.Sample = sampleChannel - - C.gstreamer_pipeline_attach_appsink(p.Ctx, sinkNameUnsafe) -} - -func (p *Pipeline) AttachAppsrc(srcName string) { - srcNameUnsafe := C.CString(srcName) - defer C.free(unsafe.Pointer(srcNameUnsafe)) - - C.gstreamer_pipeline_attach_appsrc(p.Ctx, srcNameUnsafe) -} - -func (p *Pipeline) Play() { - C.gstreamer_pipeline_play(p.Ctx) -} - -func (p *Pipeline) Pause() { - C.gstreamer_pipeline_pause(p.Ctx) -} - -func (p *Pipeline) Destroy() { - C.gstreamer_pipeline_destory(p.Ctx) - - pipelinesLock.Lock() - delete(pipelines, p.id) - pipelinesLock.Unlock() - - C.free(unsafe.Pointer(p.Ctx)) - p = nil -} - -func (p *Pipeline) Push(buffer []byte) { - bytes := C.CBytes(buffer) - defer C.free(bytes) - - C.gstreamer_pipeline_push(p.Ctx, bytes, C.int(len(buffer))) -} - -func (p *Pipeline) SetPropInt(binName string, prop string, value int) bool { - cBinName := C.CString(binName) - defer C.free(unsafe.Pointer(cBinName)) - - cProp := C.CString(prop) - defer C.free(unsafe.Pointer(cProp)) - - cValue := C.int(value) - - p.logger.Debug().Msgf("setting prop %s of %s to %d", prop, binName, value) - - ok := C.gstreamer_pipeline_set_prop_int(p.Ctx, cBinName, cProp, cValue) - return ok == C.TRUE -} - -func (p *Pipeline) SetCapsFramerate(binName string, numerator, denominator int) bool { - cBinName := C.CString(binName) - cNumerator := C.int(numerator) - cDenominator := C.int(denominator) - - defer C.free(unsafe.Pointer(cBinName)) - - p.logger.Debug().Msgf("setting caps framerate of %s to %d/%d", binName, numerator, denominator) - - ok := C.gstreamer_pipeline_set_caps_framerate(p.Ctx, cBinName, cNumerator, cDenominator) - return ok == C.TRUE -} - -func (p *Pipeline) SetCapsResolution(binName string, width, height int) bool { - cBinName := C.CString(binName) - cWidth := C.int(width) - cHeight := C.int(height) - - defer C.free(unsafe.Pointer(cBinName)) - - p.logger.Debug().Msgf("setting caps resolution of %s to %dx%d", binName, width, height) - - ok := C.gstreamer_pipeline_set_caps_resolution(p.Ctx, cBinName, cWidth, cHeight) - return ok == C.TRUE -} - -// gst-inspect-1.0 -func CheckPlugins(plugins []string) error { - var plugin *C.GstPlugin - for _, pluginstr := range plugins { - plugincstr := C.CString(pluginstr) - plugin = C.gst_registry_find_plugin(registry, plugincstr) - C.free(unsafe.Pointer(plugincstr)) - if plugin == nil { - return fmt.Errorf("required gstreamer plugin %s not found", pluginstr) - } - } - - return nil -} - -//export goHandlePipelineBuffer -func goHandlePipelineBuffer(buffer unsafe.Pointer, bufferLen C.int, duration C.int, pipelineID C.int) { - defer C.free(buffer) - - pipelinesLock.Lock() - pipeline, ok := pipelines[int(pipelineID)] - pipelinesLock.Unlock() - - if ok { - pipeline.Sample <- types.Sample{ - Data: C.GoBytes(buffer, bufferLen), - Timestamp: time.Now(), - Duration: time.Duration(duration), - } - } else { - log.Warn(). - Str("module", "capture"). - Str("submodule", "gstreamer"). - Int("pipeline_id", int(pipelineID)). - Msgf("discarding sample, pipeline not found") - } -} - -//export goPipelineLog -func goPipelineLog(levelUnsafe *C.char, msgUnsafe *C.char, pipelineID C.int) { - levelStr := C.GoString(levelUnsafe) - msg := C.GoString(msgUnsafe) - - level, _ := zerolog.ParseLevel(levelStr) - log.WithLevel(level). - Str("module", "capture"). - Str("submodule", "gstreamer"). - Int("pipeline_id", int(pipelineID)). - Msg(msg) -} diff --git a/server/internal/capture/gst/gst.h b/server/internal/capture/gst/gst.h deleted file mode 100644 index a562ec34..00000000 --- a/server/internal/capture/gst/gst.h +++ /dev/null @@ -1,27 +0,0 @@ -#pragma once - -#include -#include -#include - -typedef struct GstPipelineCtx { - int pipelineId; - GstElement *pipeline; - GstElement *appsink; - GstElement *appsrc; -} GstPipelineCtx; - -extern void goHandlePipelineBuffer(void *buffer, int bufferLen, int samples, int pipelineId); -extern void goPipelineLog(char *level, char *msg, int pipelineId); - -GstPipelineCtx *gstreamer_pipeline_create(char *pipelineStr, int pipelineId, GError **error); -void gstreamer_pipeline_attach_appsink(GstPipelineCtx *ctx, char *sinkName); -void gstreamer_pipeline_attach_appsrc(GstPipelineCtx *ctx, char *srcName); -void gstreamer_pipeline_play(GstPipelineCtx *ctx); -void gstreamer_pipeline_pause(GstPipelineCtx *ctx); -void gstreamer_pipeline_destory(GstPipelineCtx *ctx); -void gstreamer_pipeline_push(GstPipelineCtx *ctx, void *buffer, int bufferLen); - -gboolean gstreamer_pipeline_set_prop_int(GstPipelineCtx *ctx, char *binName, char *prop, gint value); -gboolean gstreamer_pipeline_set_caps_framerate(GstPipelineCtx *ctx, const gchar* binName, gint numerator, gint denominator); -gboolean gstreamer_pipeline_set_caps_resolution(GstPipelineCtx *ctx, const gchar* binName, gint width, gint height); diff --git a/server/internal/capture/manager.go b/server/internal/capture/manager.go index 0cc48325..f4d004c0 100644 --- a/server/internal/capture/manager.go +++ b/server/internal/capture/manager.go @@ -2,48 +2,189 @@ package capture import ( "errors" + "fmt" + "strings" "github.com/rs/zerolog" "github.com/rs/zerolog/log" - "m1k1o/neko/internal/capture/gst" - "m1k1o/neko/internal/config" - "m1k1o/neko/internal/types" + "github.com/demodesk/neko/internal/config" + "github.com/demodesk/neko/pkg/types" + "github.com/demodesk/neko/pkg/types/codec" ) type CaptureManagerCtx struct { logger zerolog.Logger desktop types.DesktopManager + config *config.Capture // sinks - broadcast *BroacastManagerCtx - audio *StreamSinkManagerCtx - video *StreamSinkManagerCtx + broadcast *BroacastManagerCtx + screencast *ScreencastManagerCtx + audio *StreamSinkManagerCtx + video *StreamSelectorManagerCtx + + // sources + webcam *StreamSrcManagerCtx + microphone *StreamSrcManagerCtx } func New(desktop types.DesktopManager, config *config.Capture) *CaptureManagerCtx { logger := log.With().Str("module", "capture").Logger() + videos := map[string]types.StreamSinkManager{} + for video_id, cnf := range config.VideoPipelines { + pipelineConf := cnf + + createPipeline := func() (string, error) { + if pipelineConf.GstPipeline != "" { + // replace {display} with valid display + return strings.Replace(pipelineConf.GstPipeline, "{display}", config.Display, 1), nil + } + + screen := desktop.GetScreenSize() + pipeline, err := pipelineConf.GetPipeline(screen) + if err != nil { + return "", err + } + + return fmt.Sprintf( + "ximagesrc display-name=%s show-pointer=false use-damage=false "+ + "%s ! appsink name=appsink", config.Display, pipeline, + ), nil + } + + // trigger function to catch evaluation errors at startup + pipeline, err := createPipeline() + if err != nil { + logger.Panic().Err(err). + Str("video_id", video_id). + Msg("failed to create video pipeline") + } + + logger.Info(). + Str("video_id", video_id). + Str("pipeline", pipeline). + Msg("syntax check for video stream pipeline passed") + + // append to videos + videos[video_id] = streamSinkNew(config.VideoCodec, createPipeline, video_id) + } + return &CaptureManagerCtx{ logger: logger, desktop: desktop, + config: config, // sinks broadcast: broadcastNew(func(url string) (string, error) { - return NewBroadcastPipeline(config.AudioDevice, config.Display, config.BroadcastPipeline, url) - }, config.BroadcastUrl, config.BroadcastAutostart), - audio: streamSinkNew(config.AudioCodec, func() (string, error) { - return NewAudioPipeline(config.AudioCodec, config.AudioDevice, config.AudioPipeline, config.AudioBitrate) - }, "audio"), - video: streamSinkNew(config.VideoCodec, func() (string, error) { - // use screen fps as default - fps := desktop.GetScreenSize().Rate - // if max fps is set, cap it to that value - if config.VideoMaxFPS > 0 && config.VideoMaxFPS < fps { - fps = config.VideoMaxFPS + if config.BroadcastPipeline != "" { + var pipeline = config.BroadcastPipeline + // replace {display} with valid display + pipeline = strings.Replace(pipeline, "{display}", config.Display, 1) + // replace {device} with valid device + pipeline = strings.Replace(pipeline, "{device}", config.AudioDevice, 1) + // replace {url} with valid URL + return strings.Replace(pipeline, "{url}", url, 1), nil } - return NewVideoPipeline(config.VideoCodec, config.Display, config.VideoPipeline, fps, config.VideoBitrate, config.VideoHWEnc) - }, "video"), + + return fmt.Sprintf( + "flvmux name=mux ! rtmpsink location='%s live=1' "+ + "pulsesrc device=%s "+ + "! audio/x-raw,channels=2 "+ + "! audioconvert "+ + "! queue "+ + "! voaacenc bitrate=%d "+ + "! mux. "+ + "ximagesrc display-name=%s show-pointer=true use-damage=false "+ + "! video/x-raw "+ + "! videoconvert "+ + "! queue "+ + "! x264enc threads=4 bitrate=%d key-int-max=15 byte-stream=true tune=zerolatency speed-preset=%s "+ + "! mux.", url, config.AudioDevice, config.BroadcastAudioBitrate*1000, config.Display, config.BroadcastVideoBitrate, config.BroadcastPreset, + ), nil + }, config.BroadcastUrl, config.BroadcastAutostart), + screencast: screencastNew(config.ScreencastEnabled, func() string { + if config.ScreencastPipeline != "" { + // replace {display} with valid display + return strings.Replace(config.ScreencastPipeline, "{display}", config.Display, 1) + } + + return fmt.Sprintf( + "ximagesrc display-name=%s show-pointer=true use-damage=false "+ + "! video/x-raw,framerate=%s "+ + "! videoconvert "+ + "! queue "+ + "! jpegenc quality=%s "+ + "! appsink name=appsink", config.Display, config.ScreencastRate, config.ScreencastQuality, + ) + }()), + + audio: streamSinkNew(config.AudioCodec, func() (string, error) { + if config.AudioPipeline != "" { + // replace {device} with valid device + return strings.Replace(config.AudioPipeline, "{device}", config.AudioDevice, 1), nil + } + + return fmt.Sprintf( + "pulsesrc device=%s "+ + "! audio/x-raw,channels=2 "+ + "! audioconvert "+ + "! queue "+ + "! %s "+ + "! appsink name=appsink", config.AudioDevice, config.AudioCodec.Pipeline, + ), nil + }, "audio"), + video: streamSelectorNew(config.VideoCodec, videos, config.VideoIDs), + + // sources + webcam: streamSrcNew(config.WebcamEnabled, map[string]string{ + codec.VP8().Name: "appsrc format=time is-live=true do-timestamp=true name=appsrc " + + fmt.Sprintf("! application/x-rtp, payload=%d, encoding-name=VP8-DRAFT-IETF-01 ", codec.VP8().PayloadType) + + "! rtpvp8depay " + + "! decodebin " + + "! videoconvert " + + "! videorate " + + "! videoscale " + + fmt.Sprintf("! video/x-raw,width=%d,height=%d ", config.WebcamWidth, config.WebcamHeight) + + "! identity drop-allocation=true " + + fmt.Sprintf("! v4l2sink sync=false device=%s", config.WebcamDevice), + // TODO: Test this pipeline. + codec.VP9().Name: "appsrc format=time is-live=true do-timestamp=true name=appsrc " + + "! application/x-rtp " + + "! rtpvp9depay " + + "! decodebin " + + "! videoconvert " + + "! videorate " + + "! videoscale " + + fmt.Sprintf("! video/x-raw,width=%d,height=%d ", config.WebcamWidth, config.WebcamHeight) + + "! identity drop-allocation=true " + + fmt.Sprintf("! v4l2sink sync=false device=%s", config.WebcamDevice), + // TODO: Test this pipeline. + codec.H264().Name: "appsrc format=time is-live=true do-timestamp=true name=appsrc " + + "! application/x-rtp " + + "! rtph264depay " + + "! decodebin " + + "! videoconvert " + + "! videorate " + + "! videoscale " + + fmt.Sprintf("! video/x-raw,width=%d,height=%d ", config.WebcamWidth, config.WebcamHeight) + + "! identity drop-allocation=true " + + fmt.Sprintf("! v4l2sink sync=false device=%s", config.WebcamDevice), + }, "webcam"), + microphone: streamSrcNew(config.MicrophoneEnabled, map[string]string{ + codec.Opus().Name: "appsrc format=time is-live=true do-timestamp=true name=appsrc " + + fmt.Sprintf("! application/x-rtp, payload=%d, encoding-name=OPUS ", codec.Opus().PayloadType) + + "! rtpopusdepay " + + "! decodebin " + + fmt.Sprintf("! pulsesink device=%s", config.MicrophoneDevice), + // TODO: Test this pipeline. + codec.G722().Name: "appsrc format=time is-live=true do-timestamp=true name=appsrc " + + "! application/x-rtp clock-rate=8000 " + + "! rtpg722depay " + + "! decodebin " + + fmt.Sprintf("! pulsesink device=%s", config.MicrophoneDevice), + }, "microphone"), } } @@ -54,55 +195,51 @@ func (manager *CaptureManagerCtx) Start() { } } - go gst.RunMainLoop() - go func() { - for { - before, ok := <-manager.desktop.GetScreenSizeChangeChannel() - if !ok { - manager.logger.Info().Msg("screen size change channel was closed") - return - } + manager.desktop.OnBeforeScreenSizeChange(func() { + manager.video.destroyPipelines() - if before { - // before screen size change, we need to destroy all pipelines + if manager.broadcast.Started() { + manager.broadcast.destroyPipeline() + } - if manager.video.Started() { - manager.video.destroyPipeline() - } + if manager.screencast.Started() { + manager.screencast.destroyPipeline() + } + }) - if manager.broadcast.Started() { - manager.broadcast.destroyPipeline() - } - } else { - // after screen size change, we need to recreate all pipelines + manager.desktop.OnAfterScreenSizeChange(func() { + err := manager.video.recreatePipelines() + if err != nil { + manager.logger.Panic().Err(err).Msg("unable to recreate video pipelines") + } - if manager.video.Started() { - err := manager.video.createPipeline() - if err != nil && !errors.Is(err, types.ErrCapturePipelineAlreadyExists) { - manager.logger.Panic().Err(err).Msg("unable to recreate video pipeline") - } - } - - if manager.broadcast.Started() { - err := manager.broadcast.createPipeline() - if err != nil && !errors.Is(err, types.ErrCapturePipelineAlreadyExists) { - manager.logger.Panic().Err(err).Msg("unable to recreate broadcast pipeline") - } - } + if manager.broadcast.Started() { + err := manager.broadcast.createPipeline() + if err != nil && !errors.Is(err, types.ErrCapturePipelineAlreadyExists) { + manager.logger.Panic().Err(err).Msg("unable to recreate broadcast pipeline") } } - }() + + if manager.screencast.Started() { + err := manager.screencast.createPipeline() + if err != nil && !errors.Is(err, types.ErrCapturePipelineAlreadyExists) { + manager.logger.Panic().Err(err).Msg("unable to recreate screencast pipeline") + } + } + }) } func (manager *CaptureManagerCtx) Shutdown() error { manager.logger.Info().Msgf("shutdown") manager.broadcast.shutdown() + manager.screencast.shutdown() manager.audio.shutdown() manager.video.shutdown() - gst.QuitMainLoop() + manager.webcam.shutdown() + manager.microphone.shutdown() return nil } @@ -111,10 +248,22 @@ func (manager *CaptureManagerCtx) Broadcast() types.BroadcastManager { return manager.broadcast } +func (manager *CaptureManagerCtx) Screencast() types.ScreencastManager { + return manager.screencast +} + func (manager *CaptureManagerCtx) Audio() types.StreamSinkManager { return manager.audio } -func (manager *CaptureManagerCtx) Video() types.StreamSinkManager { +func (manager *CaptureManagerCtx) Video() types.StreamSelectorManager { return manager.video } + +func (manager *CaptureManagerCtx) Webcam() types.StreamSrcManager { + return manager.webcam +} + +func (manager *CaptureManagerCtx) Microphone() types.StreamSrcManager { + return manager.microphone +} diff --git a/internal/capture/screencast.go b/server/internal/capture/screencast.go similarity index 100% rename from internal/capture/screencast.go rename to server/internal/capture/screencast.go diff --git a/internal/capture/streamselector.go b/server/internal/capture/streamselector.go similarity index 100% rename from internal/capture/streamselector.go rename to server/internal/capture/streamselector.go diff --git a/server/internal/capture/streamsink.go b/server/internal/capture/streamsink.go index 2aa3b367..99703bba 100644 --- a/server/internal/capture/streamsink.go +++ b/server/internal/capture/streamsink.go @@ -2,41 +2,122 @@ package capture import ( "errors" + "reflect" "sync" + "sync/atomic" + "time" + + "github.com/prometheus/client_golang/prometheus" + "github.com/prometheus/client_golang/prometheus/promauto" "github.com/rs/zerolog" "github.com/rs/zerolog/log" - "m1k1o/neko/internal/capture/gst" - "m1k1o/neko/internal/types" - "m1k1o/neko/internal/types/codec" + "github.com/demodesk/neko/pkg/gst" + "github.com/demodesk/neko/pkg/types" + "github.com/demodesk/neko/pkg/types/codec" ) +var moveSinkListenerMu = sync.Mutex{} + type StreamSinkManagerCtx struct { - logger zerolog.Logger - mu sync.Mutex - sampleChannel chan types.Sample + id string + + // wait for a keyframe before sending samples + waitForKf bool + + bitrate uint64 // atomic + brBuckets map[int]float64 + + logger zerolog.Logger + mu sync.Mutex + wg sync.WaitGroup codec codec.RTPCodec - pipeline *gst.Pipeline + pipeline gst.Pipeline pipelineMu sync.Mutex pipelineFn func() (string, error) - listeners int + listeners map[uintptr]types.SampleListener + listenersKf map[uintptr]types.SampleListener // keyframe lobby listenersMu sync.Mutex + + // metrics + currentListeners prometheus.Gauge + totalBytes prometheus.Counter + pipelinesCounter prometheus.Counter + pipelinesActive prometheus.Gauge } -func streamSinkNew(codec codec.RTPCodec, pipelineFn func() (string, error), video_id string) *StreamSinkManagerCtx { +func streamSinkNew(codec codec.RTPCodec, pipelineFn func() (string, error), id string) *StreamSinkManagerCtx { logger := log.With(). Str("module", "capture"). Str("submodule", "stream-sink"). - Str("video_id", video_id).Logger() + Str("id", id).Logger() manager := &StreamSinkManagerCtx{ - logger: logger, - codec: codec, - pipelineFn: pipelineFn, - sampleChannel: make(chan types.Sample), + id: id, + + // only wait for keyframes if the codec is video + waitForKf: codec.IsVideo(), + + bitrate: 0, + brBuckets: map[int]float64{}, + + logger: logger, + codec: codec, + pipelineFn: pipelineFn, + + listeners: map[uintptr]types.SampleListener{}, + listenersKf: map[uintptr]types.SampleListener{}, + + // metrics + currentListeners: promauto.NewGauge(prometheus.GaugeOpts{ + Name: "streamsink_listeners", + Namespace: "neko", + Subsystem: "capture", + Help: "Current number of listeners for a pipeline.", + ConstLabels: map[string]string{ + "video_id": id, + "codec_name": codec.Name, + "codec_type": codec.Type.String(), + }, + }), + totalBytes: promauto.NewCounter(prometheus.CounterOpts{ + Name: "streamsink_bytes", + Namespace: "neko", + Subsystem: "capture", + Help: "Total number of bytes created by the pipeline.", + ConstLabels: map[string]string{ + "video_id": id, + "codec_name": codec.Name, + "codec_type": codec.Type.String(), + }, + }), + pipelinesCounter: promauto.NewCounter(prometheus.CounterOpts{ + Name: "pipelines_total", + Namespace: "neko", + Subsystem: "capture", + Help: "Total number of created pipelines.", + ConstLabels: map[string]string{ + "submodule": "streamsink", + "video_id": id, + "codec_name": codec.Name, + "codec_type": codec.Type.String(), + }, + }), + pipelinesActive: promauto.NewGauge(prometheus.GaugeOpts{ + Name: "pipelines_active", + Namespace: "neko", + Subsystem: "capture", + Help: "Total number of active pipelines.", + ConstLabels: map[string]string{ + "submodule": "streamsink", + "video_id": id, + "codec_name": codec.Name, + "codec_type": codec.Type.String(), + }, + }), } return manager @@ -45,7 +126,25 @@ func streamSinkNew(codec codec.RTPCodec, pipelineFn func() (string, error), vide func (manager *StreamSinkManagerCtx) shutdown() { manager.logger.Info().Msgf("shutdown") - manager.destroyPipeline() + manager.listenersMu.Lock() + for key := range manager.listeners { + delete(manager.listeners, key) + } + for key := range manager.listenersKf { + delete(manager.listenersKf, key) + } + manager.listenersMu.Unlock() + + manager.DestroyPipeline() + manager.wg.Wait() +} + +func (manager *StreamSinkManagerCtx) ID() string { + return manager.id +} + +func (manager *StreamSinkManagerCtx) Bitrate() uint64 { + return atomic.LoadUint64(&manager.bitrate) } func (manager *StreamSinkManagerCtx) Codec() codec.RTPCodec { @@ -53,8 +152,8 @@ func (manager *StreamSinkManagerCtx) Codec() codec.RTPCodec { } func (manager *StreamSinkManagerCtx) start() error { - if manager.listeners == 0 { - err := manager.createPipeline() + if len(manager.listeners)+len(manager.listenersKf) == 0 { + err := manager.CreatePipeline() if err != nil && !errors.Is(err, types.ErrCapturePipelineAlreadyExists) { return err } @@ -66,45 +165,123 @@ func (manager *StreamSinkManagerCtx) start() error { } func (manager *StreamSinkManagerCtx) stop() { - if manager.listeners == 0 { - manager.destroyPipeline() + if len(manager.listeners)+len(manager.listenersKf) == 0 { + manager.DestroyPipeline() manager.logger.Info().Msgf("last listener, stopping") } } -func (manager *StreamSinkManagerCtx) addListener() { +func (manager *StreamSinkManagerCtx) addListener(listener types.SampleListener) { + ptr := reflect.ValueOf(listener).Pointer() + emitKeyframe := false + manager.listenersMu.Lock() - manager.listeners++ + if manager.waitForKf { + // if this is the first listener, we need to emit a keyframe + emitKeyframe = len(manager.listenersKf) == 0 + // if we're waiting for a keyframe, add it to the keyframe lobby + manager.listenersKf[ptr] = listener + } else { + // otherwise, add it as a regular listener + manager.listeners[ptr] = listener + } manager.listenersMu.Unlock() + + manager.logger.Debug().Interface("ptr", ptr).Msgf("adding listener") + manager.currentListeners.Set(float64(manager.ListenersCount())) + + // if we will be waiting for a keyframe, emit one now + if manager.pipeline != nil && emitKeyframe { + manager.pipeline.EmitVideoKeyframe() + } } -func (manager *StreamSinkManagerCtx) removeListener() { +func (manager *StreamSinkManagerCtx) removeListener(listener types.SampleListener) { + ptr := reflect.ValueOf(listener).Pointer() + manager.listenersMu.Lock() - manager.listeners-- + delete(manager.listeners, ptr) + delete(manager.listenersKf, ptr) // if it's a keyframe listener, remove it too manager.listenersMu.Unlock() + + manager.logger.Debug().Interface("ptr", ptr).Msgf("removing listener") + manager.currentListeners.Set(float64(manager.ListenersCount())) } -func (manager *StreamSinkManagerCtx) AddListener() error { +func (manager *StreamSinkManagerCtx) AddListener(listener types.SampleListener) error { manager.mu.Lock() defer manager.mu.Unlock() + if listener == nil { + return errors.New("listener cannot be nil") + } + // start if stopped if err := manager.start(); err != nil { return err } // add listener - manager.addListener() + manager.addListener(listener) return nil } -func (manager *StreamSinkManagerCtx) RemoveListener() error { +func (manager *StreamSinkManagerCtx) RemoveListener(listener types.SampleListener) error { manager.mu.Lock() defer manager.mu.Unlock() + if listener == nil { + return errors.New("listener cannot be nil") + } + // remove listener - manager.removeListener() + manager.removeListener(listener) + + // stop if started + manager.stop() + + return nil +} + +// moving listeners between streams ensures, that target pipeline is running +// before listener is added, and stops source pipeline if there are 0 listeners +func (manager *StreamSinkManagerCtx) MoveListenerTo(listener types.SampleListener, stream types.StreamSinkManager) error { + if listener == nil { + return errors.New("listener cannot be nil") + } + + targetStream, ok := stream.(*StreamSinkManagerCtx) + if !ok { + return errors.New("target stream manager does not support moving listeners") + } + + // we need to acquire both mutextes, from source stream and from target stream + // in order to do that safely (without possibility of deadlock) we need third + // global mutex, that ensures atomic locking + + // lock global mutex + moveSinkListenerMu.Lock() + + // lock source stream + manager.mu.Lock() + defer manager.mu.Unlock() + + // lock target stream + targetStream.mu.Lock() + defer targetStream.mu.Unlock() + + // unlock global mutex + moveSinkListenerMu.Unlock() + + // start if stopped + if err := targetStream.start(); err != nil { + return err + } + + // swap listeners + manager.removeListener(listener) + targetStream.addListener(listener) // stop if started manager.stop() @@ -116,14 +293,14 @@ func (manager *StreamSinkManagerCtx) ListenersCount() int { manager.listenersMu.Lock() defer manager.listenersMu.Unlock() - return manager.listeners + return len(manager.listeners) + len(manager.listenersKf) } func (manager *StreamSinkManagerCtx) Started() bool { return manager.ListenersCount() > 0 } -func (manager *StreamSinkManagerCtx) createPipeline() error { +func (manager *StreamSinkManagerCtx) CreatePipeline() error { manager.pipelineMu.Lock() defer manager.pipelineMu.Unlock() @@ -146,18 +323,79 @@ func (manager *StreamSinkManagerCtx) createPipeline() error { return err } - appsinkSubfix := "audio" - if manager.codec.IsVideo() { - appsinkSubfix = "video" - } - - manager.pipeline.AttachAppsink("appsink"+appsinkSubfix, manager.sampleChannel) + manager.pipeline.AttachAppsink("appsink") manager.pipeline.Play() + manager.wg.Add(1) + pipeline := manager.pipeline + + go func() { + manager.logger.Debug().Msg("started emitting samples") + defer manager.wg.Done() + + for { + sample, ok := <-pipeline.Sample() + if !ok { + manager.logger.Debug().Msg("stopped emitting samples") + return + } + + manager.onSample(sample) + } + }() + + manager.pipelinesCounter.Inc() + manager.pipelinesActive.Set(1) + return nil } -func (manager *StreamSinkManagerCtx) destroyPipeline() { +func (manager *StreamSinkManagerCtx) saveSampleBitrate(timestamp time.Time, delta float64) { + // get unix timestamp in seconds + sec := timestamp.Unix() + // last bucket is timestamp rounded to 3 seconds - 1 second + last := int((sec - 1) % 3) + // current bucket is timestamp rounded to 3 seconds + curr := int(sec % 3) + // next bucket is timestamp rounded to 3 seconds + 1 second + next := int((sec + 1) % 3) + + if manager.brBuckets[next] != 0 { + // atomic update bitrate + atomic.StoreUint64(&manager.bitrate, uint64(manager.brBuckets[last])) + // empty next bucket + manager.brBuckets[next] = 0 + } + + // add rate to current bucket + manager.brBuckets[curr] += delta +} + +func (manager *StreamSinkManagerCtx) onSample(sample types.Sample) { + manager.listenersMu.Lock() + defer manager.listenersMu.Unlock() + + // save to metrics + length := float64(sample.Length) + manager.totalBytes.Add(length) + manager.saveSampleBitrate(sample.Timestamp, length) + + // if is not delta unit -> it can be decoded independently -> it is a keyframe + if manager.waitForKf && !sample.DeltaUnit && len(manager.listenersKf) > 0 { + // if current sample is a keyframe, move listeners from + // keyframe lobby to actual listeners map and clear lobby + for k, v := range manager.listenersKf { + manager.listeners[k] = v + } + manager.listenersKf = make(map[uintptr]types.SampleListener) + } + + for _, l := range manager.listeners { + l.WriteSample(sample) + } +} + +func (manager *StreamSinkManagerCtx) DestroyPipeline() { manager.pipelineMu.Lock() defer manager.pipelineMu.Unlock() @@ -168,8 +406,9 @@ func (manager *StreamSinkManagerCtx) destroyPipeline() { manager.pipeline.Destroy() manager.logger.Info().Msgf("destroying pipeline") manager.pipeline = nil -} -func (manager *StreamSinkManagerCtx) GetSampleChannel() chan types.Sample { - return manager.sampleChannel + manager.pipelinesActive.Set(0) + + manager.brBuckets = make(map[int]float64) + atomic.StoreUint64(&manager.bitrate, 0) } diff --git a/internal/capture/streamsrc.go b/server/internal/capture/streamsrc.go similarity index 100% rename from internal/capture/streamsrc.go rename to server/internal/capture/streamsrc.go diff --git a/server/internal/config/capture.go b/server/internal/config/capture.go index 2db0be6b..7cc389b3 100644 --- a/server/internal/config/capture.go +++ b/server/internal/config/capture.go @@ -1,55 +1,197 @@ package config import ( - "m1k1o/neko/internal/types/codec" + "os" "strings" "github.com/pion/webrtc/v3" "github.com/rs/zerolog/log" "github.com/spf13/cobra" "github.com/spf13/viper" + + "github.com/demodesk/neko/pkg/types" + "github.com/demodesk/neko/pkg/types/codec" + "github.com/demodesk/neko/pkg/utils" ) +// Legacy capture configuration type HwEnc int +// Legacy capture configuration const ( - HwEncNone HwEnc = iota + HwEncUnset HwEnc = iota + HwEncNone HwEncVAAPI HwEncNVENC ) type Capture struct { - // video - Display string - VideoCodec codec.RTPCodec - VideoHWEnc HwEnc // TODO: Pipeline builder. - VideoBitrate uint // TODO: Pipeline builder. - VideoMaxFPS int16 // TODO: Pipeline builder. - VideoPipeline string + Display string + + VideoCodec codec.RTPCodec + VideoIDs []string + VideoPipelines map[string]types.VideoConfig - // audio AudioDevice string AudioCodec codec.RTPCodec - AudioBitrate uint // TODO: Pipeline builder. AudioPipeline string - // broadcast - BroadcastPipeline string - BroadcastUrl string - BroadcastAutostart bool + BroadcastAudioBitrate int + BroadcastVideoBitrate int + BroadcastPreset string + BroadcastPipeline string + BroadcastUrl string + BroadcastAutostart bool + + ScreencastEnabled bool + ScreencastRate string + ScreencastQuality string + ScreencastPipeline string + + WebcamEnabled bool + WebcamDevice string + WebcamWidth int + WebcamHeight int + + MicrophoneEnabled bool + MicrophoneDevice string } func (Capture) Init(cmd *cobra.Command) error { - // - // video - // + // audio + cmd.PersistentFlags().String("capture.audio.device", "audio_output.monitor", "pulseaudio device to capture") + if err := viper.BindPFlag("capture.audio.device", cmd.PersistentFlags().Lookup("capture.audio.device")); err != nil { + return err + } - cmd.PersistentFlags().String("display", ":99.0", "XDisplay to capture") + cmd.PersistentFlags().String("capture.audio.codec", "opus", "audio codec to be used") + if err := viper.BindPFlag("capture.audio.codec", cmd.PersistentFlags().Lookup("capture.audio.codec")); err != nil { + return err + } + + cmd.PersistentFlags().String("capture.audio.pipeline", "", "gstreamer pipeline used for audio streaming") + if err := viper.BindPFlag("capture.audio.pipeline", cmd.PersistentFlags().Lookup("capture.audio.pipeline")); err != nil { + return err + } + + // videos + cmd.PersistentFlags().String("capture.video.display", "", "X display to capture") + if err := viper.BindPFlag("capture.video.display", cmd.PersistentFlags().Lookup("capture.video.display")); err != nil { + return err + } + + cmd.PersistentFlags().String("capture.video.codec", "vp8", "video codec to be used") + if err := viper.BindPFlag("capture.video.codec", cmd.PersistentFlags().Lookup("capture.video.codec")); err != nil { + return err + } + + cmd.PersistentFlags().StringSlice("capture.video.ids", []string{}, "ordered list of video ids") + if err := viper.BindPFlag("capture.video.ids", cmd.PersistentFlags().Lookup("capture.video.ids")); err != nil { + return err + } + + cmd.PersistentFlags().String("capture.video.pipelines", "[]", "pipelines config in JSON used for video streaming") + if err := viper.BindPFlag("capture.video.pipelines", cmd.PersistentFlags().Lookup("capture.video.pipelines")); err != nil { + return err + } + + // broadcast + cmd.PersistentFlags().Int("capture.broadcast.audio_bitrate", 128, "broadcast audio bitrate in KB/s") + if err := viper.BindPFlag("capture.broadcast.audio_bitrate", cmd.PersistentFlags().Lookup("capture.broadcast.audio_bitrate")); err != nil { + return err + } + + cmd.PersistentFlags().Int("capture.broadcast.video_bitrate", 4096, "broadcast video bitrate in KB/s") + if err := viper.BindPFlag("capture.broadcast.video_bitrate", cmd.PersistentFlags().Lookup("capture.broadcast.video_bitrate")); err != nil { + return err + } + + cmd.PersistentFlags().String("capture.broadcast.preset", "veryfast", "broadcast speed preset for h264 encoding") + if err := viper.BindPFlag("capture.broadcast.preset", cmd.PersistentFlags().Lookup("capture.broadcast.preset")); err != nil { + return err + } + + cmd.PersistentFlags().String("capture.broadcast.pipeline", "", "gstreamer pipeline used for broadcasting") + if err := viper.BindPFlag("capture.broadcast.pipeline", cmd.PersistentFlags().Lookup("capture.broadcast.pipeline")); err != nil { + return err + } + + cmd.PersistentFlags().String("capture.broadcast.url", "", "initial URL for broadcasting, setting this value will automatically start broadcasting") + if err := viper.BindPFlag("capture.broadcast.url", cmd.PersistentFlags().Lookup("capture.broadcast.url")); err != nil { + return err + } + + cmd.PersistentFlags().Bool("capture.broadcast.autostart", true, "automatically start broadcasting when neko starts and broadcast_url is set") + if err := viper.BindPFlag("capture.broadcast.autostart", cmd.PersistentFlags().Lookup("capture.broadcast.autostart")); err != nil { + return err + } + + // screencast + cmd.PersistentFlags().Bool("capture.screencast.enabled", false, "enable screencast") + if err := viper.BindPFlag("capture.screencast.enabled", cmd.PersistentFlags().Lookup("capture.screencast.enabled")); err != nil { + return err + } + + cmd.PersistentFlags().String("capture.screencast.rate", "10/1", "screencast frame rate") + if err := viper.BindPFlag("capture.screencast.rate", cmd.PersistentFlags().Lookup("capture.screencast.rate")); err != nil { + return err + } + + cmd.PersistentFlags().String("capture.screencast.quality", "60", "screencast JPEG quality") + if err := viper.BindPFlag("capture.screencast.quality", cmd.PersistentFlags().Lookup("capture.screencast.quality")); err != nil { + return err + } + + cmd.PersistentFlags().String("capture.screencast.pipeline", "", "gstreamer pipeline used for screencasting") + if err := viper.BindPFlag("capture.screencast.pipeline", cmd.PersistentFlags().Lookup("capture.screencast.pipeline")); err != nil { + return err + } + + // webcam + cmd.PersistentFlags().Bool("capture.webcam.enabled", false, "enable webcam stream") + if err := viper.BindPFlag("capture.webcam.enabled", cmd.PersistentFlags().Lookup("capture.webcam.enabled")); err != nil { + return err + } + + // sudo apt install v4l2loopback-dkms v4l2loopback-utils + // sudo apt-get install linux-headers-`uname -r` linux-modules-extra-`uname -r` + // sudo modprobe v4l2loopback exclusive_caps=1 + cmd.PersistentFlags().String("capture.webcam.device", "/dev/video0", "v4l2sink device used for webcam") + if err := viper.BindPFlag("capture.webcam.device", cmd.PersistentFlags().Lookup("capture.webcam.device")); err != nil { + return err + } + + cmd.PersistentFlags().Int("capture.webcam.width", 1280, "webcam stream width") + if err := viper.BindPFlag("capture.webcam.width", cmd.PersistentFlags().Lookup("capture.webcam.width")); err != nil { + return err + } + + cmd.PersistentFlags().Int("capture.webcam.height", 720, "webcam stream height") + if err := viper.BindPFlag("capture.webcam.height", cmd.PersistentFlags().Lookup("capture.webcam.height")); err != nil { + return err + } + + // microphone + cmd.PersistentFlags().Bool("capture.microphone.enabled", true, "enable microphone stream") + if err := viper.BindPFlag("capture.microphone.enabled", cmd.PersistentFlags().Lookup("capture.microphone.enabled")); err != nil { + return err + } + + cmd.PersistentFlags().String("capture.microphone.device", "audio_input", "pulseaudio device used for microphone") + if err := viper.BindPFlag("capture.microphone.device", cmd.PersistentFlags().Lookup("capture.microphone.device")); err != nil { + return err + } + + return nil +} + +func (Capture) InitV2(cmd *cobra.Command) error { + cmd.PersistentFlags().String("display", "", "V2: XDisplay to capture") if err := viper.BindPFlag("display", cmd.PersistentFlags().Lookup("display")); err != nil { return err } - cmd.PersistentFlags().String("video_codec", "vp8", "video codec to be used") + cmd.PersistentFlags().String("video_codec", "", "V2: video codec to be used") if err := viper.BindPFlag("video_codec", cmd.PersistentFlags().Lookup("video_codec")); err != nil { return err } @@ -78,22 +220,22 @@ func (Capture) Init(cmd *cobra.Command) error { return err } - cmd.PersistentFlags().String("hwenc", "", "use hardware accelerated encoding") + cmd.PersistentFlags().String("hwenc", "", "V2: use hardware accelerated encoding") if err := viper.BindPFlag("hwenc", cmd.PersistentFlags().Lookup("hwenc")); err != nil { return err } - cmd.PersistentFlags().Int("video_bitrate", 3072, "video bitrate in kbit/s") + cmd.PersistentFlags().Int("video_bitrate", 0, "V2: video bitrate in kbit/s") if err := viper.BindPFlag("video_bitrate", cmd.PersistentFlags().Lookup("video_bitrate")); err != nil { return err } - cmd.PersistentFlags().Int("max_fps", 25, "maximum fps delivered via WebRTC, 0 is for no maximum") + cmd.PersistentFlags().Int("max_fps", 0, "V2: maximum fps delivered via WebRTC, 0 is for no maximum") if err := viper.BindPFlag("max_fps", cmd.PersistentFlags().Lookup("max_fps")); err != nil { return err } - cmd.PersistentFlags().String("video", "", "video codec parameters to use for streaming") + cmd.PersistentFlags().String("video", "", "V2: video codec parameters to use for streaming") if err := viper.BindPFlag("video", cmd.PersistentFlags().Lookup("video")); err != nil { return err } @@ -102,12 +244,12 @@ func (Capture) Init(cmd *cobra.Command) error { // audio // - cmd.PersistentFlags().String("device", "audio_output.monitor", "audio device to capture") + cmd.PersistentFlags().String("device", "", "V2: audio device to capture") if err := viper.BindPFlag("device", cmd.PersistentFlags().Lookup("device")); err != nil { return err } - cmd.PersistentFlags().String("audio_codec", "opus", "audio codec to be used") + cmd.PersistentFlags().String("audio_codec", "", "V2: audio codec to be used") if err := viper.BindPFlag("audio_codec", cmd.PersistentFlags().Lookup("audio_codec")); err != nil { return err } @@ -137,12 +279,12 @@ func (Capture) Init(cmd *cobra.Command) error { } // audio codecs - cmd.PersistentFlags().Int("audio_bitrate", 128, "audio bitrate in kbit/s") + cmd.PersistentFlags().Int("audio_bitrate", 0, "V2: audio bitrate in kbit/s") if err := viper.BindPFlag("audio_bitrate", cmd.PersistentFlags().Lookup("audio_bitrate")); err != nil { return err } - cmd.PersistentFlags().String("audio", "", "audio codec parameters to use for streaming") + cmd.PersistentFlags().String("audio", "", "V2: audio codec parameters to use for streaming") if err := viper.BindPFlag("audio", cmd.PersistentFlags().Lookup("audio")); err != nil { return err } @@ -151,17 +293,17 @@ func (Capture) Init(cmd *cobra.Command) error { // broadcast // - cmd.PersistentFlags().String("broadcast_pipeline", "", "custom gst pipeline used for broadcasting, strings {url} {device} {display} will be replaced") + cmd.PersistentFlags().String("broadcast_pipeline", "", "V2: custom gst pipeline used for broadcasting, strings {url} {device} {display} will be replaced") if err := viper.BindPFlag("broadcast_pipeline", cmd.PersistentFlags().Lookup("broadcast_pipeline")); err != nil { return err } - cmd.PersistentFlags().String("broadcast_url", "", "a default default URL for broadcast streams, can be disabled/changed later by admins in the GUI") + cmd.PersistentFlags().String("broadcast_url", "", "V2: a default default URL for broadcast streams, can be disabled/changed later by admins in the GUI") if err := viper.BindPFlag("broadcast_url", cmd.PersistentFlags().Lookup("broadcast_url")); err != nil { return err } - cmd.PersistentFlags().Bool("broadcast_autostart", true, "automatically start broadcasting when neko starts and broadcast_url is set") + cmd.PersistentFlags().Bool("broadcast_autostart", false, "V2: automatically start broadcasting when neko starts and broadcast_url is set") if err := viper.BindPFlag("broadcast_autostart", cmd.PersistentFlags().Lookup("broadcast_autostart")); err != nil { return err } @@ -172,86 +314,220 @@ func (Capture) Init(cmd *cobra.Command) error { func (s *Capture) Set() { var ok bool - // + s.Display = viper.GetString("capture.video.display") + + // Display is provided by env variable unless explicitly set + if s.Display == "" { + s.Display = os.Getenv("DISPLAY") + } + // video - // - - s.Display = viper.GetString("display") - - videoCodec := viper.GetString("video_codec") + videoCodec := viper.GetString("capture.video.codec") s.VideoCodec, ok = codec.ParseStr(videoCodec) - if !ok || s.VideoCodec.Type != webrtc.RTPCodecTypeVideo { + if !ok || !s.VideoCodec.IsVideo() { log.Warn().Str("codec", videoCodec).Msgf("unknown video codec, using Vp8") s.VideoCodec = codec.VP8() } + s.VideoIDs = viper.GetStringSlice("capture.video.ids") + if err := viper.UnmarshalKey("capture.video.pipelines", &s.VideoPipelines, viper.DecodeHook( + utils.JsonStringAutoDecode(s.VideoPipelines), + )); err != nil { + log.Warn().Err(err).Msgf("unable to parse video pipelines") + } + + // default video + if len(s.VideoPipelines) == 0 { + log.Warn().Msgf("no video pipelines specified, using defaults") + + s.VideoCodec = codec.VP8() + s.VideoPipelines = map[string]types.VideoConfig{ + "main": { + Fps: "25", + GstEncoder: "vp8enc", + GstParams: map[string]string{ + "target-bitrate": "round(3072 * 650)", + "cpu-used": "4", + "end-usage": "cbr", + "threads": "4", + "deadline": "1", + "undershoot": "95", + "buffer-size": "(3072 * 4)", + "buffer-initial-size": "(3072 * 2)", + "buffer-optimal-size": "(3072 * 3)", + "keyframe-max-dist": "25", + "min-quantizer": "4", + "max-quantizer": "20", + }, + }, + } + s.VideoIDs = []string{"main"} + } + + // audio + s.AudioDevice = viper.GetString("capture.audio.device") + s.AudioPipeline = viper.GetString("capture.audio.pipeline") + + audioCodec := viper.GetString("capture.audio.codec") + s.AudioCodec, ok = codec.ParseStr(audioCodec) + if !ok || !s.AudioCodec.IsAudio() { + log.Warn().Str("codec", audioCodec).Msgf("unknown audio codec, using Opus") + s.AudioCodec = codec.Opus() + } + + // broadcast + s.BroadcastAudioBitrate = viper.GetInt("capture.broadcast.audio_bitrate") + s.BroadcastVideoBitrate = viper.GetInt("capture.broadcast.video_bitrate") + s.BroadcastPreset = viper.GetString("capture.broadcast.preset") + s.BroadcastPipeline = viper.GetString("capture.broadcast.pipeline") + s.BroadcastUrl = viper.GetString("capture.broadcast.url") + s.BroadcastAutostart = viper.GetBool("capture.broadcast.autostart") + + // screencast + s.ScreencastEnabled = viper.GetBool("capture.screencast.enabled") + s.ScreencastRate = viper.GetString("capture.screencast.rate") + s.ScreencastQuality = viper.GetString("capture.screencast.quality") + s.ScreencastPipeline = viper.GetString("capture.screencast.pipeline") + + // webcam + s.WebcamEnabled = viper.GetBool("capture.webcam.enabled") + s.WebcamDevice = viper.GetString("capture.webcam.device") + s.WebcamWidth = viper.GetInt("capture.webcam.width") + s.WebcamHeight = viper.GetInt("capture.webcam.height") + + // microphone + s.MicrophoneEnabled = viper.GetBool("capture.microphone.enabled") + s.MicrophoneDevice = viper.GetString("capture.microphone.device") +} + +func (s *Capture) SetV2() { + var ok bool + + // + // video + // + + 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") + } + + if videoCodec := viper.GetString("video_codec"); videoCodec != "" { + s.VideoCodec, ok = codec.ParseStr(videoCodec) + if !ok || s.VideoCodec.Type != webrtc.RTPCodecTypeVideo { + log.Warn().Str("codec", videoCodec).Msgf("unknown video codec, using Vp8") + 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") + } + if viper.GetBool("vp8") { s.VideoCodec = codec.VP8() - log.Warn().Msg("you are using deprecated config setting 'NEKO_VP8=true', use 'NEKO_VIDEO_CODEC=vp8' instead") + log.Warn().Msg("you are using deprecated config setting 'NEKO_VP8=true', use 'NEKO_CAPTURE_VIDEO_CODEC=vp8' instead") } else if viper.GetBool("vp9") { s.VideoCodec = codec.VP9() - log.Warn().Msg("you are using deprecated config setting 'NEKO_VP9=true', use 'NEKO_VIDEO_CODEC=vp9' instead") + log.Warn().Msg("you are using deprecated config setting 'NEKO_VP9=true', use 'NEKO_CAPTURE_VIDEO_CODEC=vp9' instead") } else if viper.GetBool("h264") { s.VideoCodec = codec.H264() - log.Warn().Msg("you are using deprecated config setting 'NEKO_H264=true', use 'NEKO_VIDEO_CODEC=h264' instead") + log.Warn().Msg("you are using deprecated config setting 'NEKO_H264=true', use 'NEKO_CAPTURE_VIDEO_CODEC=h264' instead") } else if viper.GetBool("av1") { s.VideoCodec = codec.AV1() - log.Warn().Msg("you are using deprecated config setting 'NEKO_AV1=true', use 'NEKO_VIDEO_CODEC=av1' instead") + log.Warn().Msg("you are using deprecated config setting 'NEKO_AV1=true', use 'NEKO_CAPTURE_VIDEO_CODEC=av1' instead") } - videoHWEnc := strings.ToLower(viper.GetString("hwenc")) - switch videoHWEnc { - case "": - fallthrough - case "none": - s.VideoHWEnc = HwEncNone - case "vaapi": - s.VideoHWEnc = HwEncVAAPI - case "nvenc": - s.VideoHWEnc = HwEncNVENC - default: - log.Warn().Str("hwenc", videoHWEnc).Msgf("unknown video hw encoder, using CPU") + videoHWEnc := HwEncUnset + if hwenc := strings.ToLower(viper.GetString("hwenc")); hwenc != "" { + switch hwenc { + case "none": + videoHWEnc = HwEncNone + case "vaapi": + videoHWEnc = HwEncVAAPI + case "nvenc": + videoHWEnc = HwEncNVENC + default: + log.Warn().Str("hwenc", hwenc).Msgf("unknown video hw encoder, using CPU") + } } - s.VideoBitrate = viper.GetUint("video_bitrate") - s.VideoMaxFPS = int16(viper.GetInt("max_fps")) - s.VideoPipeline = viper.GetString("video") + videoBitrate := viper.GetUint("video_bitrate") + videoMaxFPS := int16(viper.GetInt("max_fps")) + videoPipeline := viper.GetString("video") + + // video pipeline + if videoHWEnc != HwEncUnset || videoBitrate != 0 || videoMaxFPS != 0 || videoPipeline != "" { + pipeline, err := NewVideoPipeline(s.VideoCodec, s.Display, videoPipeline, videoMaxFPS, videoBitrate, videoHWEnc) + if err != nil { + log.Warn().Err(err).Msg("unable to create video pipeline, using default") + } else { + s.VideoPipelines = map[string]types.VideoConfig{ + "main": { + GstPipeline: pipeline, + }, + } + // TODO: add deprecated warning and proper alternative + } + } // // audio // - s.AudioDevice = viper.GetString("device") + 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") + } - audioCodec := viper.GetString("audio_codec") - s.AudioCodec, ok = codec.ParseStr(audioCodec) - if !ok || s.AudioCodec.Type != webrtc.RTPCodecTypeAudio { - log.Warn().Str("codec", audioCodec).Msgf("unknown audio codec, using Opus") - s.AudioCodec = codec.Opus() + if audioCodec := viper.GetString("audio_codec"); audioCodec != "" { + s.AudioCodec, ok = codec.ParseStr(audioCodec) + if !ok || s.AudioCodec.Type != webrtc.RTPCodecTypeAudio { + log.Warn().Str("codec", audioCodec).Msgf("unknown audio codec, using Opus") + 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") } if viper.GetBool("opus") { s.AudioCodec = codec.Opus() - log.Warn().Msg("you are using deprecated config setting 'NEKO_OPUS=true', use 'NEKO_VIDEO_CODEC=opus' instead") + log.Warn().Msg("you are using deprecated config setting 'NEKO_OPUS=true', use 'NEKO_CAPTURE_AUDIO_CODEC=opus' instead") } else if viper.GetBool("g722") { s.AudioCodec = codec.G722() - log.Warn().Msg("you are using deprecated config setting 'NEKO_G722=true', use 'NEKO_VIDEO_CODEC=g722' instead") + log.Warn().Msg("you are using deprecated config setting 'NEKO_G722=true', use 'NEKO_CAPTURE_AUDIO_CODEC=g722' instead") } else if viper.GetBool("pcmu") { s.AudioCodec = codec.PCMU() - log.Warn().Msg("you are using deprecated config setting 'NEKO_PCMU=true', use 'NEKO_VIDEO_CODEC=pcmu' instead") + log.Warn().Msg("you are using deprecated config setting 'NEKO_PCMU=true', use 'NEKO_CAPTURE_AUDIO_CODEC=pcmu' instead") } else if viper.GetBool("pcma") { s.AudioCodec = codec.PCMA() - log.Warn().Msg("you are using deprecated config setting 'NEKO_PCMA=true', use 'NEKO_VIDEO_CODEC=pcma' instead") + log.Warn().Msg("you are using deprecated config setting 'NEKO_PCMA=true', use 'NEKO_CAPTURE_AUDIO_CODEC=pcma' instead") } - s.AudioBitrate = viper.GetUint("audio_bitrate") - s.AudioPipeline = viper.GetString("audio") + audioBitrate := viper.GetUint("audio_bitrate") + audioPipeline := viper.GetString("audio") + + // audio pipeline + if audioBitrate != 0 || audioPipeline != "" { + pipeline, err := NewAudioPipeline(s.AudioCodec, s.AudioDevice, audioPipeline, audioBitrate) + if err != nil { + log.Warn().Err(err).Msg("unable to create audio pipeline, using default") + } else { + s.AudioPipeline = pipeline + } + // TODO: add deprecated warning and proper alternative + } // // broadcast // - s.BroadcastPipeline = viper.GetString("broadcast_pipeline") - s.BroadcastUrl = viper.GetString("broadcast_url") - s.BroadcastAutostart = viper.GetBool("broadcast_autostart") + 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") + } + 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") + } + 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") + } } diff --git a/server/internal/capture/pipelines.go b/server/internal/config/capture_pipeline.go similarity index 95% rename from server/internal/capture/pipelines.go rename to server/internal/config/capture_pipeline.go index 654ddd31..35469cd6 100644 --- a/server/internal/capture/pipelines.go +++ b/server/internal/config/capture_pipeline.go @@ -1,12 +1,12 @@ -package capture +// Legacy pipeline configuration for gstreamer. +package config import ( "fmt" "strings" - "m1k1o/neko/internal/capture/gst" - "m1k1o/neko/internal/config" - "m1k1o/neko/internal/types/codec" + "github.com/demodesk/neko/pkg/gst" + "github.com/demodesk/neko/pkg/types/codec" ) /* @@ -34,7 +34,7 @@ const ( audioSrc = "pulsesrc device=%s ! audio/x-raw,channels=2 ! audioconvert ! " ) -func NewBroadcastPipeline(device string, display string, pipelineSrc string, url string) (string, error) { +func NewBroadcastPipeline(device string, display string, pipelineSrc string, url string) string { video := fmt.Sprintf(videoSrc, display, 25) audio := fmt.Sprintf(audioSrc, device) @@ -50,10 +50,10 @@ func NewBroadcastPipeline(device string, display string, pipelineSrc string, url pipelineStr = fmt.Sprintf("flvmux name=mux ! rtmpsink location='%s live=1' %s audio/x-raw,channels=2 ! audioconvert ! voaacenc ! mux. %s x264enc bframes=0 key-int-max=60 byte-stream=true tune=zerolatency speed-preset=veryfast ! mux.", url, audio, video) } - return pipelineStr, nil + return pipelineStr } -func NewVideoPipeline(rtpCodec codec.RTPCodec, display string, pipelineSrc string, fps int16, bitrate uint, hwenc config.HwEnc) (string, error) { +func NewVideoPipeline(rtpCodec codec.RTPCodec, display string, pipelineSrc string, fps int16, bitrate uint, hwenc HwEnc) (string, error) { pipelineStr := " ! appsink name=appsinkvideo" // if using custom pipeline @@ -69,7 +69,7 @@ func NewVideoPipeline(rtpCodec codec.RTPCodec, display string, pipelineSrc strin switch rtpCodec.Name { case codec.VP8().Name: - if hwenc == config.HwEncVAAPI { + if hwenc == HwEncVAAPI { if err := gst.CheckPlugins([]string{"ximagesrc", "vaapi"}); err != nil { return "", err } @@ -144,13 +144,13 @@ func NewVideoPipeline(rtpCodec codec.RTPCodec, display string, pipelineSrc strin vbvbuf = bitrate } - if hwenc == config.HwEncVAAPI { + if hwenc == HwEncVAAPI { if err := gst.CheckPlugins([]string{"vaapi"}); err != nil { return "", err } pipelineStr = fmt.Sprintf(videoSrc+"video/x-raw,format=NV12 ! vaapih264enc rate-control=vbr bitrate=%d keyframe-period=180 quality-level=7 ! video/x-h264,stream-format=byte-stream,profile=constrained-baseline"+pipelineStr, display, fps, bitrate) - } else if hwenc == config.HwEncNVENC { + } else if hwenc == HwEncNVENC { if err := gst.CheckPlugins([]string{"nvcodec"}); err != nil { return "", err } diff --git a/server/internal/config/desktop.go b/server/internal/config/desktop.go index 6d1ec583..41494e43 100644 --- a/server/internal/config/desktop.go +++ b/server/internal/config/desktop.go @@ -5,20 +5,67 @@ import ( "regexp" "strconv" + "github.com/rs/zerolog/log" "github.com/spf13/cobra" "github.com/spf13/viper" + + "github.com/demodesk/neko/pkg/types" ) type Desktop struct { Display string - ScreenWidth int - ScreenHeight int - ScreenRate int16 + ScreenSize types.ScreenSize + + UseInputDriver bool + InputSocket string + + Unminimize bool + UploadDrop bool + FileChooserDialog bool } func (Desktop) Init(cmd *cobra.Command) error { - cmd.PersistentFlags().String("screen", "1280x720@30", "default screen resolution and framerate") + cmd.PersistentFlags().String("desktop.display", "", "X display to use for desktop sharing") + if err := viper.BindPFlag("desktop.display", cmd.PersistentFlags().Lookup("desktop.display")); err != nil { + return err + } + + cmd.PersistentFlags().String("desktop.screen", "1280x720@30", "default screen size and framerate") + if err := viper.BindPFlag("desktop.screen", cmd.PersistentFlags().Lookup("desktop.screen")); err != nil { + return err + } + + cmd.PersistentFlags().Bool("desktop.input.enabled", true, "whether custom xf86 input driver should be used to handle touchscreen") + if err := viper.BindPFlag("desktop.input.enabled", cmd.PersistentFlags().Lookup("desktop.input.enabled")); err != nil { + return err + } + + cmd.PersistentFlags().String("desktop.input.socket", "/tmp/xf86-input-neko.sock", "socket path for custom xf86 input driver connection") + if err := viper.BindPFlag("desktop.input.socket", cmd.PersistentFlags().Lookup("desktop.input.socket")); err != nil { + return err + } + + cmd.PersistentFlags().Bool("desktop.unminimize", true, "automatically unminimize window when it is minimized") + if err := viper.BindPFlag("desktop.unminimize", cmd.PersistentFlags().Lookup("desktop.unminimize")); err != nil { + return err + } + + cmd.PersistentFlags().Bool("desktop.upload_drop", true, "whether drop upload is enabled") + if err := viper.BindPFlag("desktop.upload_drop", cmd.PersistentFlags().Lookup("desktop.upload_drop")); err != nil { + return err + } + + cmd.PersistentFlags().Bool("desktop.file_chooser_dialog", false, "whether to handle file chooser dialog externally") + if err := viper.BindPFlag("desktop.file_chooser_dialog", cmd.PersistentFlags().Lookup("desktop.file_chooser_dialog")); err != nil { + return err + } + + return nil +} + +func (Desktop) InitV2(cmd *cobra.Command) error { + cmd.PersistentFlags().String("screen", "", "V2: default screen resolution and framerate") if err := viper.BindPFlag("screen", cmd.PersistentFlags().Lookup("screen")); err != nil { return err } @@ -27,15 +74,21 @@ func (Desktop) Init(cmd *cobra.Command) error { } func (s *Desktop) Set() { - // Display is provided by env variable - s.Display = os.Getenv("DISPLAY") + s.Display = viper.GetString("desktop.display") - s.ScreenWidth = 1280 - s.ScreenHeight = 720 - s.ScreenRate = 30 + // Display is provided by env variable unless explicitly set + if s.Display == "" { + s.Display = os.Getenv("DISPLAY") + } + + s.ScreenSize = types.ScreenSize{ + Width: 1280, + Height: 720, + Rate: 30, + } r := regexp.MustCompile(`([0-9]{1,4})x([0-9]{1,4})@([0-9]{1,3})`) - res := r.FindStringSubmatch(viper.GetString("screen")) + res := r.FindStringSubmatch(viper.GetString("desktop.screen")) if len(res) > 0 { width, err1 := strconv.ParseInt(res[1], 10, 64) @@ -43,9 +96,35 @@ func (s *Desktop) Set() { rate, err3 := strconv.ParseInt(res[3], 10, 64) if err1 == nil && err2 == nil && err3 == nil { - s.ScreenWidth = int(width) - s.ScreenHeight = int(height) - s.ScreenRate = int16(rate) + s.ScreenSize.Width = int(width) + s.ScreenSize.Height = int(height) + s.ScreenSize.Rate = int16(rate) } } + + s.UseInputDriver = viper.GetBool("desktop.input.enabled") + s.InputSocket = viper.GetString("desktop.input.socket") + s.Unminimize = viper.GetBool("desktop.unminimize") + s.UploadDrop = viper.GetBool("desktop.upload_drop") + s.FileChooserDialog = viper.GetBool("desktop.file_chooser_dialog") +} + +func (s *Desktop) SetV2() { + if viper.IsSet("screen") { + r := regexp.MustCompile(`([0-9]{1,4})x([0-9]{1,4})@([0-9]{1,3})`) + res := r.FindStringSubmatch(viper.GetString("screen")) + + if len(res) > 0 { + width, err1 := strconv.ParseInt(res[1], 10, 64) + height, err2 := strconv.ParseInt(res[2], 10, 64) + rate, err3 := strconv.ParseInt(res[3], 10, 64) + + if err1 == nil && err2 == nil && err3 == nil { + s.ScreenSize.Width = int(width) + s.ScreenSize.Height = int(height) + s.ScreenSize.Rate = int16(rate) + } + } + log.Warn().Msg("you are using v2 configuration 'NEKO_SCREEN' which is deprecated, please use 'NEKO_DESKTOP_SCREEN' instead") + } } diff --git a/internal/config/member.go b/server/internal/config/member.go similarity index 79% rename from internal/config/member.go rename to server/internal/config/member.go index 1cccde36..56d903a1 100644 --- a/internal/config/member.go +++ b/server/internal/config/member.go @@ -68,6 +68,20 @@ func (Member) Init(cmd *cobra.Command) error { return nil } +func (Member) InitV2(cmd *cobra.Command) error { + cmd.PersistentFlags().String("password", "", "V2: password for connecting to stream") + if err := viper.BindPFlag("password", cmd.PersistentFlags().Lookup("password")); err != nil { + return err + } + + cmd.PersistentFlags().String("password_admin", "", "V2: admin password for connecting to stream") + if err := viper.BindPFlag("password_admin", cmd.PersistentFlags().Lookup("password_admin")); err != nil { + return err + } + + return nil +} + func (s *Member) Set() { s.Provider = viper.GetString("member.provider") @@ -126,3 +140,20 @@ func (s *Member) Set() { log.Warn().Err(err).Msgf("unable to parse member multiuser admin profile") } } + +func (s *Member) SetV2() { + if viper.IsSet("password") || viper.IsSet("password_admin") { + s.Provider = "multiuser" + if userPassword := viper.GetString("password"); userPassword != "" { + s.Multiuser.UserPassword = userPassword + } else { + s.Multiuser.UserPassword = "neko" + } + if adminPassword := viper.GetString("password_admin"); adminPassword != "" { + s.Multiuser.AdminPassword = adminPassword + } else { + 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") + } +} diff --git a/internal/config/plugins.go b/server/internal/config/plugins.go similarity index 100% rename from internal/config/plugins.go rename to server/internal/config/plugins.go diff --git a/server/internal/config/root.go b/server/internal/config/root.go index 0223d7b2..978e5130 100644 --- a/server/internal/config/root.go +++ b/server/internal/config/root.go @@ -1,29 +1,69 @@ package config import ( + "os" + "path/filepath" + "runtime" + + "github.com/rs/zerolog" + "github.com/rs/zerolog/log" "github.com/spf13/cobra" "github.com/spf13/viper" ) type Root struct { - Debug bool - Logs bool - CfgFile string + Config string + + LogLevel zerolog.Level + LogTime string + LogJson bool + LogNocolor bool + LogDir string } func (Root) Init(cmd *cobra.Command) error { + cmd.PersistentFlags().StringP("config", "c", "", "configuration file path") + if err := viper.BindPFlag("config", cmd.PersistentFlags().Lookup("config")); err != nil { + return err + } + + // just a shortcut cmd.PersistentFlags().BoolP("debug", "d", false, "enable debug mode") if err := viper.BindPFlag("debug", cmd.PersistentFlags().Lookup("debug")); err != nil { return err } - cmd.PersistentFlags().BoolP("logs", "l", false, "save logs to file") - if err := viper.BindPFlag("logs", cmd.PersistentFlags().Lookup("logs")); err != nil { + 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 } - cmd.PersistentFlags().String("config", "", "configuration file path") - if err := viper.BindPFlag("config", cmd.PersistentFlags().Lookup("config")); err != nil { + cmd.PersistentFlags().String("log.time", "unix", "time format used in logs (unix, unixms, unixmicro)") + if err := viper.BindPFlag("log.time", cmd.PersistentFlags().Lookup("log.time")); err != nil { + return err + } + + cmd.PersistentFlags().Bool("log.json", false, "logs in JSON format") + if err := viper.BindPFlag("log.json", cmd.PersistentFlags().Lookup("log.json")); err != nil { + return err + } + + cmd.PersistentFlags().Bool("log.nocolor", false, "no ANSI colors in non-JSON output") + if err := viper.BindPFlag("log.nocolor", cmd.PersistentFlags().Lookup("log.nocolor")); err != nil { + return err + } + + cmd.PersistentFlags().String("log.dir", "", "logging directory to store logs") + if err := viper.BindPFlag("log.dir", cmd.PersistentFlags().Lookup("log.dir")); err != nil { + return err + } + + return nil +} + +func (Root) InitV2(cmd *cobra.Command) error { + cmd.PersistentFlags().BoolP("logs", "l", false, "V2: save logs to file") + if err := viper.BindPFlag("logs", cmd.PersistentFlags().Lookup("logs")); err != nil { return err } @@ -31,7 +71,53 @@ func (Root) Init(cmd *cobra.Command) error { } func (s *Root) Set() { - s.Logs = viper.GetBool("logs") - s.Debug = viper.GetBool("debug") - s.CfgFile = viper.GetString("config") + s.Config = viper.GetString("config") + + logLevel := viper.GetString("log.level") + level, err := zerolog.ParseLevel(logLevel) + if err != nil { + log.Warn().Msgf("unknown log level %s", logLevel) + } else { + s.LogLevel = level + } + + logTime := viper.GetString("log.time") + switch logTime { + case "unix": + s.LogTime = zerolog.TimeFormatUnix + case "unixms": + s.LogTime = zerolog.TimeFormatUnixMs + case "unixmicro": + s.LogTime = zerolog.TimeFormatUnixMicro + default: + log.Warn().Msgf("unknown log time %s", logTime) + } + + s.LogJson = viper.GetBool("log.json") + s.LogNocolor = viper.GetBool("log.nocolor") + s.LogDir = viper.GetString("log.dir") + + if viper.GetBool("debug") && s.LogLevel != zerolog.TraceLevel { + s.LogLevel = zerolog.DebugLevel + } + + // support for NO_COLOR env variable: https://no-color.org/ + if os.Getenv("NO_COLOR") != "" { + s.LogNocolor = true + } +} + +func (s *Root) SetV2() { + if viper.IsSet("logs") { + if viper.GetBool("logs") { + logs := filepath.Join(".", "logs") + if runtime.GOOS == "linux" { + logs = "/var/log/neko" + } + s.LogDir = logs + } else { + s.LogDir = "" + } + log.Warn().Msg("you are using v2 configuration 'NEKO_LOGS' which is deprecated, please use 'NEKO_LOG_DIR=/path/to/logs' instead") + } } diff --git a/server/internal/config/server.go b/server/internal/config/server.go index a06f5a89..72b891d5 100644 --- a/server/internal/config/server.go +++ b/server/internal/config/server.go @@ -1,13 +1,13 @@ package config import ( - "net/http" "path" + "github.com/rs/zerolog/log" "github.com/spf13/cobra" "github.com/spf13/viper" - "m1k1o/neko/internal/utils" + "github.com/demodesk/neko/pkg/utils" ) type Server struct { @@ -17,41 +17,92 @@ type Server struct { Proxy bool Static string PathPrefix string + PProf bool + Metrics bool CORS []string } func (Server) Init(cmd *cobra.Command) error { - cmd.PersistentFlags().String("bind", "127.0.0.1:8080", "address/port/socket to serve neko") + cmd.PersistentFlags().String("server.bind", "127.0.0.1:8080", "address/port/socket to serve neko") + if err := viper.BindPFlag("server.bind", cmd.PersistentFlags().Lookup("server.bind")); err != nil { + return err + } + + cmd.PersistentFlags().String("server.cert", "", "path to the SSL cert used to secure the neko server") + if err := viper.BindPFlag("server.cert", cmd.PersistentFlags().Lookup("server.cert")); err != nil { + return err + } + + cmd.PersistentFlags().String("server.key", "", "path to the SSL key used to secure the neko server") + if err := viper.BindPFlag("server.key", cmd.PersistentFlags().Lookup("server.key")); err != nil { + return err + } + + cmd.PersistentFlags().Bool("server.proxy", false, "trust reverse proxy headers") + if err := viper.BindPFlag("server.proxy", cmd.PersistentFlags().Lookup("server.proxy")); err != nil { + return err + } + + cmd.PersistentFlags().String("server.static", "", "path to neko client files to serve") + if err := viper.BindPFlag("server.static", cmd.PersistentFlags().Lookup("server.static")); err != nil { + return err + } + + cmd.PersistentFlags().String("server.path_prefix", "/", "path prefix for HTTP requests") + if err := viper.BindPFlag("server.path_prefix", cmd.PersistentFlags().Lookup("server.path_prefix")); err != nil { + return err + } + + cmd.PersistentFlags().Bool("server.pprof", false, "enable pprof endpoint available at /debug/pprof") + if err := viper.BindPFlag("server.pprof", cmd.PersistentFlags().Lookup("server.pprof")); err != nil { + return err + } + + cmd.PersistentFlags().Bool("server.metrics", true, "enable prometheus metrics available at /metrics") + if err := viper.BindPFlag("server.metrics", cmd.PersistentFlags().Lookup("server.metrics")); err != nil { + return err + } + + cmd.PersistentFlags().StringSlice("server.cors", []string{}, "list of allowed origins for CORS, if empty CORS is disabled, if '*' is present all origins are allowed") + if err := viper.BindPFlag("server.cors", cmd.PersistentFlags().Lookup("server.cors")); err != nil { + return err + } + + return nil +} + +func (Server) InitV2(cmd *cobra.Command) error { + cmd.PersistentFlags().String("bind", "", "V2: address/port/socket to serve neko") if err := viper.BindPFlag("bind", cmd.PersistentFlags().Lookup("bind")); err != nil { return err } - cmd.PersistentFlags().String("cert", "", "path to the SSL cert used to secure the neko server") + cmd.PersistentFlags().String("cert", "", "V2: path to the SSL cert used to secure the neko server") if err := viper.BindPFlag("cert", cmd.PersistentFlags().Lookup("cert")); err != nil { return err } - cmd.PersistentFlags().String("key", "", "path to the SSL key used to secure the neko server") + cmd.PersistentFlags().String("key", "", "V2: path to the SSL key used to secure the neko server") if err := viper.BindPFlag("key", cmd.PersistentFlags().Lookup("key")); err != nil { return err } - cmd.PersistentFlags().Bool("proxy", false, "enable reverse proxy mode") + cmd.PersistentFlags().Bool("proxy", false, "V2: enable reverse proxy mode") if err := viper.BindPFlag("proxy", cmd.PersistentFlags().Lookup("proxy")); err != nil { return err } - cmd.PersistentFlags().String("static", "./www", "path to neko client files to serve") + cmd.PersistentFlags().String("static", "", "V2: path to neko client files to serve") if err := viper.BindPFlag("static", cmd.PersistentFlags().Lookup("static")); err != nil { return err } - cmd.PersistentFlags().String("path_prefix", "/", "path prefix for HTTP requests") + cmd.PersistentFlags().String("path_prefix", "", "V2: path prefix for HTTP requests") if err := viper.BindPFlag("path_prefix", cmd.PersistentFlags().Lookup("path_prefix")); err != nil { return err } - cmd.PersistentFlags().StringSlice("cors", []string{"*"}, "list of allowed origins for CORS") + cmd.PersistentFlags().StringSlice("cors", []string{}, "V2: list of allowed origins for CORS") if err := viper.BindPFlag("cors", cmd.PersistentFlags().Lookup("cors")); err != nil { return err } @@ -60,21 +111,68 @@ func (Server) Init(cmd *cobra.Command) error { } func (s *Server) Set() { - s.Cert = viper.GetString("cert") - s.Key = viper.GetString("key") - s.Bind = viper.GetString("bind") - s.Proxy = viper.GetBool("proxy") - s.Static = viper.GetString("static") - s.PathPrefix = path.Join("/", path.Clean(viper.GetString("path_prefix"))) + s.Cert = viper.GetString("server.cert") + s.Key = viper.GetString("server.key") + s.Bind = viper.GetString("server.bind") + s.Proxy = viper.GetBool("server.proxy") + s.Static = viper.GetString("server.static") + s.PathPrefix = path.Join("/", path.Clean(viper.GetString("server.path_prefix"))) + s.PProf = viper.GetBool("server.pprof") + s.Metrics = viper.GetBool("server.metrics") - s.CORS = viper.GetStringSlice("cors") + s.CORS = viper.GetStringSlice("server.cors") in, _ := utils.ArrayIn("*", s.CORS) if len(s.CORS) == 0 || in { s.CORS = []string{"*"} } } -func (s *Server) AllowOrigin(r *http.Request, origin string) bool { +func (s *Server) SetV2() { + 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") + } + 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") + } + 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") + } + 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") + } + 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") + } + 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") + } + if viper.IsSet("cors") { + s.CORS = viper.GetStringSlice("cors") + in, _ := utils.ArrayIn("*", s.CORS) + if len(s.CORS) == 0 || in { + s.CORS = []string{"*"} + } + log.Warn().Msg("you are using v2 configuration 'NEKO_CORS' which is deprecated, please use 'NEKO_SERVER_CORS' instead") + } +} + +func (s *Server) HasCors() bool { + return len(s.CORS) > 0 +} + +func (s *Server) AllowOrigin(origin string) bool { + // if CORS is disabled, allow all origins + if len(s.CORS) == 0 { + return true + } + + // if CORS is enabled, allow only origins in the list in, _ := utils.ArrayIn(origin, s.CORS) return in || s.CORS[0] == "*" } diff --git a/internal/config/session.go b/server/internal/config/session.go similarity index 60% rename from internal/config/session.go rename to server/internal/config/session.go index 6ab4776f..08934ab4 100644 --- a/internal/config/session.go +++ b/server/internal/config/session.go @@ -3,6 +3,7 @@ package config import ( "time" + "github.com/rs/zerolog/log" "github.com/spf13/cobra" "github.com/spf13/viper" ) @@ -11,7 +12,9 @@ type Session struct { File string PrivateMode bool + LockedLogins bool LockedControls bool + ControlProtection bool ImplicitHosting bool InactiveCursors bool MercifulReconnect bool @@ -34,11 +37,21 @@ func (Session) Init(cmd *cobra.Command) error { return err } + cmd.PersistentFlags().Bool("session.locked_logins", false, "whether logins should be locked for users initially") + if err := viper.BindPFlag("session.locked_logins", cmd.PersistentFlags().Lookup("session.locked_logins")); err != nil { + return err + } + cmd.PersistentFlags().Bool("session.locked_controls", false, "whether controls should be locked for users initially") if err := viper.BindPFlag("session.locked_controls", cmd.PersistentFlags().Lookup("session.locked_controls")); err != nil { return err } + cmd.PersistentFlags().Bool("session.control_protection", false, "users can gain control only if at least one admin is in the room") + if err := viper.BindPFlag("session.control_protection", cmd.PersistentFlags().Lookup("session.control_protection")); err != nil { + return err + } + cmd.PersistentFlags().Bool("session.implicit_hosting", true, "allow implicit control switching") if err := viper.BindPFlag("session.implicit_hosting", cmd.PersistentFlags().Lookup("session.implicit_hosting")); err != nil { return err @@ -83,11 +96,32 @@ func (Session) Init(cmd *cobra.Command) error { return nil } +func (Session) InitV2(cmd *cobra.Command) error { + cmd.PersistentFlags().StringSlice("locks", []string{}, "V2: resources, that will be locked when starting (control, login)") + if err := viper.BindPFlag("locks", cmd.PersistentFlags().Lookup("locks")); err != nil { + return err + } + + cmd.PersistentFlags().Bool("control_protection", false, "V2: control protection means, users can gain control only if at least one admin is in the room") + if err := viper.BindPFlag("control_protection", cmd.PersistentFlags().Lookup("control_protection")); err != nil { + return err + } + + cmd.PersistentFlags().Bool("implicit_control", false, "V2: if enabled members can gain control implicitly") + if err := viper.BindPFlag("implicit_control", cmd.PersistentFlags().Lookup("implicit_control")); err != nil { + return err + } + + return nil +} + func (s *Session) Set() { s.File = viper.GetString("session.file") s.PrivateMode = viper.GetBool("session.private_mode") + s.LockedLogins = viper.GetBool("session.locked_logins") s.LockedControls = viper.GetBool("session.locked_controls") + s.ControlProtection = viper.GetBool("session.control_protection") s.ImplicitHosting = viper.GetBool("session.implicit_hosting") s.InactiveCursors = viper.GetBool("session.inactive_cursors") s.MercifulReconnect = viper.GetBool("session.merciful_reconnect") @@ -98,3 +132,28 @@ func (s *Session) Set() { s.CookieExpiration = time.Duration(viper.GetInt("session.cookie.expiration")) * time.Hour s.CookieSecure = viper.GetBool("session.cookie.secure") } + +func (s *Session) SetV2() { + if viper.IsSet("locks") { + locks := viper.GetStringSlice("locks") + for _, lock := range locks { + switch lock { + // TODO: file_transfer + case "control": + s.LockedControls = true + case "login": + s.LockedLogins = true + } + } + 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") + } + + 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") + } + 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") + } +} diff --git a/server/internal/config/webrtc.go b/server/internal/config/webrtc.go index d7dcb1b9..0b421134 100644 --- a/server/internal/config/webrtc.go +++ b/server/internal/config/webrtc.go @@ -4,131 +4,396 @@ import ( "encoding/json" "strconv" "strings" - - "m1k1o/neko/internal/utils" + "time" "github.com/rs/zerolog/log" "github.com/spf13/cobra" "github.com/spf13/viper" - "github.com/pion/webrtc/v3" + "github.com/demodesk/neko/pkg/types" + "github.com/demodesk/neko/pkg/utils" ) -type WebRTC struct { - ICELite bool - ICEServers []webrtc.ICEServer - EphemeralMin uint16 - EphemeralMax uint16 - NAT1To1IPs []string - TCPMUX int - UDPMUX int +// default stun server +const defStunSrv = "stun:stun.l.google.com:19302" - ImplicitControl bool +type WebRTCEstimator struct { + Enabled bool + Passive bool + Debug bool + InitialBitrate int + + // how often to read and process bandwidth estimation reports + ReadInterval time.Duration + // how long to wait for stable connection (only neutral or upward trend) before upgrading + StableDuration time.Duration + // how long to wait for unstable connection (downward trend) before downgrading + UnstableDuration time.Duration + // how long to wait for stalled connection (neutral trend with low bandwidth) before downgrading + StalledDuration time.Duration + // how long to wait before downgrading again after previous downgrade + DowngradeBackoff time.Duration + // how long to wait before upgrading again after previous upgrade + UpgradeBackoff time.Duration + // how bigger the difference between estimated and stream bitrate must be to trigger upgrade/downgrade + DiffThreshold float64 +} + +type WebRTC struct { + ICELite bool + ICETrickle bool + ICEServersFrontend []types.ICEServer + ICEServersBackend []types.ICEServer + EphemeralMin uint16 + EphemeralMax uint16 + TCPMux int + UDPMux int + + NAT1To1IPs []string + IpRetrievalUrl string + + Estimator WebRTCEstimator } func (WebRTC) Init(cmd *cobra.Command) error { - cmd.PersistentFlags().String("epr", "59000-59100", "limits the pool of ephemeral ports that ICE UDP connections can allocate from") + cmd.PersistentFlags().Bool("webrtc.icelite", false, "configures whether or not the ICE agent should be a lite agent") + if err := viper.BindPFlag("webrtc.icelite", cmd.PersistentFlags().Lookup("webrtc.icelite")); err != nil { + return err + } + + cmd.PersistentFlags().Bool("webrtc.icetrickle", true, "configures whether cadidates should be sent asynchronously using Trickle ICE") + if err := viper.BindPFlag("webrtc.icetrickle", cmd.PersistentFlags().Lookup("webrtc.icetrickle")); err != nil { + return err + } + + // Looks like this is conflicting with the frontend and backend ICE servers since latest versions + //cmd.PersistentFlags().String("webrtc.iceservers", "[]", "Global STUN and TURN servers in JSON format with `urls`, `username` and `credential` keys") + //if err := viper.BindPFlag("webrtc.iceservers", cmd.PersistentFlags().Lookup("webrtc.iceservers")); err != nil { + // return err + //} + + cmd.PersistentFlags().String("webrtc.iceservers.frontend", "[]", "Frontend only STUN and TURN servers in JSON format with `urls`, `username` and `credential` keys") + if err := viper.BindPFlag("webrtc.iceservers.frontend", cmd.PersistentFlags().Lookup("webrtc.iceservers.frontend")); err != nil { + return err + } + + cmd.PersistentFlags().String("webrtc.iceservers.backend", "[]", "Backend only STUN and TURN servers in JSON format with `urls`, `username` and `credential` keys") + if err := viper.BindPFlag("webrtc.iceservers.backend", cmd.PersistentFlags().Lookup("webrtc.iceservers.backend")); err != nil { + return err + } + + cmd.PersistentFlags().String("webrtc.epr", "", "limits the pool of ephemeral ports that ICE UDP connections can allocate from") + if err := viper.BindPFlag("webrtc.epr", cmd.PersistentFlags().Lookup("webrtc.epr")); err != nil { + return err + } + + cmd.PersistentFlags().Int("webrtc.tcpmux", 0, "single TCP mux port for all peers") + if err := viper.BindPFlag("webrtc.tcpmux", cmd.PersistentFlags().Lookup("webrtc.tcpmux")); err != nil { + return err + } + + cmd.PersistentFlags().Int("webrtc.udpmux", 0, "single UDP mux port for all peers, replaces EPR") + if err := viper.BindPFlag("webrtc.udpmux", cmd.PersistentFlags().Lookup("webrtc.udpmux")); err != nil { + return err + } + + cmd.PersistentFlags().StringSlice("webrtc.nat1to1", []string{}, "sets a list of external IP addresses of 1:1 (D)NAT and a candidate type for which the external IP address is used") + if err := viper.BindPFlag("webrtc.nat1to1", cmd.PersistentFlags().Lookup("webrtc.nat1to1")); err != nil { + return err + } + + cmd.PersistentFlags().String("webrtc.ip_retrieval_url", "https://checkip.amazonaws.com", "URL address used for retrieval of the external IP address") + if err := viper.BindPFlag("webrtc.ip_retrieval_url", cmd.PersistentFlags().Lookup("webrtc.ip_retrieval_url")); err != nil { + return err + } + + // bandwidth estimator + + cmd.PersistentFlags().Bool("webrtc.estimator.enabled", false, "enables the bandwidth estimator") + if err := viper.BindPFlag("webrtc.estimator.enabled", cmd.PersistentFlags().Lookup("webrtc.estimator.enabled")); err != nil { + return err + } + + cmd.PersistentFlags().Bool("webrtc.estimator.passive", false, "passive estimator mode, when it does not switch pipelines, only estimates") + if err := viper.BindPFlag("webrtc.estimator.passive", cmd.PersistentFlags().Lookup("webrtc.estimator.passive")); err != nil { + return err + } + + cmd.PersistentFlags().Bool("webrtc.estimator.debug", false, "enables debug logging for the bandwidth estimator") + if err := viper.BindPFlag("webrtc.estimator.debug", cmd.PersistentFlags().Lookup("webrtc.estimator.debug")); err != nil { + return err + } + + cmd.PersistentFlags().Int("webrtc.estimator.initial_bitrate", 1_000_000, "initial bitrate for the bandwidth estimator") + if err := viper.BindPFlag("webrtc.estimator.initial_bitrate", cmd.PersistentFlags().Lookup("webrtc.estimator.initial_bitrate")); err != nil { + return err + } + + cmd.PersistentFlags().Duration("webrtc.estimator.read_interval", 2*time.Second, "how often to read and process bandwidth estimation reports") + if err := viper.BindPFlag("webrtc.estimator.read_interval", cmd.PersistentFlags().Lookup("webrtc.estimator.read_interval")); err != nil { + return err + } + + cmd.PersistentFlags().Duration("webrtc.estimator.stable_duration", 12*time.Second, "how long to wait for stable connection (upward or neutral trend) before upgrading") + if err := viper.BindPFlag("webrtc.estimator.stable_duration", cmd.PersistentFlags().Lookup("webrtc.estimator.stable_duration")); err != nil { + return err + } + + cmd.PersistentFlags().Duration("webrtc.estimator.unstable_duration", 6*time.Second, "how long to wait for stalled connection (neutral trend with low bandwidth) before downgrading") + if err := viper.BindPFlag("webrtc.estimator.unstable_duration", cmd.PersistentFlags().Lookup("webrtc.estimator.unstable_duration")); err != nil { + return err + } + + cmd.PersistentFlags().Duration("webrtc.estimator.stalled_duration", 24*time.Second, "how long to wait for stalled bandwidth estimation before downgrading") + if err := viper.BindPFlag("webrtc.estimator.stalled_duration", cmd.PersistentFlags().Lookup("webrtc.estimator.stalled_duration")); err != nil { + return err + } + + cmd.PersistentFlags().Duration("webrtc.estimator.downgrade_backoff", 10*time.Second, "how long to wait before downgrading again after previous downgrade") + if err := viper.BindPFlag("webrtc.estimator.downgrade_backoff", cmd.PersistentFlags().Lookup("webrtc.estimator.downgrade_backoff")); err != nil { + return err + } + + cmd.PersistentFlags().Duration("webrtc.estimator.upgrade_backoff", 5*time.Second, "how long to wait before upgrading again after previous upgrade") + if err := viper.BindPFlag("webrtc.estimator.upgrade_backoff", cmd.PersistentFlags().Lookup("webrtc.estimator.upgrade_backoff")); err != nil { + return err + } + + cmd.PersistentFlags().Float64("webrtc.estimator.diff_threshold", 0.15, "how bigger the difference between estimated and stream bitrate must be to trigger upgrade/downgrade") + if err := viper.BindPFlag("webrtc.estimator.diff_threshold", cmd.PersistentFlags().Lookup("webrtc.estimator.diff_threshold")); err != nil { + return err + } + + return nil +} + +func (WebRTC) InitV2(cmd *cobra.Command) error { + cmd.PersistentFlags().String("epr", "", "V2: limits the pool of ephemeral ports that ICE UDP connections can allocate from") if err := viper.BindPFlag("epr", cmd.PersistentFlags().Lookup("epr")); err != nil { return err } - cmd.PersistentFlags().StringSlice("nat1to1", []string{}, "sets a list of external IP addresses of 1:1 (D)NAT and a candidate type for which the external IP address is used") + cmd.PersistentFlags().StringSlice("nat1to1", []string{}, "V2: sets a list of external IP addresses of 1:1 (D)NAT and a candidate type for which the external IP address is used") if err := viper.BindPFlag("nat1to1", cmd.PersistentFlags().Lookup("nat1to1")); err != nil { return err } - cmd.PersistentFlags().Int("tcpmux", 0, "single TCP mux port for all peers") + cmd.PersistentFlags().Int("tcpmux", 0, "V2: single TCP mux port for all peers") if err := viper.BindPFlag("tcpmux", cmd.PersistentFlags().Lookup("tcpmux")); err != nil { return err } - cmd.PersistentFlags().Int("udpmux", 0, "single UDP mux port for all peers") + cmd.PersistentFlags().Int("udpmux", 0, "V2: single UDP mux port for all peers") if err := viper.BindPFlag("udpmux", cmd.PersistentFlags().Lookup("udpmux")); err != nil { return err } - cmd.PersistentFlags().String("ipfetch", "http://checkip.amazonaws.com", "automatically fetch IP address from given URL when nat1to1 is not present") + cmd.PersistentFlags().String("ipfetch", "", "V2: automatically fetch IP address from given URL when nat1to1 is not present") if err := viper.BindPFlag("ipfetch", cmd.PersistentFlags().Lookup("ipfetch")); err != nil { return err } - cmd.PersistentFlags().Bool("icelite", false, "configures whether or not the ice agent should be a lite agent") + cmd.PersistentFlags().Bool("icelite", false, "V2: configures whether or not the ice agent should be a lite agent") if err := viper.BindPFlag("icelite", cmd.PersistentFlags().Lookup("icelite")); err != nil { return err } - cmd.PersistentFlags().StringSlice("iceserver", []string{"stun:stun.l.google.com:19302"}, "describes a single STUN and TURN server that can be used by the ICEAgent to establish a connection with a peer") + cmd.PersistentFlags().StringSlice("iceserver", []string{}, "V2: describes a single STUN and TURN server that can be used by the ICEAgent to establish a connection with a peer") if err := viper.BindPFlag("iceserver", cmd.PersistentFlags().Lookup("iceserver")); err != nil { return err } - cmd.PersistentFlags().String("iceservers", "", "describes a single STUN and TURN server that can be used by the ICEAgent to establish a connection with a peer") + cmd.PersistentFlags().String("iceservers", "", "V2: describes a single STUN and TURN server that can be used by the ICEAgent to establish a connection with a peer") if err := viper.BindPFlag("iceservers", cmd.PersistentFlags().Lookup("iceservers")); err != nil { return err } - // TODO: Should be moved to session config. - cmd.PersistentFlags().Bool("implicit_control", false, "if enabled members can gain control implicitly") - if err := viper.BindPFlag("implicit_control", cmd.PersistentFlags().Lookup("implicit_control")); err != nil { - return err - } - return nil } func (s *WebRTC) Set() { - s.NAT1To1IPs = viper.GetStringSlice("nat1to1") - s.TCPMUX = viper.GetInt("tcpmux") - s.UDPMUX = viper.GetInt("udpmux") - s.ICELite = viper.GetBool("icelite") - s.ICEServers = []webrtc.ICEServer{} + s.ICELite = viper.GetBool("webrtc.icelite") + s.ICETrickle = viper.GetBool("webrtc.icetrickle") - iceServersJson := viper.GetString("iceservers") - if iceServersJson != "" { - err := json.Unmarshal([]byte(iceServersJson), &s.ICEServers) - if err != nil { - log.Panic().Err(err).Msg("failed to process iceservers") + // parse frontend ice servers + if err := viper.UnmarshalKey("webrtc.iceservers.frontend", &s.ICEServersFrontend, viper.DecodeHook( + utils.JsonStringAutoDecode([]types.ICEServer{}), + )); err != nil { + log.Warn().Err(err).Msgf("unable to parse frontend ICE servers") + } + + // parse backend ice servers + if err := viper.UnmarshalKey("webrtc.iceservers.backend", &s.ICEServersBackend, viper.DecodeHook( + utils.JsonStringAutoDecode([]types.ICEServer{}), + )); err != nil { + log.Warn().Err(err).Msgf("unable to parse backend ICE servers") + } + + if s.ICELite && len(s.ICEServersBackend) > 0 { + log.Warn().Msgf("ICE Lite is enabled, but backend ICE servers are configured. Backend ICE servers will be ignored.") + } + + // if no frontend or backend ice servers are configured + if len(s.ICEServersFrontend) == 0 && len(s.ICEServersBackend) == 0 { + // parse global ice servers + var iceServers []types.ICEServer + if err := viper.UnmarshalKey("webrtc.iceservers", &iceServers, viper.DecodeHook( + utils.JsonStringAutoDecode([]types.ICEServer{}), + )); err != nil { + log.Warn().Err(err).Msgf("unable to parse global ICE servers") + } + + // add default stun server if none are configured + if len(iceServers) == 0 { + iceServers = append(iceServers, types.ICEServer{ + URLs: []string{defStunSrv}, + }) + } + + s.ICEServersFrontend = append(s.ICEServersFrontend, iceServers...) + s.ICEServersBackend = append(s.ICEServersBackend, iceServers...) + } + + s.TCPMux = viper.GetInt("webrtc.tcpmux") + s.UDPMux = viper.GetInt("webrtc.udpmux") + + epr := viper.GetString("webrtc.epr") + if epr != "" { + ports := strings.SplitN(epr, "-", -1) + if len(ports) > 1 { + min, err := strconv.ParseUint(ports[0], 10, 16) + if err != nil { + log.Panic().Err(err).Msgf("unable to parse ephemeral min port") + } + + max, err := strconv.ParseUint(ports[1], 10, 16) + if err != nil { + log.Panic().Err(err).Msgf("unable to parse ephemeral max port") + } + + s.EphemeralMin = uint16(min) + s.EphemeralMax = uint16(max) + } + + if s.EphemeralMin > s.EphemeralMax { + log.Panic().Msgf("ephemeral min port cannot be bigger than max") } } - iceServerSlice := viper.GetStringSlice("iceserver") - if len(iceServerSlice) > 0 { - s.ICEServers = append(s.ICEServers, webrtc.ICEServer{URLs: iceServerSlice}) + if epr == "" && s.TCPMux == 0 && s.UDPMux == 0 { + // using default epr range + s.EphemeralMin = 59000 + s.EphemeralMax = 59100 + + log.Warn(). + Uint16("min", s.EphemeralMin). + Uint16("max", s.EphemeralMax). + Msgf("no TCP, UDP mux or epr specified, using default epr range") } - if len(s.NAT1To1IPs) == 0 { - ipfetch := viper.GetString("ipfetch") - ip, err := utils.GetIP(ipfetch) - if err != nil { - log.Panic().Err(err).Str("ipfetch", ipfetch).Msg("failed to fetch ip address") - } - s.NAT1To1IPs = append(s.NAT1To1IPs, ip) - } - - min := uint16(59000) - max := uint16(59100) - epr := viper.GetString("epr") - ports := strings.SplitN(epr, "-", -1) - if len(ports) > 1 { - start, err := strconv.ParseUint(ports[0], 10, 16) + s.NAT1To1IPs = viper.GetStringSlice("webrtc.nat1to1") + s.IpRetrievalUrl = viper.GetString("webrtc.ip_retrieval_url") + if s.IpRetrievalUrl != "" && len(s.NAT1To1IPs) == 0 { + ip, err := utils.HttpRequestGET(s.IpRetrievalUrl) if err == nil { - min = uint16(start) - } - - end, err := strconv.ParseUint(ports[1], 10, 16) - if err == nil { - max = uint16(end) + s.NAT1To1IPs = append(s.NAT1To1IPs, ip) + } else { + log.Warn().Err(err).Msgf("IP retrieval failed") } } - if min > max { - s.EphemeralMin = max - s.EphemeralMax = min - } else { - s.EphemeralMin = min - s.EphemeralMax = max - } + // bandwidth estimator - // TODO: Should be moved to session config. - s.ImplicitControl = viper.GetBool("implicit_control") + s.Estimator.Enabled = viper.GetBool("webrtc.estimator.enabled") + s.Estimator.Passive = viper.GetBool("webrtc.estimator.passive") + s.Estimator.Debug = viper.GetBool("webrtc.estimator.debug") + s.Estimator.InitialBitrate = viper.GetInt("webrtc.estimator.initial_bitrate") + s.Estimator.ReadInterval = viper.GetDuration("webrtc.estimator.read_interval") + s.Estimator.StableDuration = viper.GetDuration("webrtc.estimator.stable_duration") + s.Estimator.UnstableDuration = viper.GetDuration("webrtc.estimator.unstable_duration") + s.Estimator.StalledDuration = viper.GetDuration("webrtc.estimator.stalled_duration") + s.Estimator.DowngradeBackoff = viper.GetDuration("webrtc.estimator.downgrade_backoff") + s.Estimator.UpgradeBackoff = viper.GetDuration("webrtc.estimator.upgrade_backoff") + s.Estimator.DiffThreshold = viper.GetFloat64("webrtc.estimator.diff_threshold") +} + +func (s *WebRTC) SetV2() { + 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") + } + 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") + } + 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") + } + 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") + } + + if viper.IsSet("iceservers") { + iceServers := []types.ICEServer{} + iceServersJson := viper.GetString("iceservers") + if iceServersJson != "" { + err := json.Unmarshal([]byte(iceServersJson), &iceServers) + if err != nil { + log.Panic().Err(err).Msg("failed to process iceservers") + } + } + 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") + } + + if viper.IsSet("iceserver") { + iceServerSlice := viper.GetStringSlice("iceserver") + if len(iceServerSlice) > 0 { + s.ICEServersFrontend = append(s.ICEServersFrontend, types.ICEServer{URLs: iceServerSlice}) + 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") + } + + if viper.IsSet("ipfetch") { + if len(s.NAT1To1IPs) == 0 { + ipfetch := viper.GetString("ipfetch") + ip, err := utils.HttpRequestGET(ipfetch) + if err != nil { + log.Panic().Err(err).Str("ipfetch", ipfetch).Msg("failed to fetch ip address") + } + 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") + } + + if viper.IsSet("epr") { + min := uint16(59000) + max := uint16(59100) + epr := viper.GetString("epr") + ports := strings.SplitN(epr, "-", -1) + if len(ports) > 1 { + start, err := strconv.ParseUint(ports[0], 10, 16) + if err == nil { + min = uint16(start) + } + + end, err := strconv.ParseUint(ports[1], 10, 16) + if err == nil { + max = uint16(end) + } + } + + if min > max { + s.EphemeralMin = max + s.EphemeralMax = min + } else { + s.EphemeralMin = min + s.EphemeralMax = max + } + log.Warn().Msg("you are using v2 configuration 'NEKO_EPR' which is deprecated, please use 'NEKO_WEBRTC_EPR' instead") + } } diff --git a/server/internal/config/websocket.go b/server/internal/config/websocket.go deleted file mode 100644 index 9bf97381..00000000 --- a/server/internal/config/websocket.go +++ /dev/null @@ -1,67 +0,0 @@ -package config - -import ( - "path/filepath" - - "github.com/spf13/cobra" - "github.com/spf13/viper" -) - -type WebSocket struct { - Password string - AdminPassword string - Locks []string - - ControlProtection bool - - FileTransferEnabled bool - FileTransferPath string -} - -func (WebSocket) Init(cmd *cobra.Command) error { - cmd.PersistentFlags().String("password", "neko", "password for connecting to stream") - if err := viper.BindPFlag("password", cmd.PersistentFlags().Lookup("password")); err != nil { - return err - } - - cmd.PersistentFlags().String("password_admin", "admin", "admin password for connecting to stream") - if err := viper.BindPFlag("password_admin", cmd.PersistentFlags().Lookup("password_admin")); err != nil { - return err - } - - cmd.PersistentFlags().StringSlice("locks", []string{}, "resources, that will be locked when starting (control, login)") - if err := viper.BindPFlag("locks", cmd.PersistentFlags().Lookup("locks")); err != nil { - return err - } - - cmd.PersistentFlags().Bool("control_protection", false, "control protection means, users can gain control only if at least one admin is in the room") - if err := viper.BindPFlag("control_protection", cmd.PersistentFlags().Lookup("control_protection")); err != nil { - return err - } - - // File transfer - - cmd.PersistentFlags().Bool("file_transfer_enabled", false, "enable file transfer feature") - if err := viper.BindPFlag("file_transfer_enabled", cmd.PersistentFlags().Lookup("file_transfer_enabled")); err != nil { - return err - } - - cmd.PersistentFlags().String("file_transfer_path", "/home/neko/Downloads", "path to use for file transfer") - if err := viper.BindPFlag("file_transfer_path", cmd.PersistentFlags().Lookup("file_transfer_path")); err != nil { - return err - } - - return nil -} - -func (s *WebSocket) Set() { - s.Password = viper.GetString("password") - s.AdminPassword = viper.GetString("password_admin") - s.Locks = viper.GetStringSlice("locks") - - s.ControlProtection = viper.GetBool("control_protection") - - s.FileTransferEnabled = viper.GetBool("file_transfer_enabled") - s.FileTransferPath = viper.GetString("file_transfer_path") - s.FileTransferPath = filepath.Clean(s.FileTransferPath) -} diff --git a/server/internal/desktop/clipboard.go b/server/internal/desktop/clipboard.go index 036a1177..f354fe38 100644 --- a/server/internal/desktop/clipboard.go +++ b/server/internal/desktop/clipboard.go @@ -1,11 +1,122 @@ package desktop -import "m1k1o/neko/internal/desktop/clipboard" +import ( + "bytes" + "fmt" + "os/exec" + "strings" -func (manager *DesktopManagerCtx) ReadClipboard() string { - return clipboard.Read() + "github.com/demodesk/neko/pkg/types" + "github.com/demodesk/neko/pkg/xevent" +) + +func (manager *DesktopManagerCtx) ClipboardGetText() (*types.ClipboardText, error) { + text, err := manager.ClipboardGetBinary("STRING") + if err != nil { + return nil, err + } + + // Rich text must not always be available, can fail silently. + html, _ := manager.ClipboardGetBinary("text/html") + + return &types.ClipboardText{ + Text: string(text), + HTML: string(html), + }, nil } -func (manager *DesktopManagerCtx) WriteClipboard(data string) { - clipboard.Write(data) +func (manager *DesktopManagerCtx) ClipboardSetText(data types.ClipboardText) error { + // TODO: Refactor. + // Current implementation is unable to set multiple targets. HTML + // is set, if available. Otherwise plain text. + + if data.HTML != "" { + return manager.ClipboardSetBinary("text/html", []byte(data.HTML)) + } + + return manager.ClipboardSetBinary("STRING", []byte(data.Text)) +} + +func (manager *DesktopManagerCtx) ClipboardGetBinary(mime string) ([]byte, error) { + cmd := exec.Command("xclip", "-selection", "clipboard", "-out", "-target", mime) + + var stdout, stderr bytes.Buffer + cmd.Stdout = &stdout + cmd.Stderr = &stderr + + err := cmd.Run() + if err != nil { + msg := strings.TrimSpace(stderr.String()) + return nil, fmt.Errorf("%s", msg) + } + + return stdout.Bytes(), nil +} + +func (manager *DesktopManagerCtx) ClipboardSetBinary(mime string, data []byte) error { + cmd := exec.Command("xclip", "-selection", "clipboard", "-in", "-target", mime) + + var stderr bytes.Buffer + cmd.Stderr = &stderr + + stdin, err := cmd.StdinPipe() + if err != nil { + return err + } + + // TODO: Refactor. + // We need to wait until the data came to the clipboard. + wait := make(chan struct{}) + xevent.Emmiter.Once("clipboard-updated", func(payload ...any) { + wait <- struct{}{} + }) + + err = cmd.Start() + if err != nil { + msg := strings.TrimSpace(stderr.String()) + return fmt.Errorf("%s", msg) + } + + _, err = stdin.Write(data) + if err != nil { + return err + } + + stdin.Close() + + // TODO: Refactor. + // cmd.Wait() + <-wait + + return nil +} + +func (manager *DesktopManagerCtx) ClipboardGetTargets() ([]string, error) { + cmd := exec.Command("xclip", "-selection", "clipboard", "-out", "-target", "TARGETS") + + var stdout, stderr bytes.Buffer + cmd.Stdout = &stdout + cmd.Stderr = &stderr + + err := cmd.Run() + if err != nil { + msg := strings.TrimSpace(stderr.String()) + return nil, fmt.Errorf("%s", msg) + } + + var response []string + targets := strings.Split(stdout.String(), "\n") + for _, target := range targets { + if target == "" { + continue + } + + if !strings.Contains(target, "/") { + continue + } + + response = append(response, target) + } + + return response, nil } diff --git a/server/internal/desktop/clipboard/clipboard.c b/server/internal/desktop/clipboard/clipboard.c deleted file mode 100644 index c500b33e..00000000 --- a/server/internal/desktop/clipboard/clipboard.c +++ /dev/null @@ -1,21 +0,0 @@ -#include "clipboard.h" - -static clipboard_c *CLIPBOARD = NULL; - -clipboard_c *getClipboard(void) { - if (CLIPBOARD == NULL) { - CLIPBOARD = clipboard_new(NULL); - } - - return CLIPBOARD; -} - -void ClipboardSet(char *src) { - clipboard_c *cb = getClipboard(); - clipboard_set_text_ex(cb, src, strlen(src), 0); -} - -char *ClipboardGet() { - clipboard_c *cb = getClipboard(); - return clipboard_text_ex(cb, NULL, 0); -} diff --git a/server/internal/desktop/clipboard/clipboard.go b/server/internal/desktop/clipboard/clipboard.go deleted file mode 100644 index 5c5d89b4..00000000 --- a/server/internal/desktop/clipboard/clipboard.go +++ /dev/null @@ -1,35 +0,0 @@ -package clipboard - -/* -#cgo linux LDFLAGS: /usr/local/lib/libclipboard.a -lxcb - -#include "clipboard.h" -*/ -import "C" - -import ( - "sync" - "unsafe" -) - -var mu sync.Mutex - -func Read() string { - mu.Lock() - defer mu.Unlock() - - clipboardUnsafe := C.ClipboardGet() - defer C.free(unsafe.Pointer(clipboardUnsafe)) - - return C.GoString(clipboardUnsafe) -} - -func Write(data string) { - mu.Lock() - defer mu.Unlock() - - clipboardUnsafe := C.CString(data) - defer C.free(unsafe.Pointer(clipboardUnsafe)) - - C.ClipboardSet(clipboardUnsafe) -} diff --git a/server/internal/desktop/clipboard/clipboard.h b/server/internal/desktop/clipboard/clipboard.h deleted file mode 100644 index 5f5cf36a..00000000 --- a/server/internal/desktop/clipboard/clipboard.h +++ /dev/null @@ -1,9 +0,0 @@ -#pragma once - -#include -#include - -clipboard_c *getClipboard(void); - -void ClipboardSet(char *src); -char *ClipboardGet(); diff --git a/internal/desktop/drop.go b/server/internal/desktop/drop.go similarity index 100% rename from internal/desktop/drop.go rename to server/internal/desktop/drop.go diff --git a/internal/desktop/filechooserdialog.go b/server/internal/desktop/filechooserdialog.go similarity index 100% rename from internal/desktop/filechooserdialog.go rename to server/internal/desktop/filechooserdialog.go diff --git a/server/internal/desktop/manager.go b/server/internal/desktop/manager.go index 918efb08..da998df2 100644 --- a/server/internal/desktop/manager.go +++ b/server/internal/desktop/manager.go @@ -1,36 +1,47 @@ package desktop import ( - "fmt" "sync" "time" - "m1k1o/neko/internal/config" - "m1k1o/neko/internal/desktop/xevent" - "m1k1o/neko/internal/desktop/xorg" - + "github.com/kataras/go-events" "github.com/rs/zerolog" "github.com/rs/zerolog/log" + + "github.com/demodesk/neko/internal/config" + "github.com/demodesk/neko/pkg/types" + "github.com/demodesk/neko/pkg/xevent" + "github.com/demodesk/neko/pkg/xinput" + "github.com/demodesk/neko/pkg/xorg" ) var mu = sync.Mutex{} type DesktopManagerCtx struct { - logger zerolog.Logger - wg sync.WaitGroup - shutdown chan struct{} - config *config.Desktop - - screenSizeChangeChannel chan bool + logger zerolog.Logger + wg sync.WaitGroup + shutdown chan struct{} + emmiter events.EventEmmiter + config *config.Desktop + screenSize types.ScreenSize // cached screen size + input xinput.Driver } func New(config *config.Desktop) *DesktopManagerCtx { - return &DesktopManagerCtx{ - logger: log.With().Str("module", "desktop").Logger(), - shutdown: make(chan struct{}), - config: config, + var input xinput.Driver + if config.UseInputDriver { + input = xinput.NewDriver(config.InputSocket) + } else { + input = xinput.NewDummy() + } - screenSizeChangeChannel: make(chan bool), + return &DesktopManagerCtx{ + logger: log.With().Str("module", "desktop").Logger(), + shutdown: make(chan struct{}), + emmiter: events.New(), + config: config, + screenSize: config.ScreenSize, + input: input, } } @@ -39,31 +50,48 @@ func (manager *DesktopManagerCtx) Start() { manager.logger.Panic().Str("display", manager.config.Display).Msg("unable to open display") } + // X11 can throw errors below, and the default error handler exits + xevent.SetupErrorHandler() + xorg.GetScreenConfigurations() - err := xorg.ChangeScreenSize(manager.config.ScreenWidth, manager.config.ScreenHeight, manager.config.ScreenRate) - manager.logger.Err(err). - Str("screen_size", fmt.Sprintf("%dx%d@%d", manager.config.ScreenWidth, manager.config.ScreenHeight, manager.config.ScreenRate)). - Msgf("setting initial screen size") + screenSize, err := xorg.ChangeScreenSize(manager.config.ScreenSize) + if err != nil { + manager.logger.Err(err). + Str("screen_size", screenSize.String()). + Msgf("unable to set initial screen size") + } else { + // cache screen size + manager.screenSize = screenSize + manager.logger.Info(). + Str("screen_size", screenSize.String()). + Msgf("setting initial screen size") + } + err = manager.input.Connect() + if err != nil { + // TODO: fail silently to dummy driver? + manager.logger.Panic().Err(err).Msg("unable to connect to input driver") + } + + // set up event listeners + xevent.Unminimize = manager.config.Unminimize + xevent.FileChooserDialog = manager.config.FileChooserDialog go xevent.EventLoop(manager.config.Display) - go func() { - for { - msg, ok := <-xevent.EventErrorChannel - if !ok { - manager.logger.Info().Msg("xevent error channel was closed") - return - } + // in case it was opened + if manager.config.FileChooserDialog { + go manager.CloseFileChooserDialog() + } - manager.logger.Warn(). - Uint8("error_code", msg.Error_code). - Str("message", msg.Message). - Uint8("request_code", msg.Request_code). - Uint8("minor_code", msg.Minor_code). - Msg("X event error occurred") - } - }() + manager.OnEventError(func(error_code uint8, message string, request_code uint8, minor_code uint8) { + manager.logger.Warn(). + Uint8("error_code", error_code). + Str("message", message). + Uint8("request_code", request_code). + Uint8("minor_code", minor_code). + Msg("X event error occured") + }) manager.wg.Add(1) @@ -73,26 +101,36 @@ func (manager *DesktopManagerCtx) Start() { ticker := time.NewTicker(1 * time.Second) defer ticker.Stop() + const debounceDuration = 10 * time.Second + for { select { case <-manager.shutdown: return case <-ticker.C: - xorg.CheckKeys(time.Second * 10) + xorg.CheckKeys(debounceDuration) + manager.input.Debounce(debounceDuration) } } }() } -func (manager *DesktopManagerCtx) GetScreenSizeChangeChannel() chan bool { - return manager.screenSizeChangeChannel +func (manager *DesktopManagerCtx) OnBeforeScreenSizeChange(listener func()) { + manager.emmiter.On("before_screen_size_change", func(payload ...any) { + listener() + }) +} + +func (manager *DesktopManagerCtx) OnAfterScreenSizeChange(listener func()) { + manager.emmiter.On("after_screen_size_change", func(payload ...any) { + listener() + }) } func (manager *DesktopManagerCtx) Shutdown() error { - manager.logger.Info().Msgf("desktop shutting down") + manager.logger.Info().Msgf("shutdown") close(manager.shutdown) - close(manager.screenSizeChangeChannel) manager.wg.Wait() xorg.DisplayClose() diff --git a/server/internal/desktop/xevent.go b/server/internal/desktop/xevent.go index b38a9e4e..59df1d7e 100644 --- a/server/internal/desktop/xevent.go +++ b/server/internal/desktop/xevent.go @@ -1,18 +1,35 @@ package desktop import ( - "m1k1o/neko/internal/desktop/xevent" - "m1k1o/neko/internal/types" + "github.com/demodesk/neko/pkg/xevent" ) -func (manager *DesktopManagerCtx) GetCursorChangedChannel() chan uint64 { - return xevent.CursorChangedChannel +func (manager *DesktopManagerCtx) OnCursorChanged(listener func(serial uint64)) { + xevent.Emmiter.On("cursor-changed", func(payload ...any) { + listener(payload[0].(uint64)) + }) } -func (manager *DesktopManagerCtx) GetClipboardUpdatedChannel() chan struct{} { - return xevent.ClipboardUpdatedChannel +func (manager *DesktopManagerCtx) OnClipboardUpdated(listener func()) { + xevent.Emmiter.On("clipboard-updated", func(payload ...any) { + listener() + }) } -func (manager *DesktopManagerCtx) GetEventErrorChannel() chan types.DesktopErrorMessage { - return xevent.EventErrorChannel +func (manager *DesktopManagerCtx) OnFileChooserDialogOpened(listener func()) { + xevent.Emmiter.On("file-chooser-dialog-opened", func(payload ...any) { + listener() + }) +} + +func (manager *DesktopManagerCtx) OnFileChooserDialogClosed(listener func()) { + xevent.Emmiter.On("file-chooser-dialog-closed", func(payload ...any) { + listener() + }) +} + +func (manager *DesktopManagerCtx) OnEventError(listener func(error_code uint8, message string, request_code uint8, minor_code uint8)) { + xevent.Emmiter.On("event-error", func(payload ...any) { + listener(payload[0].(uint8), payload[1].(string), payload[2].(uint8), payload[3].(uint8)) + }) } diff --git a/server/internal/desktop/xevent/xevent.c b/server/internal/desktop/xevent/xevent.c deleted file mode 100644 index 76942953..00000000 --- a/server/internal/desktop/xevent/xevent.c +++ /dev/null @@ -1,81 +0,0 @@ -#include "xevent.h" - -static int XEventError(Display *display, XErrorEvent *event) { - char message[100]; - - int error; - error = XGetErrorText(display, event->error_code, message, sizeof(message)); - if (error) { - goXEventError(event, "Could not get error message."); - } else { - goXEventError(event, message); - } - - return 1; -} - -void XEventLoop(char *name) { - Display *display = XOpenDisplay(name); - Window root = RootWindow(display, 0); - - int xfixes_event_base, xfixes_error_base; - if (!XFixesQueryExtension(display, &xfixes_event_base, &xfixes_error_base)) { - return; - } - - Atom WM_WINDOW_ROLE = XInternAtom(display, "WM_WINDOW_ROLE", 1); - Atom XA_CLIPBOARD = XInternAtom(display, "CLIPBOARD", 0); - XFixesSelectSelectionInput(display, root, XA_CLIPBOARD, XFixesSetSelectionOwnerNotifyMask); - XFixesSelectCursorInput(display, root, XFixesDisplayCursorNotifyMask); - XSelectInput(display, root, SubstructureNotifyMask); - - XSync(display, 0); - XSetErrorHandler(XEventError); - - while (goXEventActive()) { - XEvent event; - XNextEvent(display, &event); - - // XFixesDisplayCursorNotify - if (event.type == xfixes_event_base + 1) { - XFixesCursorNotifyEvent notifyEvent = *((XFixesCursorNotifyEvent *) &event); - if (notifyEvent.subtype == XFixesDisplayCursorNotify) { - goXEventCursorChanged(notifyEvent); - continue; - } - } - - // XFixesSelectionNotifyEvent - if (event.type == xfixes_event_base + XFixesSelectionNotify) { - XFixesSelectionNotifyEvent notifyEvent = *((XFixesSelectionNotifyEvent *) &event); - if (notifyEvent.subtype == XFixesSetSelectionOwnerNotify && notifyEvent.selection == XA_CLIPBOARD) { - goXEventClipboardUpdated(); - continue; - } - } - - // ConfigureNotify - if (event.type == ConfigureNotify) { - Window window = event.xconfigure.window; - - char *name; - XFetchName(display, window, &name); - - XTextProperty role; - XGetTextProperty(display, window, &role, WM_WINDOW_ROLE); - - goXEventConfigureNotify(display, window, name, role.value); - XFree(name); - continue; - } - - // UnmapNotify - if (event.type == UnmapNotify) { - Window window = event.xunmap.window; - goXEventUnmapNotify(window); - continue; - } - } - - XCloseDisplay(display); -} diff --git a/server/internal/desktop/xevent/xevent.go b/server/internal/desktop/xevent/xevent.go deleted file mode 100644 index 8c3553a7..00000000 --- a/server/internal/desktop/xevent/xevent.go +++ /dev/null @@ -1,78 +0,0 @@ -package xevent - -/* -#cgo LDFLAGS: -lX11 -lXfixes - -#include "xevent.h" -*/ -import "C" - -import ( - "unsafe" - - "m1k1o/neko/internal/types" -) - -var CursorChangedChannel chan uint64 -var ClipboardUpdatedChannel chan struct{} -var EventErrorChannel chan types.DesktopErrorMessage - -func init() { - CursorChangedChannel = make(chan uint64) - ClipboardUpdatedChannel = make(chan struct{}) - EventErrorChannel = make(chan types.DesktopErrorMessage) - - go func() { - for { - // TODO: Reserved for future use. - <-CursorChangedChannel - } - }() -} - -func EventLoop(display string) { - displayUnsafe := C.CString(display) - defer C.free(unsafe.Pointer(displayUnsafe)) - - C.XEventLoop(displayUnsafe) -} - -// TODO: Shutdown function. -//close(CursorChangedChannel) -//close(ClipboardUpdatedChannel) -//close(EventErrorChannel) - -//export goXEventCursorChanged -func goXEventCursorChanged(event C.XFixesCursorNotifyEvent) { - CursorChangedChannel <- uint64(event.cursor_serial) -} - -//export goXEventClipboardUpdated -func goXEventClipboardUpdated() { - ClipboardUpdatedChannel <- struct{}{} -} - -//export goXEventConfigureNotify -func goXEventConfigureNotify(display *C.Display, window C.Window, name *C.char, role *C.char) { - -} - -//export goXEventUnmapNotify -func goXEventUnmapNotify(window C.Window) { - -} - -//export goXEventError -func goXEventError(event *C.XErrorEvent, message *C.char) { - EventErrorChannel <- types.DesktopErrorMessage{ - Error_code: uint8(event.error_code), - Message: C.GoString(message), - Request_code: uint8(event.request_code), - Minor_code: uint8(event.minor_code), - } -} - -//export goXEventActive -func goXEventActive() C.int { - return C.int(1) -} diff --git a/server/internal/desktop/xevent/xevent.h b/server/internal/desktop/xevent/xevent.h deleted file mode 100644 index 3dfdfc0b..00000000 --- a/server/internal/desktop/xevent/xevent.h +++ /dev/null @@ -1,20 +0,0 @@ -#pragma once - -#include -#include -#include -#include -#include -#include - -extern void goXEventCursorChanged(XFixesCursorNotifyEvent event); -extern void goXEventClipboardUpdated(); -extern void goXEventConfigureNotify(Display *display, Window window, char *name, char *role); -extern void goXEventUnmapNotify(Window window); -extern void goXEventError(XErrorEvent *event, char *message); -extern int goXEventActive(); - -static int XEventError(Display *display, XErrorEvent *event); -void XEventLoop(char *display); - -void XFileChooserHide(Display *display, Window window); diff --git a/internal/desktop/xinput.go b/server/internal/desktop/xinput.go similarity index 100% rename from internal/desktop/xinput.go rename to server/internal/desktop/xinput.go diff --git a/server/internal/desktop/xorg.go b/server/internal/desktop/xorg.go index 3008eaae..c0feb5dc 100644 --- a/server/internal/desktop/xorg.go +++ b/server/internal/desktop/xorg.go @@ -6,8 +6,8 @@ import ( "regexp" "time" - "m1k1o/neko/internal/desktop/xorg" - "m1k1o/neko/internal/types" + "github.com/demodesk/neko/pkg/types" + "github.com/demodesk/neko/pkg/xorg" ) func (manager *DesktopManagerCtx) Move(x, y int) { @@ -18,8 +18,8 @@ func (manager *DesktopManagerCtx) GetCursorPosition() (int, int) { return xorg.GetCursorPosition() } -func (manager *DesktopManagerCtx) Scroll(x, y int) { - xorg.Scroll(x, y) +func (manager *DesktopManagerCtx) Scroll(deltaX, deltaY int, controlKey bool) { + xorg.Scroll(deltaX, deltaY, controlKey) } func (manager *DesktopManagerCtx) ButtonDown(code uint32) error { @@ -66,23 +66,44 @@ func (manager *DesktopManagerCtx) ResetKeys() { xorg.ResetKeys() } -func (manager *DesktopManagerCtx) ScreenConfigurations() map[int]types.ScreenConfiguration { - return xorg.ScreenConfigurations +func (manager *DesktopManagerCtx) ScreenConfigurations() []types.ScreenSize { + var configs []types.ScreenSize + for _, size := range xorg.ScreenConfigurations { + for _, fps := range size.Rates { + // filter out all irrelevant rates + if fps > 60 || (fps > 30 && fps%10 != 0) { + continue + } + + configs = append(configs, types.ScreenSize{ + Width: size.Width, + Height: size.Height, + Rate: fps, + }) + } + } + return configs } -func (manager *DesktopManagerCtx) SetScreenSize(size types.ScreenSize) error { +func (manager *DesktopManagerCtx) SetScreenSize(screenSize types.ScreenSize) (types.ScreenSize, error) { mu.Lock() - manager.GetScreenSizeChangeChannel() <- true + manager.emmiter.Emit("before_screen_size_change") defer func() { - manager.GetScreenSizeChangeChannel() <- false + manager.emmiter.Emit("after_screen_size_change") mu.Unlock() }() - return xorg.ChangeScreenSize(size.Width, size.Height, size.Rate) + screenSize, err := xorg.ChangeScreenSize(screenSize) + if err == nil { + // cache the new screen size + manager.screenSize = screenSize + } + + return screenSize, err } -func (manager *DesktopManagerCtx) GetScreenSize() *types.ScreenSize { +func (manager *DesktopManagerCtx) GetScreenSize() types.ScreenSize { return xorg.GetScreenSize() } @@ -119,24 +140,56 @@ func (manager *DesktopManagerCtx) GetKeyboardMap() (*types.KeyboardMap, error) { } func (manager *DesktopManagerCtx) SetKeyboardModifiers(mod types.KeyboardModifiers) { - if mod.NumLock != nil { - xorg.SetKeyboardModifier(xorg.KbdModNumLock, *mod.NumLock) + if mod.Shift != nil { + xorg.SetKeyboardModifier(xorg.KbdModShift, *mod.Shift) } if mod.CapsLock != nil { xorg.SetKeyboardModifier(xorg.KbdModCapsLock, *mod.CapsLock) } + + if mod.Control != nil { + xorg.SetKeyboardModifier(xorg.KbdModControl, *mod.Control) + } + + if mod.Alt != nil { + xorg.SetKeyboardModifier(xorg.KbdModAlt, *mod.Alt) + } + + if mod.NumLock != nil { + xorg.SetKeyboardModifier(xorg.KbdModNumLock, *mod.NumLock) + } + + if mod.Meta != nil { + xorg.SetKeyboardModifier(xorg.KbdModMeta, *mod.Meta) + } + + if mod.Super != nil { + xorg.SetKeyboardModifier(xorg.KbdModSuper, *mod.Super) + } + + if mod.AltGr != nil { + xorg.SetKeyboardModifier(xorg.KbdModAltGr, *mod.AltGr) + } } func (manager *DesktopManagerCtx) GetKeyboardModifiers() types.KeyboardModifiers { modifiers := xorg.GetKeyboardModifiers() - NumLock := (modifiers & xorg.KbdModNumLock) != 0 - CapsLock := (modifiers & xorg.KbdModCapsLock) != 0 + isset := func(mod xorg.KbdMod) *bool { + x := modifiers&mod != 0 + return &x + } return types.KeyboardModifiers{ - NumLock: &NumLock, - CapsLock: &CapsLock, + Shift: isset(xorg.KbdModShift), + CapsLock: isset(xorg.KbdModCapsLock), + Control: isset(xorg.KbdModControl), + Alt: isset(xorg.KbdModAlt), + NumLock: isset(xorg.KbdModNumLock), + Meta: isset(xorg.KbdModMeta), + Super: isset(xorg.KbdModSuper), + AltGr: isset(xorg.KbdModAltGr), } } diff --git a/server/internal/desktop/xorg/xorg.c b/server/internal/desktop/xorg/xorg.c deleted file mode 100644 index aa4e98f7..00000000 --- a/server/internal/desktop/xorg/xorg.c +++ /dev/null @@ -1,316 +0,0 @@ -#include "xorg.h" - -static Display *DISPLAY = NULL; - -Display *getXDisplay(void) { - return DISPLAY; -} - -int XDisplayOpen(char *name) { - DISPLAY = XOpenDisplay(name); - return DISPLAY == NULL; -} - -void XDisplayClose(void) { - XCloseDisplay(DISPLAY); -} - -void XMove(int x, int y) { - Display *display = getXDisplay(); - XWarpPointer(display, None, DefaultRootWindow(display), 0, 0, 0, 0, x, y); - XSync(display, 0); -} - -void XCursorPosition(int *x, int *y) { - Display *display = getXDisplay(); - Window root = DefaultRootWindow(display); - Window window; - int i; - unsigned mask; - XQueryPointer(display, root, &root, &window, x, y, &i, &i, &mask); -} - -void XScroll(int x, int y) { - int ydir = 4; /* Button 4 is up, 5 is down. */ - int xdir = 6; - - Display *display = getXDisplay(); - - if (y < 0) { - ydir = 5; - } - - if (x < 0) { - xdir = 7; - } - - int xi; - int yi; - - for (xi = 0; xi < abs(x); xi++) { - XTestFakeButtonEvent(display, xdir, 1, CurrentTime); - XTestFakeButtonEvent(display, xdir, 0, CurrentTime); - } - - for (yi = 0; yi < abs(y); yi++) { - XTestFakeButtonEvent(display, ydir, 1, CurrentTime); - XTestFakeButtonEvent(display, ydir, 0, CurrentTime); - } - - XSync(display, 0); -} - -void XButton(unsigned int button, int down) { - if (button == 0) - return; - - Display *display = getXDisplay(); - XTestFakeButtonEvent(display, button, down, CurrentTime); - XSync(display, 0); -} - -static xkeyentry_t *xKeysHead = NULL; - -// add keycode->keysym mapping to list -void XKeyEntryAdd(KeySym keysym, KeyCode keycode) { - xkeyentry_t *entry = (xkeyentry_t *) malloc(sizeof(xkeyentry_t)); - if (entry == NULL) - return; - - entry->keysym = keysym; - entry->keycode = keycode; - entry->next = xKeysHead; - xKeysHead = entry; -} - -// get keycode for keysym from list -KeyCode XKeyEntryGet(KeySym keysym) { - xkeyentry_t *prev = NULL; - xkeyentry_t *curr = xKeysHead; - - KeyCode keycode = 0; - while (curr != NULL) { - if (curr->keysym == keysym) { - keycode = curr->keycode; - - if (prev == NULL) { - xKeysHead = curr->next; - } else { - prev->next = curr->next; - } - - free(curr); - return keycode; - } - - prev = curr; - curr = curr->next; - } - - return 0; -} - -// From https://github.com/TigerVNC/tigervnc/blob/0946e298075f8f7b6d63e552297a787c5f84d27c/unix/x0vncserver/XDesktop.cxx#L343-L379 -KeyCode XkbKeysymToKeycode(Display* dpy, KeySym keysym) { - XkbDescPtr xkb; - XkbStateRec state; - unsigned int mods; - unsigned keycode; - - xkb = XkbGetMap(dpy, XkbAllComponentsMask, XkbUseCoreKbd); - if (!xkb) - return 0; - - XkbGetState(dpy, XkbUseCoreKbd, &state); - // XkbStateFieldFromRec() doesn't work properly because - // state.lookup_mods isn't properly updated, so we do this manually - mods = XkbBuildCoreState(XkbStateMods(&state), state.group); - - for (keycode = xkb->min_key_code; - keycode <= xkb->max_key_code; - keycode++) { - KeySym cursym; - unsigned int out_mods; - XkbTranslateKeyCode(xkb, keycode, mods, &out_mods, &cursym); - if (cursym == keysym) - break; - } - - if (keycode > xkb->max_key_code) - keycode = 0; - - XkbFreeKeyboard(xkb, XkbAllComponentsMask, True); - - // Shift+Tab is usually ISO_Left_Tab, but RFB hides this fact. Do - // another attempt if we failed the initial lookup - if ((keycode == 0) && (keysym == XK_Tab) && (mods & ShiftMask)) - return XkbKeysymToKeycode(dpy, XK_ISO_Left_Tab); - - return keycode; -} - -// From https://github.com/TigerVNC/tigervnc/blob/a434ef3377943e89165ac13c537cd0f28be97f84/unix/x0vncserver/XDesktop.cxx#L401-L453 -KeyCode XkbAddKeyKeysym(Display* dpy, KeySym keysym) { - int types[1]; - unsigned int key; - XkbDescPtr xkb; - XkbMapChangesRec changes; - KeySym *syms; - KeySym upper, lower; - - xkb = XkbGetMap(dpy, XkbAllComponentsMask, XkbUseCoreKbd); - - if (!xkb) - return 0; - - for (key = xkb->max_key_code; key >= xkb->min_key_code; key--) { - if (XkbKeyNumGroups(xkb, key) == 0) - break; - } - - // no free keycodes - if (key < xkb->min_key_code) - return 0; - - // assign empty structure - changes = *(XkbMapChangesRec *) malloc(sizeof(XkbMapChangesRec)); - for (int i = 0; i < sizeof(changes); i++) ((char *) &changes)[i] = 0; - - XConvertCase(keysym, &lower, &upper); - - if (upper == lower) - types[XkbGroup1Index] = XkbOneLevelIndex; - else - types[XkbGroup1Index] = XkbAlphabeticIndex; - - XkbChangeTypesOfKey(xkb, key, 1, XkbGroup1Mask, types, &changes); - - syms = XkbKeySymsPtr(xkb,key); - if (upper == lower) - syms[0] = keysym; - else { - syms[0] = lower; - syms[1] = upper; - } - - changes.changed |= XkbKeySymsMask; - changes.first_key_sym = key; - changes.num_key_syms = 1; - - if (XkbChangeMap(dpy, xkb, &changes)) { - return key; - } - - return 0; -} - -void XKey(KeySym keysym, int down) { - if (keysym == 0) - return; - - Display *display = getXDisplay(); - KeyCode keycode = 0; - - if (!down) - keycode = XKeyEntryGet(keysym); - - // Try to get keysyms from existing keycodes - if (keycode == 0) - keycode = XkbKeysymToKeycode(display, keysym); - - // Map non-existing keysyms to new keycodes - if (keycode == 0) - keycode = XkbAddKeyKeysym(display, keysym); - - if (down) - XKeyEntryAdd(keysym, keycode); - - XTestFakeKeyEvent(display, keycode, down, CurrentTime); - XSync(display, 0); -} - -void XGetScreenConfigurations() { - Display *display = getXDisplay(); - Window root = RootWindow(display, 0); - XRRScreenSize *xrrs; - int num_sizes; - - xrrs = XRRSizes(display, 0, &num_sizes); - for (int i = 0; i < num_sizes; i++) { - short *rates; - int num_rates; - - goCreateScreenSize(i, xrrs[i].width, xrrs[i].height, xrrs[i].mwidth, xrrs[i].mheight); - rates = XRRRates(display, 0, i, &num_rates); - for (int j = 0; j < num_rates; j++) { - goSetScreenRates(i, j, rates[j]); - } - } -} - -void XSetScreenConfiguration(int index, short rate) { - Display *display = getXDisplay(); - Window root = RootWindow(display, 0); - XRRSetScreenConfigAndRate(display, XRRGetScreenInfo(display, root), root, index, RR_Rotate_0, rate, CurrentTime); -} - -int XGetScreenSize() { - Display *display = getXDisplay(); - XRRScreenConfiguration *conf = XRRGetScreenInfo(display, RootWindow(display, 0)); - Rotation original_rotation; - return XRRConfigCurrentConfiguration(conf, &original_rotation); -} - -short XGetScreenRate() { - Display *display = getXDisplay(); - XRRScreenConfiguration *conf = XRRGetScreenInfo(display, RootWindow(display, 0)); - return XRRConfigCurrentRate(conf); -} - -void XSetKeyboardModifier(int mod, int on) { - Display *display = getXDisplay(); - XkbLockModifiers(display, XkbUseCoreKbd, mod, on ? mod : 0); - XFlush(display); -} - -char XGetKeyboardModifiers() { - XkbStateRec xkbState; - Display *display = getXDisplay(); - XkbGetState(display, XkbUseCoreKbd, &xkbState); - return xkbState.locked_mods; -} - -XFixesCursorImage *XGetCursorImage(void) { - Display *display = getXDisplay(); - return XFixesGetCursorImage(display); -} - -char *XGetScreenshot(int *w, int *h) { - Display *display = getXDisplay(); - Window root = DefaultRootWindow(display); - - XWindowAttributes attr; - XGetWindowAttributes(display, root, &attr); - int width = attr.width; - int height = attr.height; - - XImage *ximage = XGetImage(display, root, 0, 0, width, height, AllPlanes, ZPixmap); - - *w = width; - *h = height; - char *pixels = (char *)malloc(width * height * 3); - - for (int row = 0; row < height; row++) { - for (int col = 0; col < width; col++) { - int pos = ((row * width) + col) * 3; - unsigned long pixel = XGetPixel(ximage, col, row); - - pixels[pos] = (pixel & ximage->red_mask) >> 16; - pixels[pos+1] = (pixel & ximage->green_mask) >> 8; - pixels[pos+2] = pixel & ximage->blue_mask; - } - } - - XDestroyImage(ximage); - return pixels; -} diff --git a/server/internal/desktop/xorg/xorg.go b/server/internal/desktop/xorg/xorg.go deleted file mode 100644 index 0c9ed918..00000000 --- a/server/internal/desktop/xorg/xorg.go +++ /dev/null @@ -1,320 +0,0 @@ -package xorg - -/* -#cgo LDFLAGS: -lX11 -lXrandr -lXtst -lXfixes - -#include "xorg.h" -*/ -import "C" - -import ( - "fmt" - "image" - "image/color" - "sync" - "time" - "unsafe" - - "m1k1o/neko/internal/types" -) - -//go:generate ./keysymdef.sh - -type KbdMod uint8 - -const ( - KbdModCapsLock KbdMod = 2 - KbdModNumLock KbdMod = 16 -) - -var ScreenConfigurations = make(map[int]types.ScreenConfiguration) - -var debounce_button = make(map[uint32]time.Time) -var debounce_key = make(map[uint32]time.Time) -var mu = sync.Mutex{} - -func GetScreenConfigurations() { - mu.Lock() - defer mu.Unlock() - - C.XGetScreenConfigurations() -} - -func DisplayOpen(display string) bool { - mu.Lock() - defer mu.Unlock() - - displayUnsafe := C.CString(display) - defer C.free(unsafe.Pointer(displayUnsafe)) - - ok := C.XDisplayOpen(displayUnsafe) - return int(ok) == 1 -} - -func DisplayClose() { - mu.Lock() - defer mu.Unlock() - - C.XDisplayClose() -} - -func Move(x, y int) { - mu.Lock() - defer mu.Unlock() - - C.XMove(C.int(x), C.int(y)) -} - -func GetCursorPosition() (int, int) { - mu.Lock() - defer mu.Unlock() - - var x C.int - var y C.int - C.XCursorPosition(&x, &y) - - return int(x), int(y) -} - -func Scroll(x, y int) { - mu.Lock() - defer mu.Unlock() - - C.XScroll(C.int(x), C.int(y)) -} - -func ButtonDown(code uint32) error { - mu.Lock() - defer mu.Unlock() - - if _, ok := debounce_button[code]; ok { - return fmt.Errorf("debounced button %v", code) - } - - debounce_button[code] = time.Now() - - C.XButton(C.uint(code), C.int(1)) - return nil -} - -func KeyDown(code uint32) error { - mu.Lock() - defer mu.Unlock() - - if _, ok := debounce_key[code]; ok { - return fmt.Errorf("debounced key %v", code) - } - - debounce_key[code] = time.Now() - - C.XKey(C.KeySym(code), C.int(1)) - return nil -} - -func ButtonUp(code uint32) error { - mu.Lock() - defer mu.Unlock() - - if _, ok := debounce_button[code]; !ok { - return fmt.Errorf("debounced button %v", code) - } - - delete(debounce_button, code) - - C.XButton(C.uint(code), C.int(0)) - return nil -} - -func KeyUp(code uint32) error { - mu.Lock() - defer mu.Unlock() - - if _, ok := debounce_key[code]; !ok { - return fmt.Errorf("debounced key %v", code) - } - - delete(debounce_key, code) - - C.XKey(C.KeySym(code), C.int(0)) - return nil -} - -func ResetKeys() { - mu.Lock() - defer mu.Unlock() - - for code := range debounce_button { - C.XButton(C.uint(code), C.int(0)) - delete(debounce_button, code) - } - - for code := range debounce_key { - C.XKey(C.KeySym(code), C.int(0)) - delete(debounce_key, code) - } -} - -func CheckKeys(duration time.Duration) { - mu.Lock() - defer mu.Unlock() - - t := time.Now() - for code, start := range debounce_button { - if t.Sub(start) < duration { - continue - } - - C.XButton(C.uint(code), C.int(0)) - delete(debounce_button, code) - } - - for code, start := range debounce_key { - if t.Sub(start) < duration { - continue - } - - C.XKey(C.KeySym(code), C.int(0)) - delete(debounce_key, code) - } -} - -func ChangeScreenSize(width int, height int, rate int16) error { - mu.Lock() - defer mu.Unlock() - - for index, size := range ScreenConfigurations { - if size.Width == width && size.Height == height { - for _, fps := range size.Rates { - if rate == fps { - C.XSetScreenConfiguration(C.int(index), C.short(fps)) - return nil - } - } - } - } - - return fmt.Errorf("unknown screen configuration %dx%d@%d", width, height, rate) -} - -func GetScreenSize() *types.ScreenSize { - mu.Lock() - defer mu.Unlock() - - index := int(C.XGetScreenSize()) - rate := int16(C.XGetScreenRate()) - - if conf, ok := ScreenConfigurations[index]; ok { - return &types.ScreenSize{ - Width: conf.Width, - Height: conf.Height, - Rate: rate, - } - } - - return nil -} - -func SetKeyboardModifier(mod KbdMod, active bool) { - mu.Lock() - defer mu.Unlock() - - num := C.int(0) - if active { - num = C.int(1) - } - - C.XSetKeyboardModifier(C.int(mod), num) -} - -func GetKeyboardModifiers() KbdMod { - mu.Lock() - defer mu.Unlock() - - return KbdMod(C.XGetKeyboardModifiers()) -} - -func GetCursorImage() *types.CursorImage { - mu.Lock() - defer mu.Unlock() - - cur := C.XGetCursorImage() - defer C.XFree(unsafe.Pointer(cur)) - - width := int(cur.width) - height := int(cur.height) - - // Xlib stores 32-bit data in longs, even if longs are 64-bits long. - pixels := C.GoBytes(unsafe.Pointer(cur.pixels), C.int(width*height*8)) - - img := image.NewRGBA(image.Rect(0, 0, width, height)) - for y := 0; y < height; y++ { - for x := 0; x < width; x++ { - pos := ((y * width) + x) * 8 - - img.SetRGBA(x, y, color.RGBA{ - A: pixels[pos+3], - R: pixels[pos+2], - G: pixels[pos+1], - B: pixels[pos+0], - }) - } - } - - return &types.CursorImage{ - Width: uint16(width), - Height: uint16(height), - Xhot: uint16(cur.xhot), - Yhot: uint16(cur.yhot), - Serial: uint64(cur.cursor_serial), - Image: img, - } -} - -func GetScreenshotImage() *image.RGBA { - mu.Lock() - defer mu.Unlock() - - var w, h C.int - pixelsUnsafe := C.XGetScreenshot(&w, &h) - pixels := C.GoBytes(unsafe.Pointer(pixelsUnsafe), w*h*3) - defer C.free(unsafe.Pointer(pixelsUnsafe)) - - width := int(w) - height := int(h) - img := image.NewRGBA(image.Rect(0, 0, width, height)) - for row := 0; row < height; row++ { - for col := 0; col < width; col++ { - pos := ((row * width) + col) * 3 - - img.SetRGBA(col, row, color.RGBA{ - R: uint8(pixels[pos]), - G: uint8(pixels[pos+1]), - B: uint8(pixels[pos+2]), - A: 0xFF, - }) - } - } - - return img -} - -//export goCreateScreenSize -func goCreateScreenSize(index C.int, width C.int, height C.int, mwidth C.int, mheight C.int) { - ScreenConfigurations[int(index)] = types.ScreenConfiguration{ - Width: int(width), - Height: int(height), - Rates: make(map[int]int16), - } -} - -//export goSetScreenRates -func goSetScreenRates(index C.int, rate_index C.int, rateC C.short) { - rate := int16(rateC) - - // filter out all irrelevant rates - if rate > 60 || (rate > 30 && rate%10 != 0) { - return - } - - ScreenConfigurations[int(index)].Rates[int(rate_index)] = rate -} diff --git a/server/internal/desktop/xorg/xorg.h b/server/internal/desktop/xorg/xorg.h deleted file mode 100644 index 59a2b2c1..00000000 --- a/server/internal/desktop/xorg/xorg.h +++ /dev/null @@ -1,48 +0,0 @@ -#pragma once - -#include -#include -#include -#include -#include -#include -#include - -extern void goCreateScreenSize(int index, int width, int height, int mwidth, int mheight); -extern void goSetScreenRates(int index, int rate_index, short rate); - -Display *getXDisplay(void); -int XDisplayOpen(char *input); -void XDisplayClose(void); - -void XMove(int x, int y); -void XCursorPosition(int *x, int *y); -void XScroll(int x, int y); -void XButton(unsigned int button, int down); - -typedef struct xkeyentry_t { - KeySym keysym; - KeyCode keycode; - struct xkeyentry_t *next; -} xkeyentry_t; - -typedef struct xkeycode_t { - KeyCode keycode; - struct xkeycode_t *next; -} xkeycode_t; - -static void XKeyEntryAdd(KeySym keysym, KeyCode keycode); -static KeyCode XKeyEntryGet(KeySym keysym); -static KeyCode XkbKeysymToKeycode(Display *dpy, KeySym keysym); -void XKey(KeySym keysym, int down); - -void XGetScreenConfigurations(); -void XSetScreenConfiguration(int index, short rate); -int XGetScreenSize(); -short XGetScreenRate(); - -void XSetKeyboardModifier(int mod, int on); -char XGetKeyboardModifiers(); -XFixesCursorImage *XGetCursorImage(void); - -char *XGetScreenshot(int *w, int *h); diff --git a/internal/http/batch.go b/server/internal/http/batch.go similarity index 100% rename from internal/http/batch.go rename to server/internal/http/batch.go diff --git a/internal/http/debug.go b/server/internal/http/debug.go similarity index 100% rename from internal/http/debug.go rename to server/internal/http/debug.go diff --git a/server/internal/http/http.go b/server/internal/http/http.go deleted file mode 100644 index ea50350c..00000000 --- a/server/internal/http/http.go +++ /dev/null @@ -1,250 +0,0 @@ -package http - -import ( - "context" - "encoding/json" - "fmt" - "image/jpeg" - "io" - "net/http" - "os" - "regexp" - "strconv" - - "github.com/go-chi/chi/v5" - "github.com/go-chi/chi/v5/middleware" - "github.com/go-chi/cors" - "github.com/rs/zerolog" - "github.com/rs/zerolog/log" - - "m1k1o/neko/internal/config" - "m1k1o/neko/internal/types" -) - -const FILE_UPLOAD_BUF_SIZE = 65000 - -type Server struct { - logger zerolog.Logger - router *chi.Mux - http *http.Server - conf *config.Server -} - -func New(conf *config.Server, webSocketHandler types.WebSocketHandler, desktop types.DesktopManager) *Server { - logger := log.With().Str("module", "http").Logger() - - router := chi.NewRouter() - router.Use(middleware.RequestID) // Create a request ID for each request - if conf.Proxy { - router.Use(middleware.RealIP) - } - router.Use(middleware.RequestLogger(&logformatter{logger})) - router.Use(middleware.Recoverer) // Recover from panics without crashing server - router.Use(middleware.Compress(5, "application/octet-stream")) - - router.Use(cors.Handler(cors.Options{ - AllowOriginFunc: conf.AllowOrigin, - AllowedMethods: []string{"GET", "POST", "DELETE", "OPTIONS"}, - AllowedHeaders: []string{"Accept", "Authorization", "Content-Type", "X-CSRF-Token"}, - ExposedHeaders: []string{"Link"}, - AllowCredentials: true, - MaxAge: 300, // Maximum value not ignored by any of major browsers - })) - - if conf.PathPrefix != "/" { - router.Use(func(h http.Handler) http.Handler { - return http.StripPrefix(conf.PathPrefix, h) - }) - } - - router.Get("/ws", func(w http.ResponseWriter, r *http.Request) { - err := webSocketHandler.Upgrade(w, r) - if err != nil { - logger.Warn().Err(err).Msg("failed to upgrade websocket conection") - } - }) - - router.Get("/stats", func(w http.ResponseWriter, r *http.Request) { - password := r.URL.Query().Get("pwd") - isAdmin, err := webSocketHandler.IsAdmin(password) - if err != nil { - http.Error(w, err.Error(), http.StatusForbidden) - return - } - - if !isAdmin { - http.Error(w, "bad authorization", http.StatusUnauthorized) - return - } - - w.Header().Set("Content-Type", "application/json") - - stats := webSocketHandler.Stats() - if err := json.NewEncoder(w).Encode(stats); err != nil { - logger.Warn().Err(err).Msg("failed writing json error response") - } - }) - - router.Get("/screenshot.jpg", func(w http.ResponseWriter, r *http.Request) { - password := r.URL.Query().Get("pwd") - isAdmin, err := webSocketHandler.IsAdmin(password) - if err != nil { - http.Error(w, err.Error(), http.StatusForbidden) - return - } - - if !isAdmin { - http.Error(w, "bad authorization", http.StatusUnauthorized) - return - } - - if webSocketHandler.IsLocked("login") { - http.Error(w, "room is locked", http.StatusLocked) - return - } - - quality, err := strconv.Atoi(r.URL.Query().Get("quality")) - if err != nil { - quality = 90 - } - - w.Header().Set("Cache-Control", "no-cache, no-store, must-revalidate") - w.Header().Set("Content-Type", "image/jpeg") - - img := desktop.GetScreenshotImage() - if err := jpeg.Encode(w, img, &jpeg.Options{Quality: quality}); err != nil { - http.Error(w, err.Error(), http.StatusInternalServerError) - return - } - }) - - // allow downloading and uploading files - if webSocketHandler.FileTransferEnabled() { - router.Get("/file", func(w http.ResponseWriter, r *http.Request) { - password := r.URL.Query().Get("pwd") - isAuthorized, err := webSocketHandler.CanTransferFiles(password) - if err != nil { - http.Error(w, err.Error(), http.StatusForbidden) - return - } - - if !isAuthorized { - http.Error(w, "bad authorization", http.StatusUnauthorized) - return - } - - filename := r.URL.Query().Get("filename") - badChars, _ := regexp.MatchString(`(?m)\.\.(?:\/|$)`, filename) - if filename == "" || badChars { - http.Error(w, "bad filename", http.StatusBadRequest) - return - } - - filePath := webSocketHandler.FileTransferPath(filename) - f, err := os.Open(filePath) - if err != nil { - http.Error(w, "not found or unable to open", http.StatusNotFound) - return - } - defer f.Close() - - w.Header().Set("Content-Type", "application/octet-stream") - w.Header().Set("Content-Disposition", fmt.Sprintf("attachment; filename=%q", filename)) - io.Copy(w, f) - }) - - router.Post("/file", func(w http.ResponseWriter, r *http.Request) { - password := r.URL.Query().Get("pwd") - isAuthorized, err := webSocketHandler.CanTransferFiles(password) - if err != nil { - http.Error(w, err.Error(), http.StatusForbidden) - return - } - - if !isAuthorized { - http.Error(w, "bad authorization", http.StatusUnauthorized) - return - } - - err = r.ParseMultipartForm(32 << 20) - if err != nil || r.MultipartForm == nil { - logger.Warn().Err(err).Msg("failed to parse multipart form") - http.Error(w, "error parsing form", http.StatusBadRequest) - return - } - - for _, formheader := range r.MultipartForm.File["files"] { - filePath := webSocketHandler.FileTransferPath(formheader.Filename) - - formfile, err := formheader.Open() - if err != nil { - logger.Warn().Err(err).Msg("failed to open formdata file") - http.Error(w, "error writing file", http.StatusInternalServerError) - return - } - defer formfile.Close() - - f, err := os.OpenFile(filePath, os.O_WRONLY|os.O_CREATE, 0644) - if err != nil { - http.Error(w, "unable to open file for writing", http.StatusInternalServerError) - return - } - defer f.Close() - - io.Copy(f, formfile) - } - - err = r.MultipartForm.RemoveAll() - if err != nil { - logger.Warn().Err(err).Msg("failed to remove multipart form") - } - }) - } - - router.Get("/health", func(w http.ResponseWriter, r *http.Request) { - _, _ = w.Write([]byte("true")) - }) - - fs := http.FileServer(http.Dir(conf.Static)) - router.Get("/*", func(w http.ResponseWriter, r *http.Request) { - if _, err := os.Stat(conf.Static + r.URL.Path); !os.IsNotExist(err) { - fs.ServeHTTP(w, r) - } else { - http.NotFound(w, r) - } - }) - - server := &http.Server{ - Addr: conf.Bind, - Handler: router, - } - - return &Server{ - logger: logger, - router: router, - http: server, - conf: conf, - } -} - -func (s *Server) Start() { - if s.conf.Cert != "" && s.conf.Key != "" { - go func() { - if err := s.http.ListenAndServeTLS(s.conf.Cert, s.conf.Key); err != http.ErrServerClosed { - s.logger.Panic().Err(err).Msg("unable to start https server") - } - }() - s.logger.Info().Msgf("https listening on %s", s.http.Addr) - } else { - go func() { - if err := s.http.ListenAndServe(); err != http.ErrServerClosed { - s.logger.Panic().Err(err).Msg("unable to start http server") - } - }() - s.logger.Warn().Msgf("http listening on %s", s.http.Addr) - } -} - -func (s *Server) Shutdown() error { - return s.http.Shutdown(context.Background()) -} diff --git a/server/internal/types/event/events.go b/server/internal/http/legacy/event/events.go similarity index 100% rename from server/internal/types/event/events.go rename to server/internal/http/legacy/event/events.go diff --git a/server/internal/http/legacy/handler.go b/server/internal/http/legacy/handler.go new file mode 100644 index 00000000..5fdf08b6 --- /dev/null +++ b/server/internal/http/legacy/handler.go @@ -0,0 +1,241 @@ +package legacy + +import ( + "errors" + "fmt" + "net/http" + + oldEvent "github.com/demodesk/neko/internal/http/legacy/event" + oldMessage "github.com/demodesk/neko/internal/http/legacy/message" + + "github.com/demodesk/neko/pkg/types" + "github.com/demodesk/neko/pkg/types/event" + "github.com/demodesk/neko/pkg/types/message" + "github.com/demodesk/neko/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 + }, + } + + // DefaultDialer is a dialer with all fields set to the default zero values. + DefaultDialer = websocket.DefaultDialer +) + +type LegacyHandler struct { + logger zerolog.Logger + serverAddr string +} + +func New() *LegacyHandler { + // Init + + return &LegacyHandler{ + logger: log.With().Str("module", "legacy").Logger(), + serverAddr: "127.0.0.1:8080", + } +} + +func (h *LegacyHandler) Route(r types.Router) { + r.Get("/ws", func(w http.ResponseWriter, r *http.Request) error { + s := newSession(h.logger, h.serverAddr) + + // 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 + + // create a new session + username := r.URL.Query().Get("username") + password := r.URL.Query().Get("password") + token, 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 + connBackend, _, err := DefaultDialer.Dial("ws://"+h.serverAddr+"/api/ws?token="+token, nil) + 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 + if err = s.toBackend(event.SIGNAL_REQUEST, message.SignalRequest{}); 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 <- err + dst.WriteMessage(websocket.CloseMessage, m) + break + } + 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: err.Error(), + }) + continue + } else if errors.Is(err, ErrWebsocketSend) { + errc <- err + break + } else { + h.logger.Error().Err(err).Msg("couldn't rewrite text message") + } + } + } + } + + // 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: %v" + case err = <-errBackend: + message = "websocketproxy: Error when copying from client to backend: %v" + } + + 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 { + password := r.URL.Query().Get("pwd") + isAdmin, err := webSocketHandler.IsAdmin(password) + if err != nil { + return utils.HttpForbidden(err) + } + + if !isAdmin { + return utils.HttpUnauthorized().Msg("bad authorization") + } + + w.Header().Set("Content-Type", "application/json") + + stats := webSocketHandler.Stats() + return json.NewEncoder(w).Encode(stats) + }) + + r.Get("/screenshot.jpg", func(w http.ResponseWriter, r *http.Request) error { + password := r.URL.Query().Get("pwd") + isAdmin, err := webSocketHandler.IsAdmin(password) + if err != nil { + return utils.HttpForbidden(err) + } + + if !isAdmin { + return utils.HttpUnauthorized().Msg("bad authorization") + } + + if webSocketHandler.IsLocked("login") { + return utils.HttpError(http.StatusLocked).Msg("room is locked") + } + + quality, err := strconv.Atoi(r.URL.Query().Get("quality")) + if err != nil { + quality = 90 + } + + w.Header().Set("Cache-Control", "no-cache, no-store, must-revalidate") + w.Header().Set("Content-Type", "image/jpeg") + + img := desktop.GetScreenshotImage() + if err := jpeg.Encode(w, img, &jpeg.Options{Quality: quality}); err != nil { + return utils.HttpInternalServerError().WithInternalErr(err) + } + + return nil + }) + + // allow downloading and uploading files + if webSocketHandler.FileTransferEnabled() { + r.Get("/file", func(w http.ResponseWriter, r *http.Request) error { + return nil + }) + + r.Post("/file", func(w http.ResponseWriter, r *http.Request) error { + return nil + }) + } + */ + + r.Get("/health", func(w http.ResponseWriter, r *http.Request) error { + _, err := w.Write([]byte("true")) + return err + }) +} diff --git a/server/internal/types/message/messages.go b/server/internal/http/legacy/message/messages.go similarity index 98% rename from server/internal/types/message/messages.go rename to server/internal/http/legacy/message/messages.go index 89552b71..7db65f45 100644 --- a/server/internal/types/message/messages.go +++ b/server/internal/http/legacy/message/messages.go @@ -1,7 +1,7 @@ package message import ( - "m1k1o/neko/internal/types" + "github.com/demodesk/neko/internal/http/legacy/types" "github.com/pion/webrtc/v3" ) diff --git a/server/internal/http/legacy/session.go b/server/internal/http/legacy/session.go new file mode 100644 index 00000000..fd361b88 --- /dev/null +++ b/server/internal/http/legacy/session.go @@ -0,0 +1,175 @@ +package legacy + +import ( + "bytes" + "encoding/json" + "fmt" + "io" + "net/http" + "strings" + + oldTypes "github.com/demodesk/neko/internal/http/legacy/types" + + "github.com/demodesk/neko/internal/api" + "github.com/demodesk/neko/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 { + logger zerolog.Logger + serverAddr string + + id string + token string + name string + client *http.Client + + lastHostID string + lockedControls bool + lockedLogins bool + lockedFileTransfer bool + sessions map[string]*memberStruct + + connClient *websocket.Conn + connBackend *websocket.Conn +} + +func newSession(logger zerolog.Logger, serverAddr string) *session { + return &session{ + logger: logger, + serverAddr: serverAddr, + client: http.DefaultClient, + sessions: make(map[string]*memberStruct), + } +} + +func (s *session) apiReq(method, path string, request, response any) error { + body, err := json.Marshal(request) + if err != nil { + return err + } + + req, err := http.NewRequest(method, "http://"+s.serverAddr+path, bytes.NewReader(body)) + if err != nil { + return err + } + + req.Header.Set("Content-Type", "application/json") + + if s.token != "" { + req.Header.Set("Authorization", "Bearer "+s.token) + } + + res, err := s.client.Do(req) + if err != nil { + return err + } + defer res.Body.Close() + + if res.StatusCode < 200 || res.StatusCode >= 300 { + 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 fmt.Errorf("%w: %s", ErrBackendRespone, apiErr.Message) + } + // return raw body if failed to unmarshal + return fmt.Errorf("unexpected status code: %d, body: %s", res.StatusCode, strings.TrimSpace(string(body))) + } + + if res.Body == nil { + return nil + } + + if response == nil { + io.Copy(io.Discard, res.Body) + return nil + } + + return json.NewDecoder(res.Body).Decode(response) +} + +// send message to client (in old format) +func (s *session) toClient(payload any) error { + msg, err := json.Marshal(payload) + if err != nil { + return err + } + + err = s.connClient.WriteMessage(websocket.TextMessage, msg) + 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 { + rawPayload, err := json.Marshal(payload) + if err != nil { + return err + } + + msg, err := json.Marshal(&types.WebSocketMessage{ + Event: event, + Payload: rawPayload, + }) + if err != nil { + return err + } + + err = s.connBackend.WriteMessage(websocket.TextMessage, msg) + if err != nil { + return fmt.Errorf("%w: %s", ErrWebsocketSend, err) + } + + return nil +} + +func (s *session) create(username, password string) (string, error) { + data := api.SessionDataPayload{} + + err := s.apiReq(http.MethodPost, "/api/login", api.SessionLoginPayload{ + Username: username, + Password: password, + }, &data) + if err != nil { + return "", err + } + + s.id = data.ID + s.token = data.Token + s.name = data.Profile.Name + + // if Cookie auth, the token will be empty + if s.token == "" { + return "", fmt.Errorf("token not found - make sure you are not using Cookie auth on the server") + } + + return data.Token, nil +} + +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") + } +} diff --git a/server/internal/http/legacy/types/types.go b/server/internal/http/legacy/types/types.go new file mode 100644 index 00000000..eb05cbac --- /dev/null +++ b/server/internal/http/legacy/types/types.go @@ -0,0 +1,20 @@ +package types + +type Member struct { + ID string `json:"id"` + Name string `json:"displayname"` + 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 new file mode 100644 index 00000000..db0528f7 --- /dev/null +++ b/server/internal/http/legacy/wstobackend.go @@ -0,0 +1,344 @@ +package legacy + +import ( + "encoding/json" + "fmt" + "net/http" + + "github.com/gorilla/websocket" + "github.com/pion/webrtc/v3" + + oldEvent "github.com/demodesk/neko/internal/http/legacy/event" + oldMessage "github.com/demodesk/neko/internal/http/legacy/message" + + "github.com/demodesk/neko/internal/api/room" + "github.com/demodesk/neko/internal/plugins/chat" + "github.com/demodesk/neko/internal/plugins/filetransfer" + "github.com/demodesk/neko/pkg/types" + "github.com/demodesk/neko/pkg/types/event" + "github.com/demodesk/neko/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 { + // 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 != "" { + s.name = request.DisplayName + + err = s.apiReq(http.MethodPost, "/api/profile", map[string]any{ + "name": request.DisplayName, + }, nil) + if 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 + msg, err := json.Marshal(&oldMessage.EmoteSend{ + Event: oldEvent.CHAT_EMOTE, + ID: s.id, + Emote: request.Emote, + }) + if err != nil { + return err + } + + // loopback emote + err = s.connClient.WriteMessage(websocket.TextMessage, msg) + 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) + + // Screen Events + case oldEvent.SCREEN_RESOLUTION: + // No WS equivalent, call HTTP API and return screen resolution. + return fmt.Errorf("event not implemented: %s", header.Event) + + case oldEvent.SCREEN_CONFIGURATIONS: + // No WS equivalent, call HTTP API and return screen configurations. + return fmt.Errorf("event not implemented: %s", header.Event) + + 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 + } + + // TODO: No WS equivalent, call HTTP API. + return fmt.Errorf("event not implemented: %s", header.Event) + + case oldEvent.ADMIN_KICK: + request := &oldMessage.Admin{} + err := json.Unmarshal(msg, request) + if err != nil { + return err + } + + // TODO: we need to send a message to the user before kicking them + // that they are being kicked so they will not automatically rejoin + 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 new file mode 100644 index 00000000..b5a59ee8 --- /dev/null +++ b/server/internal/http/legacy/wstoclient.go @@ -0,0 +1,719 @@ +package legacy + +import ( + "encoding/json" + "errors" + "fmt" + + "github.com/pion/webrtc/v3" + + oldEvent "github.com/demodesk/neko/internal/http/legacy/event" + oldMessage "github.com/demodesk/neko/internal/http/legacy/message" + oldTypes "github.com/demodesk/neko/internal/http/legacy/types" + + "github.com/demodesk/neko/internal/plugins/chat" + "github.com/demodesk/neko/internal/plugins/filetransfer" + "github.com/demodesk/neko/pkg/types" + "github.com/demodesk/neko/pkg/types/event" + "github.com/demodesk/neko/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, + Admin: profile.IsAdmin, + Muted: !settings.CanSend, + }, 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 { + return s.toClient(&oldMessage.ControlTarget{ + Event: oldEvent.CONTROL_GIVE, + ID: request.HostID, + Target: request.ID, + }) + } + } + + 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 + } + + 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 + } + + // + // 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) + } + + // + // Locks + // + locks := map[string]string{} + if request.Settings.LockedLogins { + locks["login"] = "" // TODO: We don't know who locked the login. + s.lockedLogins = true + } + if request.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 s.toClient(&oldMessage.SystemInit{ + Event: oldEvent.SYSTEM_INIT, + ImplicitHosting: request.Settings.ImplicitHosting, + Locks: locks, + FileTransfer: true, // TODO: We don't know if file transfer is enabled, we would need to check the global config somehow. + }) + + case event.SYSTEM_ADMIN: + request := &message.SystemAdmin{} + err := json.Unmarshal(data.Payload, request) + if err != nil { + return err + } + + // + // ScreenSizesList + // + + rates := map[string][]int16{} + for _, size := range request.ScreenSizesList { + key := fmt.Sprintf("%dx%d", size.Width, size.Height) + rates[key] = append(rates[key], size.Rate) + } + + usedScreenSizes := map[string]struct{}{} + screenSizesList := map[int]oldTypes.ScreenConfiguration{} + for i, size := range request.ScreenSizesList { + key := fmt.Sprintf("%dx%d", size.Width, size.Height) + if _, ok := usedScreenSizes[key]; ok { + continue + } + + ratesMap := map[int]int16{} + for i, rate := range rates[key] { + ratesMap[i] = rate + } + + screenSizesList[i] = oldTypes.ScreenConfiguration{ + Width: size.Width, + Height: size.Height, + Rates: ratesMap, + } + } + + err = s.toClient(&oldMessage.ScreenConfigurations{ + Event: oldEvent.SCREEN_CONFIGURATIONS, + Configurations: 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_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_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, + Files: files, + }) + + // 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/logger.go b/server/internal/http/logger.go index e5b1d643..258eec54 100644 --- a/server/internal/http/logger.go +++ b/server/internal/http/logger.go @@ -5,16 +5,24 @@ import ( "net/http" "time" - "github.com/go-chi/chi/v5/middleware" + "github.com/go-chi/chi/middleware" "github.com/rs/zerolog" + + "github.com/demodesk/neko/pkg/types" + "github.com/demodesk/neko/pkg/utils" ) -type logformatter struct { +type logFormatter struct { logger zerolog.Logger } -func (l *logformatter) NewLogEntry(r *http.Request) middleware.LogEntry { - req := map[string]interface{}{} +func (l *logFormatter) NewLogEntry(r *http.Request) middleware.LogEntry { + // exclude health & metrics from logs + if r.RequestURI == "/health" || r.RequestURI == "/metrics" { + return &nulllog{} + } + + req := map[string]any{} if reqID := middleware.GetReqID(r.Context()); reqID != "" { req["id"] = reqID @@ -32,43 +40,96 @@ func (l *logformatter) NewLogEntry(r *http.Request) middleware.LogEntry { req["agent"] = r.UserAgent() req["uri"] = fmt.Sprintf("%s://%s%s", scheme, r.Host, r.RequestURI) - fields := map[string]interface{}{} - fields["req"] = req - - return &logentry{ - fields: fields, - logger: l.logger, + return &logEntry{ + logger: l.logger.With().Interface("req", req).Logger(), } } -type logentry struct { - logger zerolog.Logger - fields map[string]interface{} - errors []map[string]interface{} +type logEntry struct { + logger zerolog.Logger + err error + panic *logPanic + session types.Session } -func (e *logentry) Write(status, bytes int, header http.Header, elapsed time.Duration, extra interface{}) { - res := map[string]interface{}{} +type logPanic struct { + message string + stack string +} + +func (e *logEntry) Panic(v any, stack []byte) { + e.panic = &logPanic{ + message: fmt.Sprintf("%+v", v), + stack: string(stack), + } +} + +func (e *logEntry) Error(err error) { + e.err = err +} + +func (e *logEntry) SetSession(session types.Session) { + e.session = session +} + +func (e *logEntry) Write(status, bytes int, header http.Header, elapsed time.Duration, extra any) { + res := map[string]any{} res["time"] = time.Now().UTC().Format(time.RFC1123) res["status"] = status res["bytes"] = bytes res["elapsed"] = float64(elapsed.Nanoseconds()) / 1000000.0 - e.fields["res"] = res - e.fields["module"] = "http" + logger := e.logger.With().Interface("res", res).Logger() - if len(e.errors) > 0 { - e.fields["errors"] = e.errors - e.logger.Error().Fields(e.fields).Msgf("request failed (%d)", status) - } else { - e.logger.Debug().Fields(e.fields).Msgf("request complete (%d)", status) + // add session ID to logs (if exists) + if e.session != nil { + logger = logger.With().Str("session_id", e.session.ID()).Logger() } + + // handle panic error message + if e.panic != nil { + logger.WithLevel(zerolog.PanicLevel). + Err(e.err). + Str("stack", e.panic.stack). + Msgf("request failed (%d): %s", status, e.panic.message) + return + } + + // handle panic error message + if e.err != nil { + httpErr, ok := e.err.(*utils.HTTPError) + if !ok { + logger.Err(e.err).Msgf("request failed (%d)", status) + return + } + + if httpErr.Message == "" { + httpErr.Message = http.StatusText(httpErr.Code) + } + + var logLevel zerolog.Level + if httpErr.Code < 500 { + logLevel = zerolog.WarnLevel + } else { + logLevel = zerolog.ErrorLevel + } + + message := httpErr.Message + if httpErr.InternalMsg != "" { + message = httpErr.InternalMsg + } + + logger.WithLevel(logLevel).Err(httpErr.InternalErr).Msgf("request failed (%d): %s", status, message) + return + } + + logger.Debug().Msgf("request complete (%d)", status) } -func (e *logentry) Panic(v interface{}, stack []byte) { - err := map[string]interface{}{} - err["message"] = fmt.Sprintf("%+v", v) - err["stack"] = string(stack) +type nulllog struct{} - e.errors = append(e.errors, err) +func (e *nulllog) Panic(v any, stack []byte) {} +func (e *nulllog) Error(err error) {} +func (e *nulllog) SetSession(session types.Session) {} +func (e *nulllog) Write(status, bytes int, header http.Header, elapsed time.Duration, extra any) { } diff --git a/internal/http/manager.go b/server/internal/http/manager.go similarity index 96% rename from internal/http/manager.go rename to server/internal/http/manager.go index 4a753314..fdb630b7 100644 --- a/internal/http/manager.go +++ b/server/internal/http/manager.go @@ -10,6 +10,7 @@ import ( "github.com/rs/zerolog/log" "github.com/demodesk/neko/internal/config" + "github.com/demodesk/neko/internal/http/legacy" "github.com/demodesk/neko/pkg/types" ) @@ -54,6 +55,9 @@ func New(WebSocketManager types.WebSocketManager, ApiManager types.ApiManager, c return config.AllowOrigin(r.Header.Get("Origin")) })) + // Legacy handler + legacy.New().Route(router) + batch := batchHandler{ Router: router, PathPrefix: "/api", diff --git a/internal/http/router.go b/server/internal/http/router.go similarity index 100% rename from internal/http/router.go rename to server/internal/http/router.go diff --git a/internal/member/file/provider.go b/server/internal/member/file/provider.go similarity index 100% rename from internal/member/file/provider.go rename to server/internal/member/file/provider.go diff --git a/internal/member/file/provider_test.go b/server/internal/member/file/provider_test.go similarity index 100% rename from internal/member/file/provider_test.go rename to server/internal/member/file/provider_test.go diff --git a/internal/member/file/types.go b/server/internal/member/file/types.go similarity index 100% rename from internal/member/file/types.go rename to server/internal/member/file/types.go diff --git a/internal/member/manager.go b/server/internal/member/manager.go similarity index 97% rename from internal/member/manager.go rename to server/internal/member/manager.go index 35386570..ccf32b22 100644 --- a/internal/member/manager.go +++ b/server/internal/member/manager.go @@ -141,6 +141,10 @@ func (manager *MemberManagerCtx) Login(username string, password string) (types. return nil, "", err } + if !profile.IsAdmin && manager.sessions.Settings().LockedLogins { + return nil, "", types.ErrSessionLoginsLocked + } + session, ok := manager.sessions.Get(id) if ok { if session.State().IsConnected { diff --git a/internal/member/multiuser/provider.go b/server/internal/member/multiuser/provider.go similarity index 100% rename from internal/member/multiuser/provider.go rename to server/internal/member/multiuser/provider.go diff --git a/internal/member/multiuser/types.go b/server/internal/member/multiuser/types.go similarity index 100% rename from internal/member/multiuser/types.go rename to server/internal/member/multiuser/types.go diff --git a/internal/member/noauth/provider.go b/server/internal/member/noauth/provider.go similarity index 100% rename from internal/member/noauth/provider.go rename to server/internal/member/noauth/provider.go diff --git a/internal/member/object/provider.go b/server/internal/member/object/provider.go similarity index 100% rename from internal/member/object/provider.go rename to server/internal/member/object/provider.go diff --git a/internal/member/object/types.go b/server/internal/member/object/types.go similarity index 100% rename from internal/member/object/types.go rename to server/internal/member/object/types.go diff --git a/server/internal/plugins/chat/config.go b/server/internal/plugins/chat/config.go new file mode 100644 index 00000000..dd24835c --- /dev/null +++ b/server/internal/plugins/chat/config.go @@ -0,0 +1,23 @@ +package chat + +import ( + "github.com/spf13/cobra" + "github.com/spf13/viper" +) + +type Config struct { + Enabled bool +} + +func (Config) Init(cmd *cobra.Command) error { + cmd.PersistentFlags().Bool("chat.enabled", true, "whether to enable chat plugin") + if err := viper.BindPFlag("chat.enabled", cmd.PersistentFlags().Lookup("chat.enabled")); err != nil { + return err + } + + return nil +} + +func (s *Config) Set() { + s.Enabled = viper.GetBool("chat.enabled") +} diff --git a/server/internal/plugins/chat/manager.go b/server/internal/plugins/chat/manager.go new file mode 100644 index 00000000..93e3c1bd --- /dev/null +++ b/server/internal/plugins/chat/manager.go @@ -0,0 +1,162 @@ +package chat + +import ( + "encoding/json" + "errors" + "fmt" + "net/http" + "time" + + "github.com/rs/zerolog" + "github.com/rs/zerolog/log" + + "github.com/demodesk/neko/pkg/auth" + "github.com/demodesk/neko/pkg/types" + "github.com/demodesk/neko/pkg/utils" +) + +func NewManager( + sessions types.SessionManager, + config *Config, +) *Manager { + logger := log.With().Str("module", "chat").Logger() + + return &Manager{ + logger: logger, + config: config, + sessions: sessions, + } +} + +type Manager struct { + logger zerolog.Logger + config *Config + sessions types.SessionManager +} + +type Settings struct { + CanSend bool `json:"can_send" mapstructure:"can_send"` + CanReceive bool `json:"can_receive" mapstructure:"can_receive"` +} + +func (m *Manager) settingsForSession(session types.Session) (Settings, error) { + settings := Settings{ + CanSend: true, // defaults to true + CanReceive: true, // defaults to true + } + err := m.sessions.Settings().Plugins.Unmarshal(PluginName, &settings) + if err != nil && !errors.Is(err, types.ErrPluginSettingsNotFound) { + return Settings{}, fmt.Errorf("unable to unmarshal %s plugin settings from global settings: %w", PluginName, err) + } + + profile := Settings{ + CanSend: true, // defaults to true + CanReceive: true, // defaults to true + } + + err = session.Profile().Plugins.Unmarshal(PluginName, &profile) + if err != nil && !errors.Is(err, types.ErrPluginSettingsNotFound) { + return Settings{}, fmt.Errorf("unable to unmarshal %s plugin settings from profile: %w", PluginName, err) + } + + return Settings{ + CanSend: m.config.Enabled && (settings.CanSend || session.Profile().IsAdmin) && profile.CanSend, + CanReceive: m.config.Enabled && (settings.CanReceive || session.Profile().IsAdmin) && profile.CanReceive, + }, nil +} + +func (m *Manager) sendMessage(session types.Session, content Content) { + now := time.Now() + + // get all sessions that have chat enabled + var sessions []types.Session + m.sessions.Range(func(s types.Session) bool { + if settings, err := m.settingsForSession(s); err == nil && settings.CanReceive { + sessions = append(sessions, s) + } + // continue iteration over all sessions + return true + }) + + // send content to all sessions + for _, s := range sessions { + s.Send(CHAT_MESSAGE, Message{ + ID: session.ID(), + Created: now, + Content: content, + }) + } +} + +func (m *Manager) Start() error { + // send init message once a user connects + m.sessions.OnConnected(func(session types.Session) { + session.Send(CHAT_INIT, Init{ + Enabled: m.config.Enabled, + }) + }) + + return nil +} + +func (m *Manager) Shutdown() error { + return nil +} + +func (m *Manager) Route(r types.Router) { + r.With(auth.AdminsOnly).Post("/", m.sendMessageHandler) +} + +func (m *Manager) WebSocketHandler(session types.Session, msg types.WebSocketMessage) bool { + switch msg.Event { + case CHAT_MESSAGE: + var content Content + if err := json.Unmarshal(msg.Payload, &content); err != nil { + m.logger.Error().Err(err).Msg("failed to unmarshal chat message") + // we processed the message, return true + return true + } + + settings, err := m.settingsForSession(session) + if err != nil { + m.logger.Error().Err(err).Msg("error checking chat permissions for this session") + // we processed the message, return true + return true + } + if !settings.CanSend { + m.logger.Warn().Msg("not allowed to send chat messages") + // we processed the message, return true + return true + } + + m.sendMessage(session, content) + return true + } + return false +} + +func (m *Manager) sendMessageHandler(w http.ResponseWriter, r *http.Request) error { + session, ok := auth.GetSession(r) + if !ok { + return utils.HttpUnauthorized("session not found") + } + + settings, err := m.settingsForSession(session) + if err != nil { + return utils.HttpInternalServerError(). + WithInternalErr(err). + Msg("error checking chat permissions for this session") + } + + if !settings.CanSend { + return utils.HttpForbidden("not allowed to send chat messages") + } + + content := Content{} + if err := utils.HttpJsonRequest(w, r, &content); err != nil { + return err + } + + m.sendMessage(session, content) + return utils.HttpSuccess(w) +} diff --git a/server/internal/plugins/chat/plugin.go b/server/internal/plugins/chat/plugin.go new file mode 100644 index 00000000..d4bc9463 --- /dev/null +++ b/server/internal/plugins/chat/plugin.go @@ -0,0 +1,35 @@ +package chat + +import ( + "github.com/demodesk/neko/pkg/types" +) + +type Plugin struct { + config *Config + manager *Manager +} + +func NewPlugin() *Plugin { + return &Plugin{ + config: &Config{}, + } +} + +func (p *Plugin) Name() string { + return PluginName +} + +func (p *Plugin) Config() types.PluginConfig { + return p.config +} + +func (p *Plugin) Start(m types.PluginManagers) error { + p.manager = NewManager(m.SessionManager, p.config) + m.ApiManager.AddRouter("/chat", p.manager.Route) + m.WebSocketManager.AddHandler(p.manager.WebSocketHandler) + return p.manager.Start() +} + +func (p *Plugin) Shutdown() error { + return p.manager.Shutdown() +} diff --git a/server/internal/plugins/chat/types.go b/server/internal/plugins/chat/types.go new file mode 100644 index 00000000..33e9d11a --- /dev/null +++ b/server/internal/plugins/chat/types.go @@ -0,0 +1,24 @@ +package chat + +import "time" + +const PluginName = "chat" + +const ( + CHAT_INIT = "chat/init" + CHAT_MESSAGE = "chat/message" +) + +type Init struct { + Enabled bool `json:"enabled"` +} + +type Content struct { + Text string `json:"text"` +} + +type Message struct { + ID string `json:"id"` + Created time.Time `json:"created"` + Content Content `json:"content"` +} diff --git a/internal/plugins/dependency.go b/server/internal/plugins/dependency.go similarity index 100% rename from internal/plugins/dependency.go rename to server/internal/plugins/dependency.go diff --git a/internal/plugins/dependency_test.go b/server/internal/plugins/dependency_test.go similarity index 100% rename from internal/plugins/dependency_test.go rename to server/internal/plugins/dependency_test.go diff --git a/server/internal/plugins/filetransfer/config.go b/server/internal/plugins/filetransfer/config.go new file mode 100644 index 00000000..04ed8109 --- /dev/null +++ b/server/internal/plugins/filetransfer/config.go @@ -0,0 +1,63 @@ +package filetransfer + +import ( + "path/filepath" + "time" + + "github.com/spf13/cobra" + "github.com/spf13/viper" +) + +type Config struct { + Enabled bool + RootDir string + RefreshInterval time.Duration +} + +func (Config) Init(cmd *cobra.Command) error { + cmd.PersistentFlags().Bool("filetransfer.enabled", false, "whether file transfer is enabled") + if err := viper.BindPFlag("filetransfer.enabled", cmd.PersistentFlags().Lookup("filetransfer.enabled")); err != nil { + return err + } + + cmd.PersistentFlags().String("filetransfer.dir", "/home/neko/Downloads", "root directory for file transfer") + if err := viper.BindPFlag("filetransfer.dir", cmd.PersistentFlags().Lookup("filetransfer.dir")); err != nil { + return err + } + + cmd.PersistentFlags().Duration("filetransfer.refresh_interval", 30*time.Second, "interval to refresh file list") + if err := viper.BindPFlag("filetransfer.refresh_interval", cmd.PersistentFlags().Lookup("filetransfer.refresh_interval")); err != nil { + return err + } + + // v2 config + + cmd.PersistentFlags().Bool("file_transfer_enabled", false, "enable file transfer feature") + if err := viper.BindPFlag("file_transfer_enabled", cmd.PersistentFlags().Lookup("file_transfer_enabled")); err != nil { + return err + } + + cmd.PersistentFlags().String("file_transfer_path", "", "path to use for file transfer") + if err := viper.BindPFlag("file_transfer_path", cmd.PersistentFlags().Lookup("file_transfer_path")); err != nil { + return err + } + + return nil +} + +func (s *Config) Set() { + s.Enabled = viper.GetBool("filetransfer.enabled") + rootDir := viper.GetString("filetransfer.dir") + s.RootDir = filepath.Clean(rootDir) + s.RefreshInterval = viper.GetDuration("filetransfer.refresh_interval") + + // v2 config + + if viper.IsSet("file_transfer_enabled") { + s.Enabled = viper.GetBool("file_transfer_enabled") + } + if viper.IsSet("file_transfer_path") { + rootDir = viper.GetString("file_transfer_path") + s.RootDir = filepath.Clean(rootDir) + } +} diff --git a/server/internal/plugins/filetransfer/manager.go b/server/internal/plugins/filetransfer/manager.go new file mode 100644 index 00000000..0a6603e4 --- /dev/null +++ b/server/internal/plugins/filetransfer/manager.go @@ -0,0 +1,332 @@ +package filetransfer + +import ( + "errors" + "fmt" + "io" + "net/http" + "os" + "path/filepath" + "regexp" + "sync" + "time" + + "github.com/demodesk/neko/pkg/auth" + "github.com/demodesk/neko/pkg/types" + "github.com/demodesk/neko/pkg/utils" + "github.com/fsnotify/fsnotify" + "github.com/rs/zerolog" + "github.com/rs/zerolog/log" +) + +const MULTIPART_FORM_MAX_MEMORY = 32 << 20 + +func NewManager( + sessions types.SessionManager, + config *Config, +) *Manager { + logger := log.With().Str("module", "filetransfer").Logger() + + return &Manager{ + logger: logger, + config: config, + sessions: sessions, + shutdown: make(chan struct{}), + } +} + +type Manager struct { + logger zerolog.Logger + config *Config + sessions types.SessionManager + shutdown chan struct{} + mu sync.RWMutex + fileList []Item +} + +func (m *Manager) isEnabledForSession(session types.Session) (bool, error) { + settings := Settings{ + Enabled: true, // defaults to true + } + err := m.sessions.Settings().Plugins.Unmarshal(PluginName, &settings) + if err != nil && !errors.Is(err, types.ErrPluginSettingsNotFound) { + return false, fmt.Errorf("unable to unmarshal %s plugin settings from global settings: %w", PluginName, err) + } + + profile := Settings{ + Enabled: true, // defaults to true + } + + err = session.Profile().Plugins.Unmarshal(PluginName, &profile) + if err != nil && !errors.Is(err, types.ErrPluginSettingsNotFound) { + return false, fmt.Errorf("unable to unmarshal %s plugin settings from profile: %w", PluginName, err) + } + + return m.config.Enabled && (settings.Enabled || session.Profile().IsAdmin) && profile.Enabled, nil +} + +func (m *Manager) refresh() (error, bool) { + // if file transfer is disabled, return immediately without refreshing + if !m.config.Enabled { + return nil, false + } + + files, err := ListFiles(m.config.RootDir) + if err != nil { + return err, false + } + + m.mu.Lock() + defer m.mu.Unlock() + + // check if file list has changed (todo: use hash instead of comparing all fields) + changed := false + if len(files) == len(m.fileList) { + for i, file := range files { + if file.Name != m.fileList[i].Name || file.Size != m.fileList[i].Size { + changed = true + break + } + } + } else { + changed = true + } + + m.fileList = files + return nil, changed +} + +func (m *Manager) broadcastUpdate() { + m.mu.RLock() + fileList := m.fileList + m.mu.RUnlock() + + m.sessions.Broadcast(FILETRANSFER_UPDATE, Message{ + Enabled: m.config.Enabled, + RootDir: m.config.RootDir, + Files: fileList, + }) +} + +func (m *Manager) sendUpdate(session types.Session) { + m.mu.RLock() + fileList := m.fileList + m.mu.RUnlock() + + session.Send(FILETRANSFER_UPDATE, Message{ + Enabled: m.config.Enabled, + RootDir: m.config.RootDir, + Files: fileList, + }) +} + +func (m *Manager) Start() error { + // send init message once a user connects + m.sessions.OnConnected(func(session types.Session) { + m.sendUpdate(session) + }) + + // if file transfer is disabled, return immediately without starting the watcher + if !m.config.Enabled { + return nil + } + + if _, err := os.Stat(m.config.RootDir); os.IsNotExist(err) { + err = os.Mkdir(m.config.RootDir, os.ModePerm) + m.logger.Err(err).Msg("creating file transfer directory") + } + + watcher, err := fsnotify.NewWatcher() + if err != nil { + return fmt.Errorf("unable to start file transfer dir watcher: %w", err) + } + + go func() { + defer watcher.Close() + + // periodically refresh file list + ticker := time.NewTicker(m.config.RefreshInterval) + defer ticker.Stop() + + for { + select { + case <-m.shutdown: + m.logger.Info().Msg("shutting down file transfer manager") + return + case <-ticker.C: + err, changed := m.refresh() + if err != nil { + m.logger.Err(err).Msg("unable to refresh file transfer list") + } + if changed { + m.broadcastUpdate() + } + case e, ok := <-watcher.Events: + if !ok { + m.logger.Info().Msg("file transfer dir watcher closed") + return + } + + if e.Has(fsnotify.Create) || e.Has(fsnotify.Remove) || e.Has(fsnotify.Rename) { + m.logger.Debug().Str("event", e.String()).Msg("file transfer dir watcher event") + + err, changed := m.refresh() + if err != nil { + m.logger.Err(err).Msg("unable to refresh file transfer list") + } + + if changed { + m.broadcastUpdate() + } + } + case err := <-watcher.Errors: + m.logger.Err(err).Msg("error in file transfer dir watcher") + } + } + }() + + if err := watcher.Add(m.config.RootDir); err != nil { + return fmt.Errorf("unable to watch file transfer dir: %w", err) + } + + // initial refresh + err, changed := m.refresh() + if err != nil { + return fmt.Errorf("unable to refresh file transfer list: %w", err) + } + if changed { + m.broadcastUpdate() + } + + return nil +} + +func (m *Manager) Shutdown() error { + close(m.shutdown) + return nil +} + +func (m *Manager) Route(r types.Router) { + r.With(auth.AdminsOnly).Get("/", m.downloadFileHandler) + r.With(auth.AdminsOnly).Post("/", m.uploadFileHandler) +} + +func (m *Manager) WebSocketHandler(session types.Session, msg types.WebSocketMessage) bool { + switch msg.Event { + case FILETRANSFER_UPDATE: + err, changed := m.refresh() + if err != nil { + m.logger.Err(err).Msg("unable to refresh file transfer list") + } + + if changed { + // broadcast update message to all clients + m.broadcastUpdate() + } else { + // send update message to this client only + m.sendUpdate(session) + } + return true + } + + // not handled by this plugin + return false +} + +func (m *Manager) downloadFileHandler(w http.ResponseWriter, r *http.Request) error { + session, ok := auth.GetSession(r) + if !ok { + return utils.HttpUnauthorized("session not found") + } + + enabled, err := m.isEnabledForSession(session) + if err != nil { + return utils.HttpInternalServerError(). + WithInternalErr(err). + Msg("error checking file transfer permissions") + } + + if !enabled { + return utils.HttpForbidden("file transfer is disabled") + } + + filename := r.URL.Query().Get("filename") + badChars, err := regexp.MatchString(`(?m)\.\.(?:\/|$)`, filename) + if filename == "" || badChars || err != nil { + return utils.HttpBadRequest(). + WithInternalErr(err). + Msg("bad filename") + } + + // ensure filename is clean and only contains the basename + filename = filepath.Clean(filename) + filename = filepath.Base(filename) + filePath := filepath.Join(m.config.RootDir, filename) + + http.ServeFile(w, r, filePath) + return nil +} + +func (m *Manager) uploadFileHandler(w http.ResponseWriter, r *http.Request) error { + session, ok := auth.GetSession(r) + if !ok { + return utils.HttpUnauthorized("session not found") + } + + enabled, err := m.isEnabledForSession(session) + if err != nil { + return utils.HttpInternalServerError(). + WithInternalErr(err). + Msg("error checking file transfer permissions") + } + + if !enabled { + return utils.HttpForbidden("file transfer is disabled") + } + + err = r.ParseMultipartForm(MULTIPART_FORM_MAX_MEMORY) + if err != nil || r.MultipartForm == nil { + return utils.HttpBadRequest(). + WithInternalErr(err). + Msg("error parsing form") + } + + defer func() { + err = r.MultipartForm.RemoveAll() + if err != nil { + m.logger.Warn().Err(err).Msg("failed to clean up multipart form") + } + }() + + for _, formheader := range r.MultipartForm.File["files"] { + // ensure filename is clean and only contains the basename + filename := filepath.Clean(formheader.Filename) + filename = filepath.Base(filename) + filePath := filepath.Join(m.config.RootDir, filename) + + formfile, err := formheader.Open() + if err != nil { + return utils.HttpBadRequest(). + WithInternalErr(err). + Msg("error opening formdata file") + } + defer formfile.Close() + + f, err := os.OpenFile(filePath, os.O_WRONLY|os.O_CREATE, 0644) + if err != nil { + return utils.HttpInternalServerError(). + WithInternalErr(err). + Msg("error opening file for writing") + } + defer f.Close() + + _, err = io.Copy(f, formfile) + if err != nil { + return utils.HttpInternalServerError(). + WithInternalErr(err). + Msg("error writing file") + } + } + + return nil +} diff --git a/server/internal/plugins/filetransfer/plugin.go b/server/internal/plugins/filetransfer/plugin.go new file mode 100644 index 00000000..a98672a1 --- /dev/null +++ b/server/internal/plugins/filetransfer/plugin.go @@ -0,0 +1,35 @@ +package filetransfer + +import ( + "github.com/demodesk/neko/pkg/types" +) + +type Plugin struct { + config *Config + manager *Manager +} + +func NewPlugin() *Plugin { + return &Plugin{ + config: &Config{}, + } +} + +func (p *Plugin) Name() string { + return PluginName +} + +func (p *Plugin) Config() types.PluginConfig { + return p.config +} + +func (p *Plugin) Start(m types.PluginManagers) error { + p.manager = NewManager(m.SessionManager, p.config) + m.ApiManager.AddRouter("/filetransfer", p.manager.Route) + m.WebSocketManager.AddHandler(p.manager.WebSocketHandler) + return p.manager.Start() +} + +func (p *Plugin) Shutdown() error { + return p.manager.Shutdown() +} diff --git a/server/internal/plugins/filetransfer/types.go b/server/internal/plugins/filetransfer/types.go new file mode 100644 index 00000000..32748d46 --- /dev/null +++ b/server/internal/plugins/filetransfer/types.go @@ -0,0 +1,30 @@ +package filetransfer + +const PluginName = "filetransfer" + +type Settings struct { + Enabled bool `json:"enabled" mapstructure:"enabled"` +} + +const ( + FILETRANSFER_UPDATE = "filetransfer/update" +) + +type Message struct { + Enabled bool `json:"enabled"` + RootDir string `json:"root_dir"` + Files []Item `json:"files"` +} + +type ItemType string + +const ( + ItemTypeFile ItemType = "file" + ItemTypeDir ItemType = "dir" +) + +type Item struct { + Name string `json:"name"` + Type ItemType `json:"type"` + Size int64 `json:"size,omitempty"` +} diff --git a/server/internal/plugins/filetransfer/utils.go b/server/internal/plugins/filetransfer/utils.go new file mode 100644 index 00000000..c4c828ae --- /dev/null +++ b/server/internal/plugins/filetransfer/utils.go @@ -0,0 +1,32 @@ +package filetransfer + +import "os" + +func ListFiles(path string) ([]Item, error) { + items, err := os.ReadDir(path) + if err != nil { + return nil, err + } + + out := make([]Item, len(items)) + for i, item := range items { + var itemType ItemType + var size int64 = 0 + if item.IsDir() { + itemType = ItemTypeDir + } else { + itemType = ItemTypeFile + info, err := item.Info() + if err == nil { + size = info.Size() + } + } + out[i] = Item{ + Name: item.Name(), + Type: itemType, + Size: size, + } + } + + return out, nil +} diff --git a/internal/plugins/manager.go b/server/internal/plugins/manager.go similarity index 94% rename from internal/plugins/manager.go rename to server/internal/plugins/manager.go index 79d8cc28..24ada08f 100644 --- a/internal/plugins/manager.go +++ b/server/internal/plugins/manager.go @@ -11,6 +11,8 @@ import ( "github.com/spf13/cobra" "github.com/demodesk/neko/internal/config" + "github.com/demodesk/neko/internal/plugins/chat" + "github.com/demodesk/neko/internal/plugins/filetransfer" "github.com/demodesk/neko/pkg/types" ) @@ -42,6 +44,10 @@ func New(config *config.Plugins) *ManagerCtx { manager.logger.Info().Msgf("loading finished, total %d plugins", manager.plugins.len()) } + // add built-in plugins + manager.plugins.addPlugin(filetransfer.NewPlugin()) + manager.plugins.addPlugin(chat.NewPlugin()) + return manager } diff --git a/internal/session/auth.go b/server/internal/session/auth.go similarity index 100% rename from internal/session/auth.go rename to server/internal/session/auth.go diff --git a/server/internal/session/manager.go b/server/internal/session/manager.go index cd93927a..db6aaa8d 100644 --- a/server/internal/session/manager.go +++ b/server/internal/session/manager.go @@ -1,241 +1,470 @@ package session import ( - "fmt" + "errors" "sync" + "sync/atomic" + "github.com/kataras/go-events" "github.com/rs/zerolog" "github.com/rs/zerolog/log" - "m1k1o/neko/internal/types" - "m1k1o/neko/internal/utils" + "github.com/demodesk/neko/internal/config" + "github.com/demodesk/neko/pkg/types" + "github.com/demodesk/neko/pkg/utils" ) -func New(capture types.CaptureManager) *SessionManager { - return &SessionManager{ - logger: log.With().Str("module", "session").Logger(), - host: "", - capture: capture, - eventsChannel: make(chan types.SessionEvent, 10), - members: make(map[string]*Session), - } -} - -type SessionManager struct { - mu sync.Mutex - logger zerolog.Logger - host string - capture types.CaptureManager - members map[string]*Session - eventsChannel chan types.SessionEvent - // TODO: Handle locks in sessions as flags. - controlLocked bool -} - -func (manager *SessionManager) New(id string, admin bool, socket types.WebSocket) types.Session { - session := &Session{ - id: id, - admin: admin, - manager: manager, - socket: socket, - logger: manager.logger.With().Str("id", id).Logger(), - connected: false, +func New(config *config.Session) *SessionManagerCtx { + manager := &SessionManagerCtx{ + logger: log.With().Str("module", "session").Logger(), + config: config, + settings: types.Settings{ + PrivateMode: config.PrivateMode, + LockedLogins: config.LockedLogins, + LockedControls: config.LockedControls || config.ControlProtection, + ControlProtection: config.ControlProtection, + ImplicitHosting: config.ImplicitHosting, + InactiveCursors: config.InactiveCursors, + MercifulReconnect: config.MercifulReconnect, + }, + tokens: make(map[string]string), + sessions: make(map[string]*SessionCtx), + cursors: make(map[types.Session][]types.Cursor), + emmiter: events.New(), } - manager.mu.Lock() - manager.members[id] = session - manager.capture.Audio().AddListener() - manager.capture.Video().AddListener() - manager.mu.Unlock() - - manager.eventsChannel <- types.SessionEvent{ - Type: types.SESSION_CREATED, - Id: id, - Session: session, - } - - return session -} - -func (manager *SessionManager) HasHost() bool { - return manager.host != "" -} - -func (manager *SessionManager) IsHost(id string) bool { - return manager.host == id -} - -func (manager *SessionManager) SetHost(id string) error { - manager.mu.Lock() - _, ok := manager.members[id] - manager.mu.Unlock() - - if ok { - manager.host = id - - manager.eventsChannel <- types.SessionEvent{ - Type: types.SESSION_HOST_SET, - Id: id, + // create API session + if config.APIToken != "" { + manager.apiSession = &SessionCtx{ + id: "API", + token: config.APIToken, + manager: manager, + logger: manager.logger.With().Str("session_id", "API").Logger(), + profile: types.MemberProfile{ + Name: "API Session", + IsAdmin: true, + CanLogin: true, + CanConnect: false, + CanWatch: true, + CanHost: true, + CanAccessClipboard: true, + }, } - - return nil } - return fmt.Errorf("invalid session id %s", id) + // try to load sessions from file + manager.load() + + return manager } -func (manager *SessionManager) GetHost() (types.Session, bool) { - manager.mu.Lock() - defer manager.mu.Unlock() +type SessionManagerCtx struct { + logger zerolog.Logger + config *config.Session - host, ok := manager.members[manager.host] - return host, ok + settings types.Settings + settingsMu sync.Mutex + + tokens map[string]string + sessions map[string]*SessionCtx + sessionsMu sync.Mutex + + hostId atomic.Value + + cursors map[types.Session][]types.Cursor + cursorsMu sync.Mutex + + emmiter events.EventEmmiter + apiSession *SessionCtx } -func (manager *SessionManager) ClearHost() { - id := manager.host - manager.host = "" - - manager.eventsChannel <- types.SessionEvent{ - Type: types.SESSION_HOST_CLEARED, - Id: id, +func (manager *SessionManagerCtx) Create(id string, profile types.MemberProfile) (types.Session, string, error) { + token, err := utils.NewUID(64) + if err != nil { + return nil, "", err } + + manager.sessionsMu.Lock() + if _, ok := manager.sessions[id]; ok { + manager.sessionsMu.Unlock() + return nil, "", types.ErrSessionAlreadyExists + } + + if _, ok := manager.tokens[token]; ok { + manager.sessionsMu.Unlock() + return nil, "", errors.New("session token already exists") + } + + session := &SessionCtx{ + id: id, + token: token, + manager: manager, + logger: manager.logger.With().Str("session_id", id).Logger(), + profile: profile, + } + + manager.tokens[token] = id + manager.sessions[id] = session + manager.sessionsMu.Unlock() + + manager.emmiter.Emit("created", session) + manager.save() + + return session, token, nil } -func (manager *SessionManager) Has(id string) bool { - manager.mu.Lock() - defer manager.mu.Unlock() +func (manager *SessionManagerCtx) Update(id string, profile types.MemberProfile) error { + manager.sessionsMu.Lock() - _, ok := manager.members[id] - return ok + session, ok := manager.sessions[id] + if !ok { + manager.sessionsMu.Unlock() + return types.ErrSessionNotFound + } + + old := session.profile + session.profile = profile + manager.sessionsMu.Unlock() + + manager.emmiter.Emit("profile_changed", session, profile, old) + manager.save() + + session.profileChanged() + return nil } -func (manager *SessionManager) Get(id string) (types.Session, bool) { - manager.mu.Lock() - defer manager.mu.Unlock() +func (manager *SessionManagerCtx) Delete(id string) error { + manager.sessionsMu.Lock() + session, ok := manager.sessions[id] + if !ok { + manager.sessionsMu.Unlock() + return types.ErrSessionNotFound + } - session, ok := manager.members[id] + delete(manager.tokens, session.token) + delete(manager.sessions, id) + manager.sessionsMu.Unlock() + + if session.State().IsConnected { + session.DestroyWebSocketPeer("session deleted") + } + + if session.State().IsWatching { + session.GetWebRTCPeer().Destroy() + } + + manager.emmiter.Emit("deleted", session) + manager.save() + + return nil +} + +func (manager *SessionManagerCtx) Disconnect(id string) error { + manager.sessionsMu.Lock() + session, ok := manager.sessions[id] + if !ok { + manager.sessionsMu.Unlock() + return types.ErrSessionNotFound + } + manager.sessionsMu.Unlock() + + if session.State().IsConnected { + session.DestroyWebSocketPeer("session disconnected") + } + + if session.State().IsWatching { + session.GetWebRTCPeer().Destroy() + } + + return nil +} + +func (manager *SessionManagerCtx) Get(id string) (types.Session, bool) { + manager.sessionsMu.Lock() + defer manager.sessionsMu.Unlock() + + session, ok := manager.sessions[id] return session, ok } -// TODO: Handle locks in sessions as flags. -func (manager *SessionManager) SetControlLocked(locked bool) { - manager.controlLocked = locked -} +func (manager *SessionManagerCtx) GetByToken(token string) (types.Session, bool) { + manager.sessionsMu.Lock() + id, ok := manager.tokens[token] + manager.sessionsMu.Unlock() -func (manager *SessionManager) CanControl(id string) bool { - session, ok := manager.Get(id) - return ok && (!manager.controlLocked || session.Admin()) -} - -func (manager *SessionManager) Admins() []*types.Member { - manager.mu.Lock() - defer manager.mu.Unlock() - - members := []*types.Member{} - for _, session := range manager.members { - if !session.connected || !session.admin { - continue - } - - member := session.Member() - if member != nil { - members = append(members, member) - } - } - - return members -} - -func (manager *SessionManager) Members() []*types.Member { - manager.mu.Lock() - defer manager.mu.Unlock() - - members := []*types.Member{} - for _, session := range manager.members { - if !session.connected { - continue - } - - member := session.Member() - if member != nil { - members = append(members, member) - } - } - return members -} - -func (manager *SessionManager) Destroy(id string) { - manager.mu.Lock() - session, ok := manager.members[id] if ok { - err := session.destroy() - delete(manager.members, id) + return manager.Get(id) + } - manager.capture.Audio().RemoveListener() - manager.capture.Video().RemoveListener() - manager.mu.Unlock() + // is API session + if manager.apiSession != nil && manager.apiSession.token == token { + return manager.apiSession, true + } - manager.eventsChannel <- types.SessionEvent{ - Type: types.SESSION_DESTROYED, - Id: id, - Session: session, + return nil, false +} + +func (manager *SessionManagerCtx) List() []types.Session { + manager.sessionsMu.Lock() + defer manager.sessionsMu.Unlock() + + var sessions []types.Session + for _, session := range manager.sessions { + sessions = append(sessions, session) + } + + return sessions +} + +func (manager *SessionManagerCtx) Range(f func(session types.Session) bool) { + manager.sessionsMu.Lock() + defer manager.sessionsMu.Unlock() + + for _, session := range manager.sessions { + if !f(session) { + return } - manager.logger.Err(err).Str("session_id", id).Msg("destroying session") + } +} + +// --- +// host +// --- + +func (manager *SessionManagerCtx) setHost(session, host types.Session) { + var hostId string + if host != nil { + hostId = host.ID() + } + + manager.hostId.Store(hostId) + manager.emmiter.Emit("host_changed", session, host) +} + +func (manager *SessionManagerCtx) GetHost() (types.Session, bool) { + hostId, ok := manager.hostId.Load().(string) + if !ok || hostId == "" { + return nil, false + } + + return manager.Get(hostId) +} + +func (manager *SessionManagerCtx) isHost(host types.Session) bool { + hostId, ok := manager.hostId.Load().(string) + return ok && hostId == host.ID() +} + +// --- +// cursors +// --- + +func (manager *SessionManagerCtx) SetCursor(cursor types.Cursor, session types.Session) { + manager.cursorsMu.Lock() + defer manager.cursorsMu.Unlock() + + list, ok := manager.cursors[session] + if !ok { + list = []types.Cursor{} + } + + list = append(list, cursor) + manager.cursors[session] = list +} + +func (manager *SessionManagerCtx) PopCursors() map[types.Session][]types.Cursor { + manager.cursorsMu.Lock() + defer manager.cursorsMu.Unlock() + + cursors := manager.cursors + manager.cursors = make(map[types.Session][]types.Cursor) + + return cursors +} + +// --- +// broadcasts +// --- + +func (manager *SessionManagerCtx) Broadcast(event string, payload any, exclude ...string) { + for _, session := range manager.List() { + if !session.State().IsConnected { + continue + } + + if len(exclude) > 0 { + if in, _ := utils.ArrayIn(session.ID(), exclude); in { + continue + } + } + + session.Send(event, payload) + } +} + +func (manager *SessionManagerCtx) AdminBroadcast(event string, payload any, exclude ...string) { + for _, session := range manager.List() { + if !session.State().IsConnected || !session.Profile().IsAdmin { + continue + } + + if len(exclude) > 0 { + if in, _ := utils.ArrayIn(session.ID(), exclude); in { + continue + } + } + + session.Send(event, payload) + } +} + +func (manager *SessionManagerCtx) InactiveCursorsBroadcast(event string, payload any, exclude ...string) { + for _, session := range manager.List() { + if !session.State().IsConnected || !session.Profile().CanSeeInactiveCursors { + continue + } + + if len(exclude) > 0 { + if in, _ := utils.ArrayIn(session.ID(), exclude); in { + continue + } + } + + session.Send(event, payload) + } +} + +// --- +// events +// --- + +func (manager *SessionManagerCtx) OnCreated(listener func(session types.Session)) { + manager.emmiter.On("created", func(payload ...any) { + listener(payload[0].(*SessionCtx)) + }) +} + +func (manager *SessionManagerCtx) OnDeleted(listener func(session types.Session)) { + manager.emmiter.On("deleted", func(payload ...any) { + listener(payload[0].(*SessionCtx)) + }) +} + +func (manager *SessionManagerCtx) OnConnected(listener func(session types.Session)) { + manager.emmiter.On("connected", func(payload ...any) { + listener(payload[0].(*SessionCtx)) + }) +} + +func (manager *SessionManagerCtx) OnDisconnected(listener func(session types.Session)) { + manager.emmiter.On("disconnected", func(payload ...any) { + listener(payload[0].(*SessionCtx)) + }) +} + +func (manager *SessionManagerCtx) OnProfileChanged(listener func(session types.Session, new, old types.MemberProfile)) { + manager.emmiter.On("profile_changed", func(payload ...any) { + listener(payload[0].(*SessionCtx), payload[1].(types.MemberProfile), payload[2].(types.MemberProfile)) + }) +} + +func (manager *SessionManagerCtx) OnStateChanged(listener func(session types.Session)) { + manager.emmiter.On("state_changed", func(payload ...any) { + listener(payload[0].(*SessionCtx)) + }) +} + +func (manager *SessionManagerCtx) OnHostChanged(listener func(session, host types.Session)) { + manager.emmiter.On("host_changed", func(payload ...any) { + if payload[1] == nil { + listener(payload[0].(*SessionCtx), nil) + } else { + listener(payload[0].(*SessionCtx), payload[1].(*SessionCtx)) + } + }) +} + +func (manager *SessionManagerCtx) OnSettingsChanged(listener func(session types.Session, new, old types.Settings)) { + manager.emmiter.On("settings_changed", func(payload ...any) { + listener(payload[0].(types.Session), payload[1].(types.Settings), payload[2].(types.Settings)) + }) +} + +// --- +// settings +// --- + +func (manager *SessionManagerCtx) UpdateSettingsFunc(session types.Session, f func(settings *types.Settings) bool) { + manager.settingsMu.Lock() + new := manager.settings + if f(&new) { + old := manager.settings + manager.settings = new + manager.settingsMu.Unlock() + manager.updateSettings(session, new, old) return } - - manager.mu.Unlock() + manager.settingsMu.Unlock() } -func (manager *SessionManager) Clear() error { - return nil -} +func (manager *SessionManagerCtx) updateSettings(session types.Session, new, old types.Settings) { + // if private mode changed + if old.PrivateMode != new.PrivateMode { + // update webrtc paused state for all sessions + for _, s := range manager.List() { + enabled := s.PrivateModeEnabled() -func (manager *SessionManager) Broadcast(v interface{}, exclude []string) error { - manager.mu.Lock() - defer manager.mu.Unlock() + // if session had control, it must release it + if enabled && s.IsHost() { + session.ClearHost() + } - for id, session := range manager.members { - if !session.connected { - continue - } - - if in, _ := utils.ArrayIn(id, exclude); in { - continue - } - - if err := session.Send(v); err != nil { - return err + // its webrtc connection will be paused or unpaused + if webrtcPeer := s.GetWebRTCPeer(); webrtcPeer != nil { + webrtcPeer.SetPaused(enabled) + } } } - return nil -} + // if control protection changed and controls are not locked + if old.ControlProtection != new.ControlProtection && new.ControlProtection && !new.LockedControls { + // if there is no admin, lock controls + hasAdmin := false + manager.Range(func(session types.Session) bool { + if session.Profile().IsAdmin && session.State().IsConnected { + hasAdmin = true + return false + } + return true + }) -func (manager *SessionManager) AdminBroadcast(v interface{}, exclude []string) error { - manager.mu.Lock() - defer manager.mu.Unlock() - - for id, session := range manager.members { - if !session.connected || !session.admin { - continue - } - - if in, _ := utils.ArrayIn(id, exclude); in { - continue - } - - if err := session.Send(v); err != nil { - return err + if !hasAdmin { + manager.settingsMu.Lock() + manager.settings.LockedControls = true + new.LockedControls = true + manager.settingsMu.Unlock() } } - return nil + // if contols have been locked + if old.LockedControls != new.LockedControls && new.LockedControls { + // if the host is not admin, it must release controls + host, hasHost := manager.GetHost() + if hasHost && !host.Profile().IsAdmin { + session.ClearHost() + } + } + + manager.emmiter.Emit("settings_changed", session, new, old) } -func (manager *SessionManager) GetEventsChannel() chan types.SessionEvent { - return manager.eventsChannel +func (manager *SessionManagerCtx) Settings() types.Settings { + manager.settingsMu.Lock() + defer manager.settingsMu.Unlock() + + return manager.settings } -var _ types.SessionManager = (*SessionManager)(nil) +func (manager *SessionManagerCtx) CookieEnabled() bool { + return manager.config.CookieEnabled +} diff --git a/internal/session/serialize.go b/server/internal/session/serialize.go similarity index 100% rename from internal/session/serialize.go rename to server/internal/session/serialize.go diff --git a/server/internal/session/session.go b/server/internal/session/session.go index 3f964c4d..76d7eff6 100644 --- a/server/internal/session/session.go +++ b/server/internal/session/session.go @@ -1,190 +1,289 @@ package session import ( - "m1k1o/neko/internal/types" - "m1k1o/neko/internal/types/event" - "m1k1o/neko/internal/types/message" + "sync" + "time" "github.com/rs/zerolog" + + "github.com/demodesk/neko/pkg/types" + "github.com/demodesk/neko/pkg/types/event" ) -type Session struct { - logger zerolog.Logger - id string - name string - admin bool - muted bool - connected bool - manager *SessionManager - socket types.WebSocket - peer types.Peer +// client is expected to reconnect within 5 second +// if some unexpected websocket disconnect happens +const WS_DELAYED_DURATION = 5 * time.Second + +type SessionCtx struct { + id string + token string + logger zerolog.Logger + manager *SessionManagerCtx + profile types.MemberProfile + state types.SessionState + + websocketPeer types.WebSocketPeer + websocketMu sync.Mutex + + // websocket delayed set connected events + wsDelayedMu sync.Mutex + wsDelayedTimer *time.Timer + + webrtcPeer types.WebRTCPeer + webrtcMu sync.Mutex } -func (session *Session) ID() string { +func (session *SessionCtx) ID() string { return session.id } -func (session *Session) Name() string { - return session.name +func (session *SessionCtx) Profile() types.MemberProfile { + return session.profile } -func (session *Session) Admin() bool { - return session.admin -} - -func (session *Session) Muted() bool { - return session.muted -} - -func (session *Session) Connected() bool { - return session.connected -} - -func (session *Session) Address() string { - if session.socket == nil { - return "" +func (session *SessionCtx) profileChanged() { + if !session.profile.CanHost && session.IsHost() { + session.ClearHost() } - return session.socket.Address() -} -func (session *Session) Member() *types.Member { - return &types.Member{ - ID: session.id, - Name: session.name, - Admin: session.admin, - Muted: session.muted, + if (!session.profile.CanConnect || !session.profile.CanLogin || !session.profile.CanWatch) && session.state.IsWatching { + session.GetWebRTCPeer().Destroy() + } + + if (!session.profile.CanConnect || !session.profile.CanLogin) && session.state.IsConnected { + session.DestroyWebSocketPeer("profile changed") + } + + // update webrtc paused state + if webrtcPeer := session.GetWebRTCPeer(); webrtcPeer != nil { + webrtcPeer.SetPaused(session.PrivateModeEnabled()) } } -func (session *Session) SetMuted(muted bool) { - session.muted = muted +func (session *SessionCtx) State() types.SessionState { + return session.state } -func (session *Session) SetName(name string) error { - session.name = name - return nil +func (session *SessionCtx) IsHost() bool { + return session.manager.isHost(session) } -func (session *Session) SetSocket(socket types.WebSocket) error { - session.socket = socket - return nil +// only needed for legacy webrtc handler +func (session *SessionCtx) LegacyIsHost() bool { + implicitHosting := session.manager.Settings().ImplicitHosting + return !(!implicitHosting && !session.manager.isHost(session)) || (implicitHosting && !session.profile.CanHost) } -func (session *Session) SetPeer(peer types.Peer) error { - session.peer = peer - return nil +func (session *SessionCtx) SetAsHost() { + session.manager.setHost(session, session) } -func (session *Session) SetConnected(connected bool) error { - session.connected = connected +func (session *SessionCtx) SetAsHostBy(host types.Session) { + session.manager.setHost(session, host) +} + +func (session *SessionCtx) ClearHost() { + session.manager.setHost(session, nil) +} + +func (session *SessionCtx) PrivateModeEnabled() bool { + return session.manager.Settings().PrivateMode && !session.profile.IsAdmin +} + +func (session *SessionCtx) SetCursor(cursor types.Cursor) { + if session.manager.Settings().InactiveCursors && session.profile.SendsInactiveCursor { + session.manager.SetCursor(cursor, session) + } +} + +// --- +// websocket +// --- + +// Connect WebSocket peer sets current peer and emits connected event. It also destroys the +// previous peer, if there was one. If the peer is already set, it will be ignored. +func (session *SessionCtx) ConnectWebSocketPeer(websocketPeer types.WebSocketPeer) { + session.websocketMu.Lock() + isCurrentPeer := websocketPeer == session.websocketPeer + session.websocketPeer, websocketPeer = websocketPeer, session.websocketPeer + session.websocketMu.Unlock() + + // ignore if already set + if isCurrentPeer { + return + } + + session.logger.Info().Msg("set websocket connected") + + // update state + now := time.Now() + session.state.IsConnected = true + session.state.ConnectedSince = &now + session.state.NotConnectedSince = nil + + session.manager.emmiter.Emit("connected", session) + + // if there is a previous peer, destroy it + if websocketPeer != nil { + websocketPeer.Destroy("connection replaced") + } +} + +// Disconnect WebSocket peer sets current peer to nil and emits disconnected event. It also +// allows for a delayed disconnect. That means, the peer will not be disconnected immediately, +// but after a delay. If the peer is connected again before the delay, the disconnect will be +// cancelled. +// +// If the peer is not the current peer or the peer is nil, it will be ignored. +func (session *SessionCtx) DisconnectWebSocketPeer(websocketPeer types.WebSocketPeer, delayed bool) { + session.websocketMu.Lock() + isCurrentPeer := websocketPeer == session.websocketPeer && websocketPeer != nil + session.websocketMu.Unlock() + + // ignore if not current peer + if !isCurrentPeer { + return + } + + // + // ws delayed + // + + var wsDelayedTimer *time.Timer + + if delayed { + wsDelayedTimer = time.AfterFunc(WS_DELAYED_DURATION, func() { + session.DisconnectWebSocketPeer(websocketPeer, false) + }) + } + + session.wsDelayedMu.Lock() + if session.wsDelayedTimer != nil { + session.wsDelayedTimer.Stop() + } + session.wsDelayedTimer = wsDelayedTimer + session.wsDelayedMu.Unlock() + + if delayed { + session.logger.Info().Msg("delayed websocket disconnected") + return + } + + // + // not delayed + // + + session.logger.Info().Msg("set websocket disconnected") + + now := time.Now() + session.state.IsConnected = false + session.state.ConnectedSince = nil + session.state.NotConnectedSince = &now + + session.manager.emmiter.Emit("disconnected", session) + + session.websocketMu.Lock() + if websocketPeer == session.websocketPeer { + session.websocketPeer = nil + } + session.websocketMu.Unlock() +} + +// Destroy WebSocket peer disconnects the peer and destroys it. It ensures that the peer is +// disconnected immediately even though normal flow would be to disconnect it delayed. +func (session *SessionCtx) DestroyWebSocketPeer(reason string) { + session.websocketMu.Lock() + peer := session.websocketPeer + session.websocketMu.Unlock() + + if peer == nil { + return + } + + // disconnect peer first, so that it is not used anymore + session.DisconnectWebSocketPeer(peer, false) + + // destroy it afterwards + peer.Destroy(reason) +} + +// Send event to websocket peer. +func (session *SessionCtx) Send(event string, payload any) { + session.websocketMu.Lock() + peer := session.websocketPeer + session.websocketMu.Unlock() + + if peer != nil { + peer.Send(event, payload) + } +} + +// --- +// webrtc +// --- + +// Set webrtc peer and destroy the old one, if there is old one. +func (session *SessionCtx) SetWebRTCPeer(webrtcPeer types.WebRTCPeer) { + session.webrtcMu.Lock() + session.webrtcPeer, webrtcPeer = webrtcPeer, session.webrtcPeer + session.webrtcMu.Unlock() + + if webrtcPeer != nil && webrtcPeer != session.webrtcPeer { + webrtcPeer.Destroy() + } +} + +// Set if current webrtc peer is connected or not. Since there might be lefover calls from +// webrtc peer, that are not used anymore, we need to check if the webrtc peer is still the +// same as the one we are setting the connected state for. +// +// If webrtc peer is disconnected, we don't expect it to be reconnected, so we set it to nil +// and send a signal close to the client. New connection is expected to use a new webrtc peer. +func (session *SessionCtx) SetWebRTCConnected(webrtcPeer types.WebRTCPeer, connected bool) { + session.webrtcMu.Lock() + isCurrentPeer := webrtcPeer == session.webrtcPeer + session.webrtcMu.Unlock() + + if !isCurrentPeer { + return + } + + session.logger.Info(). + Bool("connected", connected). + Msg("set webrtc connected") + + // update state + session.state.IsWatching = connected + if now := time.Now(); connected { + session.state.WatchingSince = &now + session.state.NotWatchingSince = nil + } else { + session.state.WatchingSince = nil + session.state.NotWatchingSince = &now + } + + session.manager.emmiter.Emit("state_changed", session) + if connected { - session.manager.eventsChannel <- types.SessionEvent{ - Type: types.SESSION_CONNECTED, - Id: session.id, - Session: session, - } + return + } + + session.webrtcMu.Lock() + isCurrentPeer = webrtcPeer == session.webrtcPeer + if isCurrentPeer { + session.webrtcPeer = nil + } + session.webrtcMu.Unlock() + + if isCurrentPeer { + session.Send(event.SIGNAL_CLOSE, nil) } - return nil } -func (session *Session) Kick(reason string) error { - if session.socket == nil { - return nil - } - if err := session.socket.Send(&message.SystemMessage{ - Event: event.SYSTEM_DISCONNECT, - Message: reason, - }); err != nil { - return err - } +// Get current WebRTC peer. Nil if not connected. +func (session *SessionCtx) GetWebRTCPeer() types.WebRTCPeer { + session.webrtcMu.Lock() + defer session.webrtcMu.Unlock() - return session.destroy() -} - -func (session *Session) Send(v interface{}) error { - if session.socket == nil { - return nil - } - return session.socket.Send(v) -} - -func (session *Session) SignalLocalOffer(sdp string) error { - if session.peer == nil { - return nil - } - session.logger.Info().Msg("signal update - LocalOffer") - return session.socket.Send(&message.SignalOffer{ - Event: event.SIGNAL_OFFER, - SDP: sdp, - }) -} - -func (session *Session) SignalLocalAnswer(sdp string) error { - if session.peer == nil { - return nil - } - - session.logger.Info().Msg("signal update - LocalAnswer") - return session.socket.Send(&message.SignalAnswer{ - Event: event.SIGNAL_ANSWER, - SDP: sdp, - }) -} - -func (session *Session) SignalLocalCandidate(data string) error { - if session.socket == nil { - return nil - } - session.logger.Info().Msg("signal update - LocalCandidate") - return session.socket.Send(&message.SignalCandidate{ - Event: event.SIGNAL_CANDIDATE, - Data: data, - }) -} - -func (session *Session) SignalRemoteOffer(sdp string) error { - if session.peer == nil { - return nil - } - if err := session.peer.SetOffer(sdp); err != nil { - return err - } - sdp, err := session.peer.CreateAnswer() - if err != nil { - return err - } - session.logger.Info().Msg("signal update - RemoteOffer") - return session.SignalLocalAnswer(sdp) -} - -func (session *Session) SignalRemoteAnswer(sdp string) error { - if session.peer == nil { - return nil - } - session.logger.Info().Msg("signal update - RemoteAnswer") - return session.peer.SetAnswer(sdp) -} - -func (session *Session) SignalRemoteCandidate(data string) error { - if session.socket == nil { - return nil - } - session.logger.Info().Msg("signal update - RemoteCandidate") - return session.peer.SetCandidate(data) -} - -func (session *Session) destroy() error { - if session.socket != nil { - if err := session.socket.Destroy(); err != nil { - return err - } - } - - if session.peer != nil { - if err := session.peer.Destroy(); err != nil { - return err - } - } - - return nil + return session.webrtcPeer } diff --git a/server/internal/types/capture.go b/server/internal/types/capture.go deleted file mode 100644 index 36e54b8b..00000000 --- a/server/internal/types/capture.go +++ /dev/null @@ -1,38 +0,0 @@ -package types - -import ( - "errors" - - "m1k1o/neko/internal/types/codec" -) - -var ( - ErrCapturePipelineAlreadyExists = errors.New("capture pipeline already exists") -) - -type BroadcastManager interface { - Start(url string) error - Stop() - Started() bool - Url() string -} - -type StreamSinkManager interface { - Codec() codec.RTPCodec - - AddListener() error - RemoveListener() error - - ListenersCount() int - Started() bool - GetSampleChannel() chan Sample -} - -type CaptureManager interface { - Start() - Shutdown() error - - Broadcast() BroadcastManager - Audio() StreamSinkManager - Video() StreamSinkManager -} diff --git a/server/internal/types/codec/codecs.go b/server/internal/types/codec/codecs.go deleted file mode 100644 index 4c5b8435..00000000 --- a/server/internal/types/codec/codecs.go +++ /dev/null @@ -1,196 +0,0 @@ -package codec - -import ( - "strings" - - "github.com/pion/webrtc/v3" -) - -var RTCPFeedback = []webrtc.RTCPFeedback{ - {Type: webrtc.TypeRTCPFBTransportCC, Parameter: ""}, - {Type: webrtc.TypeRTCPFBGoogREMB, Parameter: ""}, - - // https://www.iana.org/assignments/sdp-parameters/sdp-parameters.xhtml#sdp-parameters-19 - {Type: webrtc.TypeRTCPFBCCM, Parameter: "fir"}, - - // https://www.iana.org/assignments/sdp-parameters/sdp-parameters.xhtml#sdp-parameters-15 - {Type: webrtc.TypeRTCPFBNACK, Parameter: "pli"}, - {Type: webrtc.TypeRTCPFBNACK, Parameter: ""}, -} - -func ParseRTC(codec webrtc.RTPCodecParameters) (RTPCodec, bool) { - codecName := strings.Split(codec.RTPCodecCapability.MimeType, "/")[1] - return ParseStr(codecName) -} - -func ParseStr(codecName string) (codec RTPCodec, ok bool) { - ok = true - - switch strings.ToLower(codecName) { - case VP8().Name: - codec = VP8() - case VP9().Name: - codec = VP9() - case AV1().Name: - codec = AV1() - case H264().Name: - codec = H264() - case Opus().Name: - codec = Opus() - case G722().Name: - codec = G722() - case PCMU().Name: - codec = PCMU() - case PCMA().Name: - codec = PCMA() - default: - ok = false - } - - return -} - -type RTPCodec struct { - Name string - PayloadType webrtc.PayloadType - Type webrtc.RTPCodecType - Capability webrtc.RTPCodecCapability -} - -func (codec RTPCodec) Register(engine *webrtc.MediaEngine) error { - return engine.RegisterCodec(webrtc.RTPCodecParameters{ - RTPCodecCapability: codec.Capability, - PayloadType: codec.PayloadType, - }, codec.Type) -} - -func (codec RTPCodec) IsVideo() bool { - return codec.Type == webrtc.RTPCodecTypeVideo -} - -func (codec RTPCodec) IsAudio() bool { - return codec.Type == webrtc.RTPCodecTypeAudio -} - -func VP8() RTPCodec { - return RTPCodec{ - Name: "vp8", - PayloadType: 96, - Type: webrtc.RTPCodecTypeVideo, - Capability: webrtc.RTPCodecCapability{ - MimeType: webrtc.MimeTypeVP8, - ClockRate: 90000, - Channels: 0, - SDPFmtpLine: "", - RTCPFeedback: RTCPFeedback, - }, - } -} - -// TODO: Profile ID. -func VP9() RTPCodec { - return RTPCodec{ - Name: "vp9", - PayloadType: 98, - Type: webrtc.RTPCodecTypeVideo, - Capability: webrtc.RTPCodecCapability{ - MimeType: webrtc.MimeTypeVP9, - ClockRate: 90000, - Channels: 0, - SDPFmtpLine: "profile-id=0", - RTCPFeedback: RTCPFeedback, - }, - } -} - -// TODO: Profile ID. -func H264() RTPCodec { - return RTPCodec{ - Name: "h264", - PayloadType: 102, - Type: webrtc.RTPCodecTypeVideo, - Capability: webrtc.RTPCodecCapability{ - MimeType: webrtc.MimeTypeH264, - ClockRate: 90000, - Channels: 0, - SDPFmtpLine: "level-asymmetry-allowed=1;packetization-mode=1;profile-level-id=42e01f", - RTCPFeedback: RTCPFeedback, - }, - } -} - -// TODO: Profile ID. -func AV1() RTPCodec { - return RTPCodec{ - Name: "av1", - PayloadType: 96, - Type: webrtc.RTPCodecTypeVideo, - Capability: webrtc.RTPCodecCapability{ - MimeType: webrtc.MimeTypeAV1, - ClockRate: 90000, - Channels: 0, - SDPFmtpLine: "", - RTCPFeedback: RTCPFeedback, - }, - } -} - -func Opus() RTPCodec { - return RTPCodec{ - Name: "opus", - PayloadType: 111, - Type: webrtc.RTPCodecTypeAudio, - Capability: webrtc.RTPCodecCapability{ - MimeType: webrtc.MimeTypeOpus, - ClockRate: 48000, - Channels: 2, - SDPFmtpLine: "useinbandfec=1;stereo=1", - RTCPFeedback: []webrtc.RTCPFeedback{}, - }, - } -} - -func G722() RTPCodec { - return RTPCodec{ - Name: "g722", - PayloadType: 9, - Type: webrtc.RTPCodecTypeAudio, - Capability: webrtc.RTPCodecCapability{ - MimeType: webrtc.MimeTypeG722, - ClockRate: 8000, - Channels: 0, - SDPFmtpLine: "", - RTCPFeedback: []webrtc.RTCPFeedback{}, - }, - } -} - -func PCMU() RTPCodec { - return RTPCodec{ - Name: "pcmu", - PayloadType: 0, - Type: webrtc.RTPCodecTypeAudio, - Capability: webrtc.RTPCodecCapability{ - MimeType: webrtc.MimeTypePCMU, - ClockRate: 8000, - Channels: 0, - SDPFmtpLine: "", - RTCPFeedback: []webrtc.RTCPFeedback{}, - }, - } -} - -func PCMA() RTPCodec { - return RTPCodec{ - Name: "pcma", - PayloadType: 8, - Type: webrtc.RTPCodecTypeAudio, - Capability: webrtc.RTPCodecCapability{ - MimeType: webrtc.MimeTypePCMA, - ClockRate: 8000, - Channels: 0, - SDPFmtpLine: "", - RTCPFeedback: []webrtc.RTCPFeedback{}, - }, - } -} diff --git a/server/internal/types/desktop.go b/server/internal/types/desktop.go deleted file mode 100644 index e70e370a..00000000 --- a/server/internal/types/desktop.go +++ /dev/null @@ -1,77 +0,0 @@ -package types - -import "image" - -type CursorImage struct { - Width uint16 - Height uint16 - Xhot uint16 - Yhot uint16 - Serial uint64 - Image *image.RGBA -} - -type ScreenSize struct { - Width int `json:"width"` - Height int `json:"height"` - Rate int16 `json:"rate"` -} - -type ScreenConfiguration struct { - Width int `json:"width"` - Height int `json:"height"` - Rates map[int]int16 `json:"rates"` -} - -type KeyboardModifiers struct { - NumLock *bool - CapsLock *bool -} - -type KeyboardMap struct { - Layout string - Variant string -} - -type DesktopErrorMessage struct { - Error_code uint8 - Message string - Request_code uint8 - Minor_code uint8 -} - -type DesktopManager interface { - Start() - Shutdown() error - GetScreenSizeChangeChannel() (before chan bool) // true - before, false - after - - // clipboard - ReadClipboard() string - WriteClipboard(data string) - - // xorg - Move(x, y int) - GetCursorPosition() (int, int) - Scroll(x, y int) - ButtonDown(code uint32) error - KeyDown(code uint32) error - ButtonUp(code uint32) error - KeyUp(code uint32) error - ButtonPress(code uint32) error - KeyPress(codes ...uint32) error - ResetKeys() - ScreenConfigurations() map[int]ScreenConfiguration - SetScreenSize(ScreenSize) error - GetScreenSize() *ScreenSize - SetKeyboardMap(KeyboardMap) error - GetKeyboardMap() (*KeyboardMap, error) - SetKeyboardModifiers(mod KeyboardModifiers) - GetKeyboardModifiers() KeyboardModifiers - GetCursorImage() *CursorImage - GetScreenshotImage() *image.RGBA - - // xevent - GetCursorChangedChannel() chan uint64 - GetClipboardUpdatedChannel() chan struct{} - GetEventErrorChannel() chan DesktopErrorMessage -} diff --git a/server/internal/types/session.go b/server/internal/types/session.go deleted file mode 100644 index d90faea5..00000000 --- a/server/internal/types/session.go +++ /dev/null @@ -1,67 +0,0 @@ -package types - -type Member struct { - ID string `json:"id"` - Name string `json:"displayname"` - Admin bool `json:"admin"` - Muted bool `json:"muted"` -} - -type SessionEventType int - -const ( - SESSION_CREATED SessionEventType = iota - SESSION_CONNECTED - SESSION_DESTROYED - SESSION_HOST_SET - SESSION_HOST_CLEARED -) - -type SessionEvent struct { - Type SessionEventType - Id string - Session Session -} - -type Session interface { - ID() string - Name() string - Admin() bool - Muted() bool - Connected() bool - Member() *Member - SetMuted(muted bool) - SetName(name string) error - SetConnected(connected bool) error - SetSocket(socket WebSocket) error - SetPeer(peer Peer) error - Address() string - Kick(message string) error - Send(v interface{}) error - SignalLocalOffer(sdp string) error - SignalLocalAnswer(sdp string) error - SignalLocalCandidate(data string) error - SignalRemoteOffer(sdp string) error - SignalRemoteAnswer(sdp string) error - SignalRemoteCandidate(data string) error -} - -type SessionManager interface { - New(id string, admin bool, socket WebSocket) Session - HasHost() bool - IsHost(id string) bool - SetHost(id string) error - GetHost() (Session, bool) - ClearHost() - Has(id string) bool - Get(id string) (Session, bool) - SetControlLocked(locked bool) - CanControl(id string) bool - Members() []*Member - Admins() []*Member - Destroy(id string) - Clear() error - Broadcast(v interface{}, exclude []string) error - AdminBroadcast(v interface{}, exclude []string) error - GetEventsChannel() chan SessionEvent -} diff --git a/server/internal/types/webrtc.go b/server/internal/types/webrtc.go deleted file mode 100644 index 7db122c9..00000000 --- a/server/internal/types/webrtc.go +++ /dev/null @@ -1,27 +0,0 @@ -package types - -import ( - "github.com/pion/webrtc/v3" - "github.com/pion/webrtc/v3/pkg/media" -) - -type Sample media.Sample - -type WebRTCManager interface { - Start() - Shutdown() error - CreatePeer(id string, session Session) (Peer, error) - ICELite() bool - ICEServers() []webrtc.ICEServer - ImplicitControl() bool -} - -type Peer interface { - CreateOffer() (string, error) - CreateAnswer() (string, error) - SetOffer(sdp string) error - SetAnswer(sdp string) error - SetCandidate(candidateString string) error - WriteData(v interface{}) error - Destroy() error -} diff --git a/server/internal/types/websocket.go b/server/internal/types/websocket.go deleted file mode 100644 index 15b684c9..00000000 --- a/server/internal/types/websocket.go +++ /dev/null @@ -1,48 +0,0 @@ -package types - -import ( - "net/http" - "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 WebSocket interface { - Address() string - Send(v interface{}) error - Destroy() error -} - -type WebSocketHandler interface { - Start() - Shutdown() error - Upgrade(w http.ResponseWriter, r *http.Request) error - Stats() Stats - IsLocked(resource string) bool - IsAdmin(password string) (bool, error) - - // File Transfer - CanTransferFiles(password string) (bool, error) - FileTransferPath(filename string) string - FileTransferEnabled() bool -} - -type FileListItem struct { - Filename string `json:"name"` - Type string `json:"type"` - Size int64 `json:"size"` -} diff --git a/server/internal/utils/array.go b/server/internal/utils/array.go deleted file mode 100644 index 3f6901c7..00000000 --- a/server/internal/utils/array.go +++ /dev/null @@ -1,15 +0,0 @@ -package utils - -func ArrayIn[T comparable](val T, array []T) (exists bool, index int) { - index = -1 - - for i, v := range array { - if v == val { - index = i - exists = true - return - } - } - - return -} diff --git a/server/internal/utils/color.go b/server/internal/utils/color.go deleted file mode 100644 index 919887c2..00000000 --- a/server/internal/utils/color.go +++ /dev/null @@ -1,34 +0,0 @@ -package utils - -import ( - "fmt" - "regexp" -) - -const ( - char = "&" -) - -// Colors: http://www.lihaoyi.com/post/BuildyourownCommandLinewithANSIescapecodes.html -var re = regexp.MustCompile(char + `(?m)([0-9]{1,2};[0-9]{1,2}|[0-9]{1,2})`) - -func Color(str string) string { - result := "" - lastIndex := 0 - - for _, v := range re.FindAllSubmatchIndex([]byte(str), -1) { - groups := []string{} - for i := 0; i < len(v); i += 2 { - groups = append(groups, str[v[i]:v[i+1]]) - } - - result += str[lastIndex:v[0]] + "\033[" + groups[1] + "m" - lastIndex = v[1] - } - - return result + str[lastIndex:] -} - -func Colorf(format string, a ...interface{}) string { - return fmt.Sprintf(Color(format), a...) -} diff --git a/server/internal/utils/files.go b/server/internal/utils/files.go deleted file mode 100644 index d9f24db7..00000000 --- a/server/internal/utils/files.go +++ /dev/null @@ -1,36 +0,0 @@ -package utils - -import ( - "os" - - "m1k1o/neko/internal/types" -) - -func ListFiles(path string) ([]types.FileListItem, error) { - items, err := os.ReadDir(path) - if err != nil { - return nil, err - } - - out := make([]types.FileListItem, len(items)) - for i, item := range items { - var itemType string = "" - var size int64 = 0 - if item.IsDir() { - itemType = "dir" - } else { - itemType = "file" - info, err := item.Info() - if err == nil { - size = info.Size() - } - } - out[i] = types.FileListItem{ - Filename: item.Name(), - Type: itemType, - Size: size, - } - } - - return out, nil -} diff --git a/server/internal/utils/ip.go b/server/internal/utils/ip.go deleted file mode 100644 index 40239355..00000000 --- a/server/internal/utils/ip.go +++ /dev/null @@ -1,40 +0,0 @@ -package utils - -import ( - "bytes" - "io" - "net" - "net/http" - "time" -) - -// dig @resolver1.opendns.com ANY myip.opendns.com +short -4 - -func GetIP(serverUrl string) (string, error) { - tr := &http.Transport{ - Proxy: nil, // ignore proxy - DialContext: (&net.Dialer{ - Timeout: 10 * time.Second, - KeepAlive: 30 * time.Second, - DualStack: true, - }).DialContext, - MaxIdleConns: 30, - IdleConnTimeout: 90 * time.Second, - TLSHandshakeTimeout: 15 * time.Second, - ExpectContinueTimeout: 1 * time.Second, - } - - client := &http.Client{Transport: tr} - rsp, err := client.Get(serverUrl) - if err != nil { - return "", err - } - defer rsp.Body.Close() - - buf, err := io.ReadAll(rsp.Body) - if err != nil { - return "", err - } - - return string(bytes.TrimSpace(buf)), nil -} diff --git a/server/internal/utils/json.go b/server/internal/utils/json.go deleted file mode 100644 index 7ea0494d..00000000 --- a/server/internal/utils/json.go +++ /dev/null @@ -1,10 +0,0 @@ -package utils - -import "encoding/json" - -func Unmarshal(in interface{}, raw []byte, callback func() error) error { - if err := json.Unmarshal(raw, &in); err != nil { - return err - } - return callback() -} diff --git a/server/internal/utils/uid.go b/server/internal/utils/uid.go deleted file mode 100644 index 0f4216a0..00000000 --- a/server/internal/utils/uid.go +++ /dev/null @@ -1,98 +0,0 @@ -package utils - -import ( - "crypto/rand" - "fmt" - "math" -) - -const ( - defaultAlphabet = "_-0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ" // len=64 - defaultSize = 21 - defaultMaskSize = 5 -) - -// Generator function -type Generator func([]byte) (int, error) - -// BytesGenerator is the default bytes generator -var BytesGenerator Generator = rand.Read - -func initMasks(params ...int) []uint { - var size int - if len(params) == 0 { - size = defaultMaskSize - } else { - size = params[0] - } - masks := make([]uint, size) - for i := 0; i < size; i++ { - shift := 3 + i - masks[i] = (2 << uint(shift)) - 1 - } - return masks -} - -func getMask(alphabet string, masks []uint) int { - for i := 0; i < len(masks); i++ { - curr := int(masks[i]) - if curr >= len(alphabet)-1 { - return curr - } - } - return 0 -} - -// GenerateUID is a low-level function to change alphabet and ID size. -func GenerateUID(alphabet string, size int) (string, error) { - if len(alphabet) == 0 || len(alphabet) > 255 { - return "", fmt.Errorf("alphabet must not empty and contain no more than 255 chars. Current len is %d", len(alphabet)) - } - if size <= 0 { - return "", fmt.Errorf("size must be positive integer") - } - - masks := initMasks(size) - mask := getMask(alphabet, masks) - ceilArg := 1.6 * float64(mask*size) / float64(len(alphabet)) - step := int(math.Ceil(ceilArg)) - - id := make([]byte, size) - bytes := make([]byte, step) - for j := 0; ; { - _, err := BytesGenerator(bytes) - if err != nil { - return "", err - } - for i := 0; i < step; i++ { - currByte := bytes[i] & byte(mask) - if currByte < byte(len(alphabet)) { - id[j] = alphabet[currByte] - j++ - if j == size { - return string(id[:size]), nil - } - } - } - } -} - -// NewUID generates secure URL-friendly unique ID. -func NewUID(param ...int) (string, error) { - var size int - if len(param) == 0 { - size = defaultSize - } else { - size = param[0] - } - bytes := make([]byte, size) - _, err := BytesGenerator(bytes) - if err != nil { - return "", err - } - id := make([]byte, size) - for i := 0; i < size; i++ { - id[i] = defaultAlphabet[bytes[i]&63] - } - return string(id[:size]), nil -} diff --git a/internal/webrtc/cursor/image.go b/server/internal/webrtc/cursor/image.go similarity index 100% rename from internal/webrtc/cursor/image.go rename to server/internal/webrtc/cursor/image.go diff --git a/internal/webrtc/cursor/position.go b/server/internal/webrtc/cursor/position.go similarity index 100% rename from internal/webrtc/cursor/position.go rename to server/internal/webrtc/cursor/position.go diff --git a/internal/webrtc/handler.go b/server/internal/webrtc/handler.go similarity index 100% rename from internal/webrtc/handler.go rename to server/internal/webrtc/handler.go diff --git a/server/internal/webrtc/handle.go b/server/internal/webrtc/legacyhandler.go similarity index 68% rename from server/internal/webrtc/handle.go rename to server/internal/webrtc/legacyhandler.go index 89e92125..c085b3e6 100644 --- a/server/internal/webrtc/handle.go +++ b/server/internal/webrtc/legacyhandler.go @@ -5,7 +5,8 @@ import ( "encoding/binary" "strconv" - "github.com/pion/webrtc/v3" + "github.com/demodesk/neko/pkg/types" + "github.com/rs/zerolog" ) const ( @@ -38,12 +39,16 @@ type PayloadKey struct { Key uint64 // TODO: uint32 } -func (manager *WebRTCManager) handle(id string, msg webrtc.DataChannelMessage) error { - if (!manager.config.ImplicitControl && !manager.sessions.IsHost(id)) || (manager.config.ImplicitControl && !manager.sessions.CanControl(id)) { +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(msg.Data) + buffer := bytes.NewBuffer(data) header := &PayloadHeader{} hbytes := make([]byte, 3) @@ -55,7 +60,7 @@ func (manager *WebRTCManager) handle(id string, msg webrtc.DataChannelMessage) e return err } - buffer = bytes.NewBuffer(msg.Data) + buffer = bytes.NewBuffer(data) switch header.Event { case OP_MOVE: @@ -71,13 +76,13 @@ func (manager *WebRTCManager) handle(id string, msg webrtc.DataChannelMessage) e return err } - manager.logger. + logger. Debug(). 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)) + 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 { @@ -87,19 +92,19 @@ func (manager *WebRTCManager) handle(id string, msg webrtc.DataChannelMessage) e if payload.Key < 8 { err := manager.desktop.ButtonDown(uint32(payload.Key)) if err != nil { - manager.logger.Warn().Err(err).Msg("button down failed") + logger.Warn().Err(err).Msg("button down failed") return nil } - manager.logger.Debug().Msgf("button down %d", payload.Key) + logger.Debug().Msgf("button down %d", payload.Key) } else { err := manager.desktop.KeyDown(uint32(payload.Key)) if err != nil { - manager.logger.Warn().Err(err).Msg("key down failed") + logger.Warn().Err(err).Msg("key down failed") return nil } - manager.logger.Debug().Msgf("key down %d", payload.Key) + logger.Debug().Msgf("key down %d", payload.Key) } case OP_KEY_UP: payload := &PayloadKey{} @@ -111,19 +116,19 @@ func (manager *WebRTCManager) handle(id string, msg webrtc.DataChannelMessage) e if payload.Key < 8 { err := manager.desktop.ButtonUp(uint32(payload.Key)) if err != nil { - manager.logger.Warn().Err(err).Msg("button up failed") + logger.Warn().Err(err).Msg("button up failed") return nil } - manager.logger.Debug().Msgf("button up %d", payload.Key) + logger.Debug().Msgf("button up %d", payload.Key) } else { err := manager.desktop.KeyUp(uint32(payload.Key)) if err != nil { - manager.logger.Warn().Err(err).Msg("key up failed") + logger.Warn().Err(err).Msg("key up failed") return nil } - manager.logger.Debug().Msgf("key up %d", payload.Key) + logger.Debug().Msgf("key up %d", payload.Key) } case OP_KEY_CLK: // unused diff --git a/internal/webrtc/manager.go b/server/internal/webrtc/manager.go similarity index 97% rename from internal/webrtc/manager.go rename to server/internal/webrtc/manager.go index 5412c9e7..af4161a5 100644 --- a/internal/webrtc/manager.go +++ b/server/internal/webrtc/manager.go @@ -470,6 +470,21 @@ 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 + // + + // 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/internal/webrtc/metrics.go b/server/internal/webrtc/metrics.go similarity index 100% rename from internal/webrtc/metrics.go rename to server/internal/webrtc/metrics.go diff --git a/internal/webrtc/payload/receive.go b/server/internal/webrtc/payload/receive.go similarity index 100% rename from internal/webrtc/payload/receive.go rename to server/internal/webrtc/payload/receive.go diff --git a/internal/webrtc/payload/send.go b/server/internal/webrtc/payload/send.go similarity index 100% rename from internal/webrtc/payload/send.go rename to server/internal/webrtc/payload/send.go diff --git a/internal/webrtc/payload/types.go b/server/internal/webrtc/payload/types.go similarity index 100% rename from internal/webrtc/payload/types.go rename to server/internal/webrtc/payload/types.go diff --git a/server/internal/webrtc/peer.go b/server/internal/webrtc/peer.go index e109eba1..6abd4952 100644 --- a/server/internal/webrtc/peer.go +++ b/server/internal/webrtc/peer.go @@ -1,77 +1,543 @@ package webrtc import ( - "encoding/json" + "bytes" + "encoding/binary" "sync" + "time" + "github.com/pion/interceptor/pkg/cc" + "github.com/pion/rtcp" "github.com/pion/webrtc/v3" + "github.com/rs/zerolog" + + "github.com/demodesk/neko/internal/config" + "github.com/demodesk/neko/internal/webrtc/payload" + "github.com/demodesk/neko/pkg/types" + "github.com/demodesk/neko/pkg/types/event" + "github.com/demodesk/neko/pkg/utils" ) -type Peer struct { - id string +type WebRTCPeerCtx struct { mu sync.Mutex - manager *WebRTCManager + logger zerolog.Logger + session types.Session + metrics *metrics connection *webrtc.PeerConnection + // bandwidth estimator + estimator cc.BandwidthEstimator + estimateTrend *utils.TrendDetector + // stream selectors + video types.StreamSelectorManager + audio types.StreamSinkManager + // tracks & channels + audioTrack *Track + videoTrack *Track + dataChannel *webrtc.DataChannel + rtcpChannel chan []rtcp.Packet + // config + iceTrickle bool + estimatorConfig config.WebRTCEstimator + paused bool + videoAuto bool + videoDisabled bool + audioDisabled bool } -func (peer *Peer) CreateOffer() (string, error) { - desc, err := peer.connection.CreateOffer(nil) +// +// connection +// + +func (peer *WebRTCPeerCtx) CreateOffer(ICERestart bool) (*webrtc.SessionDescription, error) { + peer.mu.Lock() + defer peer.mu.Unlock() + + offer, err := peer.connection.CreateOffer(&webrtc.OfferOptions{ + ICERestart: ICERestart, + }) if err != nil { - return "", err + return nil, err } - err = peer.connection.SetLocalDescription(desc) - if err != nil { - return "", err - } - - return desc.SDP, nil + return peer.setLocalDescription(offer) } -func (peer *Peer) CreateAnswer() (string, error) { - desc, err := peer.connection.CreateAnswer(nil) +func (peer *WebRTCPeerCtx) CreateAnswer() (*webrtc.SessionDescription, error) { + peer.mu.Lock() + defer peer.mu.Unlock() + + answer, err := peer.connection.CreateAnswer(nil) if err != nil { - return "", err + return nil, err } - err = peer.connection.SetLocalDescription(desc) - if err != nil { - return "", nil + return peer.setLocalDescription(answer) +} + +func (peer *WebRTCPeerCtx) setLocalDescription(description webrtc.SessionDescription) (*webrtc.SessionDescription, error) { + if !peer.iceTrickle { + // Create channel that is blocked until ICE Gathering is complete + gatherComplete := webrtc.GatheringCompletePromise(peer.connection) + + if err := peer.connection.SetLocalDescription(description); err != nil { + return nil, err + } + + <-gatherComplete + } else { + if err := peer.connection.SetLocalDescription(description); err != nil { + return nil, err + } } - return desc.SDP, nil + return peer.connection.LocalDescription(), nil } -func (peer *Peer) SetOffer(sdp string) error { - return peer.connection.SetRemoteDescription(webrtc.SessionDescription{SDP: sdp, Type: webrtc.SDPTypeOffer}) +func (peer *WebRTCPeerCtx) SetRemoteDescription(desc webrtc.SessionDescription) error { + peer.mu.Lock() + defer peer.mu.Unlock() + + return peer.connection.SetRemoteDescription(desc) } -func (peer *Peer) SetAnswer(sdp string) error { - return peer.connection.SetRemoteDescription(webrtc.SessionDescription{SDP: sdp, Type: webrtc.SDPTypeAnswer}) -} - -func (peer *Peer) SetCandidate(candidateString string) error { - var candidate webrtc.ICECandidateInit - err := json.Unmarshal([]byte(candidateString), &candidate) - if err != nil { - return err - } +func (peer *WebRTCPeerCtx) SetCandidate(candidate webrtc.ICECandidateInit) error { + peer.mu.Lock() + defer peer.mu.Unlock() return peer.connection.AddICECandidate(candidate) } -func (peer *Peer) WriteData(v interface{}) error { +// TODO: Add shutdown function? +func (peer *WebRTCPeerCtx) Destroy() { peer.mu.Lock() defer peer.mu.Unlock() + + var err error + + // if peer connection is not closed, close it + if peer.connection.ConnectionState() != webrtc.PeerConnectionStateClosed { + err = peer.connection.Close() + } + + peer.logger.Err(err).Msg("peer connection destroyed") +} + +func (peer *WebRTCPeerCtx) estimatorReader() { + conf := peer.estimatorConfig + + // if estimator is not in debug mode, use a nop logger + var debugLogger zerolog.Logger + if conf.Debug { + debugLogger = peer.logger.With().Str("component", "estimator").Logger().Level(zerolog.DebugLevel) + } else { + debugLogger = zerolog.Nop() + } + + // if estimator is disabled, do nothing + if peer.estimator == nil { + return + } + + // use a ticker to get current client target bitrate + ticker := time.NewTicker(conf.ReadInterval) + defer ticker.Stop() + + // since when is the estimate stable/unstable + stableSince := time.Now() // we asume stable at start + unstableSince := time.Time{} + // since when are we neutral but cannot accomodate current bitrate + // we migt be stalled or estimator just reached zer (very bad connection) + stalledSince := time.Time{} + // when was the last upgrade/downgrade + lastUpgradeTime := time.Time{} + lastDowngradeTime := time.Time{} + + for range ticker.C { + targetBitrate := peer.estimator.GetTargetBitrate() + peer.metrics.SetReceiverEstimatedTargetBitrate(float64(targetBitrate)) + + // if peer connection is closed, stop reading + if peer.connection.ConnectionState() == webrtc.PeerConnectionStateClosed { + break + } + + // if estimation or video is disabled, do nothing + if !peer.videoAuto || peer.videoDisabled || peer.paused || conf.Passive { + continue + } + + // get trend direction to decide if we should upgrade or downgrade + peer.estimateTrend.AddValue(int64(targetBitrate)) + direction := peer.estimateTrend.GetDirection() + + // get current stream bitrate + stream, ok := peer.videoTrack.Stream() + if !ok { + debugLogger.Warn().Msg("looks like we don't have a stream yet, skipping bitrate estimation") + continue + } + + // if stream bitrate is 0, we need to wait for some time until we get a valid value + streamId, streamBitrate := stream.ID(), stream.Bitrate() + if streamBitrate == 0 { + debugLogger.Warn().Msg("looks like stream bitrate is 0, we need to wait for some time") + continue + } + + // check whats the difference between target and stream bitrate + diff := float64(targetBitrate) / float64(streamBitrate) + + debugLogger.Info(). + Float64("diff", diff). + Int("target_bitrate", targetBitrate). + Uint64("stream_bitrate", streamBitrate). + Str("direction", direction.String()). + Msg("got bitrate from estimator") + + // if we can accomodate current stream or we are not netural anymore, + // we are not stalled so we reset the stalled time + if direction != utils.TrendDirectionNeutral || diff > 1+conf.DiffThreshold { + stalledSince = time.Now() + } + + // if we are neutral and stalled for too long, we might be congesting + stalled := direction == utils.TrendDirectionNeutral && time.Since(stalledSince) > conf.StalledDuration + if stalled { + debugLogger.Warn(). + Time("stalled_since", stalledSince). + Msgf("it looks like we are stalled") + } + + // if we have an downward trend or are stalled, we might be congesting + if direction == utils.TrendDirectionDownward || stalled { + // we reset the stable time because we are congesting + stableSince = time.Now() + + // if we downgraded recently, we wait for some more time + if time.Since(lastDowngradeTime) < conf.DowngradeBackoff { + debugLogger.Debug(). + Time("last_downgrade", lastDowngradeTime). + Msgf("downgraded recently, waiting for at least %v", conf.DowngradeBackoff) + continue + } + + // if we are not unstable but we fluctuate we should wait for some more time + if time.Since(unstableSince) < conf.UnstableDuration { + debugLogger.Debug(). + Time("unstable_since", unstableSince). + Msgf("we are not unstable long enough, waiting for at least %v", conf.UnstableDuration) + continue + } + + // if we still have a big difference between target and stream bitrate, we wait for some more time + if conf.DiffThreshold >= 0 && diff > 1+conf.DiffThreshold { + debugLogger.Debug(). + Float64("diff", diff). + Float64("threshold", conf.DiffThreshold). + Msgf("we still have a big difference between target and stream bitrate, " + + "therefore we still should be able to accomodate current stream") + continue + } + + err := peer.SetVideo(types.PeerVideoRequest{ + Selector: &types.StreamSelector{ + ID: streamId, + Type: types.StreamSelectorTypeLower, + }, + }) + if err != nil && err != types.ErrWebRTCStreamNotFound { + peer.logger.Warn().Err(err).Msg("failed to downgrade video stream") + } + lastDowngradeTime = time.Now() + + if err == types.ErrWebRTCStreamNotFound { + debugLogger.Info().Msg("looks like we are already on the lowest stream") + } else { + debugLogger.Info().Msg("downgraded video stream") + } + continue + } + + // we reset the unstable time because we are not congesting + unstableSince = time.Now() + + // if we have a neutral or upward trend, that means our estimate is stable + // if we are on the highest stream, we don't need to do anything + // but if there is a higher stream, we should try to upgrade and see if it works + + // if we upgraded recently, we wait for some more time + if time.Since(lastUpgradeTime) < conf.UpgradeBackoff { + debugLogger.Debug(). + Time("last_upgrade", lastUpgradeTime). + Msgf("upgraded recently, waiting for at least %v", conf.UpgradeBackoff) + continue + } + + // if we are not stable for long enough, we wait for some more time + // because bandwidth estimation might fluctuate + if time.Since(stableSince) < conf.StableDuration { + debugLogger.Debug(). + Time("stable_since", stableSince). + Msgf("we are not stable long enough, waiting for at least %v", conf.StableDuration) + continue + } + + // upgrade only if estimated bitrate passed the threshold + if conf.DiffThreshold >= 0 && diff < 1+conf.DiffThreshold { + debugLogger.Debug(). + Float64("diff", diff). + Float64("threshold", conf.DiffThreshold). + Msgf("looks like we don't have enough bitrate to accomodate higher stream, " + + "therefore we should wait for some more time") + continue + } + + err := peer.SetVideo(types.PeerVideoRequest{ + Selector: &types.StreamSelector{ + ID: streamId, + Type: types.StreamSelectorTypeHigher, + }, + }) + if err != nil && err != types.ErrWebRTCStreamNotFound { + peer.logger.Warn().Err(err).Msg("failed to upgrade video stream") + } + lastUpgradeTime = time.Now() + + if err == types.ErrWebRTCStreamNotFound { + debugLogger.Info().Msg("looks like we are already on the highest stream") + } else { + debugLogger.Info().Msg("upgraded video stream") + } + } +} + +func (peer *WebRTCPeerCtx) SetPaused(isPaused bool) error { + peer.mu.Lock() + defer peer.mu.Unlock() + + peer.videoTrack.SetPaused(isPaused || peer.videoDisabled) + peer.audioTrack.SetPaused(isPaused || peer.audioDisabled) + + peer.logger.Info().Bool("is_paused", isPaused).Msg("set paused") + peer.paused = isPaused + return nil } -func (peer *Peer) Destroy() error { - if peer.connection != nil && peer.connection.ConnectionState() != webrtc.PeerConnectionStateClosed { - if err := peer.connection.Close(); err != nil { +func (peer *WebRTCPeerCtx) Paused() bool { + peer.mu.Lock() + defer peer.mu.Unlock() + + return peer.paused +} + +// +// video +// + +func (peer *WebRTCPeerCtx) SetVideo(r types.PeerVideoRequest) error { + peer.mu.Lock() + defer peer.mu.Unlock() + + modified := false + + // video disabled + if r.Disabled != nil { + disabled := *r.Disabled + + // update only if changed + if peer.videoDisabled != disabled { + peer.videoDisabled = disabled + peer.videoTrack.SetPaused(disabled || peer.paused) + + peer.logger.Info().Bool("disabled", disabled).Msg("set video disabled") + modified = true + } + } + + // video selector + if r.Selector != nil { + selector := *r.Selector + + // get requested video stream from selector + stream, ok := peer.video.GetStream(selector) + if !ok { + return types.ErrWebRTCStreamNotFound + } + + // set video stream to track + changed, err := peer.videoTrack.SetStream(stream) + if err != nil { return err } + + // update only if stream changed + if changed { + videoID := stream.ID() + peer.metrics.SetVideoID(videoID) + + peer.logger.Info().Str("video_id", videoID).Msg("set video") + modified = true + } + } + + // video auto + if r.Auto != nil { + videoAuto := *r.Auto + + if peer.estimator == nil || peer.estimatorConfig.Passive { + peer.logger.Warn().Msg("estimator is disabled or in passive mode, cannot change video auto") + videoAuto = false // ensure video auto is disabled + } + + // update only if video auto changed + if peer.videoAuto != videoAuto { + peer.videoAuto = videoAuto + + peer.logger.Info().Bool("video_auto", videoAuto).Msg("set video auto") + modified = true + } + } + + // send video signal if modified + if modified { + go func() { + // in goroutine because of mutex and we don't want to block + peer.session.Send(event.SIGNAL_VIDEO, peer.Video()) + }() } return nil } + +func (peer *WebRTCPeerCtx) Video() types.PeerVideo { + peer.mu.Lock() + defer peer.mu.Unlock() + + // get current video stream ID + ID := "" + stream, ok := peer.videoTrack.Stream() + if ok { + ID = stream.ID() + } + + return types.PeerVideo{ + Disabled: peer.videoDisabled, + ID: ID, + Video: ID, // TODO: Remove, used for backward compatibility + Auto: peer.videoAuto, + } +} + +// +// audio +// + +func (peer *WebRTCPeerCtx) SetAudio(r types.PeerAudioRequest) error { + peer.mu.Lock() + defer peer.mu.Unlock() + + modified := false + + // audio disabled + if r.Disabled != nil { + disabled := *r.Disabled + + // update only if changed + if peer.audioDisabled != disabled { + peer.audioDisabled = disabled + peer.audioTrack.SetPaused(disabled || peer.paused) + + peer.logger.Info().Bool("disabled", disabled).Msg("set audio disabled") + modified = true + } + } + + // send video signal if modified + if modified { + go func() { + // in goroutine because of mutex and we don't want to block + peer.session.Send(event.SIGNAL_AUDIO, peer.Audio()) + }() + } + + return nil +} + +func (peer *WebRTCPeerCtx) Audio() types.PeerAudio { + peer.mu.Lock() + defer peer.mu.Unlock() + + return types.PeerAudio{ + Disabled: peer.audioDisabled, + } +} + +// +// data channel +// + +func (peer *WebRTCPeerCtx) SendCursorPosition(x, y int) error { + peer.mu.Lock() + defer peer.mu.Unlock() + + // do not send cursor position to host + if peer.session.IsHost() { + return nil + } + + header := payload.Header{ + Event: payload.OP_CURSOR_POSITION, + Length: 7, + } + + data := payload.CursorPosition{ + X: uint16(x), + Y: uint16(y), + } + + buffer := &bytes.Buffer{} + + if err := binary.Write(buffer, binary.BigEndian, header); err != nil { + return err + } + + if err := binary.Write(buffer, binary.BigEndian, data); err != nil { + return err + } + + return peer.dataChannel.Send(buffer.Bytes()) +} + +func (peer *WebRTCPeerCtx) SendCursorImage(cur *types.CursorImage, img []byte) error { + peer.mu.Lock() + defer peer.mu.Unlock() + + header := payload.Header{ + Event: payload.OP_CURSOR_IMAGE, + Length: uint16(11 + len(img)), + } + + data := payload.CursorImage{ + Width: cur.Width, + Height: cur.Height, + Xhot: cur.Xhot, + Yhot: cur.Yhot, + } + + buffer := &bytes.Buffer{} + + if err := binary.Write(buffer, binary.BigEndian, header); err != nil { + return err + } + + if err := binary.Write(buffer, binary.BigEndian, data); err != nil { + return err + } + + if err := binary.Write(buffer, binary.BigEndian, img); err != nil { + return err + } + + return peer.dataChannel.Send(buffer.Bytes()) +} diff --git a/internal/webrtc/track.go b/server/internal/webrtc/track.go similarity index 100% rename from internal/webrtc/track.go rename to server/internal/webrtc/track.go diff --git a/server/internal/webrtc/webrtc.go b/server/internal/webrtc/webrtc.go deleted file mode 100644 index fe278cae..00000000 --- a/server/internal/webrtc/webrtc.go +++ /dev/null @@ -1,345 +0,0 @@ -package webrtc - -import ( - "encoding/json" - "errors" - "fmt" - "io" - "net" - "strings" - "time" - - "github.com/pion/ice/v2" - "github.com/pion/interceptor" - "github.com/pion/webrtc/v3" - "github.com/pion/webrtc/v3/pkg/media" - "github.com/rs/zerolog" - "github.com/rs/zerolog/log" - - "m1k1o/neko/internal/config" - "m1k1o/neko/internal/types" - "m1k1o/neko/internal/webrtc/pionlog" -) - -func New(sessions types.SessionManager, capture types.CaptureManager, desktop types.DesktopManager, config *config.WebRTC) *WebRTCManager { - return &WebRTCManager{ - logger: log.With().Str("module", "webrtc").Logger(), - capture: capture, - desktop: desktop, - sessions: sessions, - config: config, - } -} - -type WebRTCManager struct { - logger zerolog.Logger - videoTrack *webrtc.TrackLocalStaticSample - audioTrack *webrtc.TrackLocalStaticSample - sessions types.SessionManager - capture types.CaptureManager - desktop types.DesktopManager - config *config.WebRTC - api *webrtc.API -} - -func (manager *WebRTCManager) Start() { - var err error - - // - // audio - // - - audioCodec := manager.capture.Audio().Codec() - manager.audioTrack, err = webrtc.NewTrackLocalStaticSample(audioCodec.Capability, "audio", "stream") - if err != nil { - manager.logger.Panic().Err(err).Msg("unable to create audio track") - } - - go func() { - for { - sample, ok := <-manager.capture.Audio().GetSampleChannel() - if !ok { - manager.logger.Debug().Msg("audio capture channel is closed") - continue - } - - err := manager.audioTrack.WriteSample(media.Sample(sample)) - if err != nil && errors.Is(err, io.ErrClosedPipe) { - manager.logger.Warn().Err(err).Msg("audio pipeline failed to write") - } - } - }() - - // - // video - // - - videoCodec := manager.capture.Video().Codec() - manager.videoTrack, err = webrtc.NewTrackLocalStaticSample(videoCodec.Capability, "video", "stream") - if err != nil { - manager.logger.Panic().Err(err).Msg("unable to create video track") - } - - go func() { - for { - sample, ok := <-manager.capture.Video().GetSampleChannel() - if !ok { - manager.logger.Debug().Msg("video capture channel is closed") - continue - } - - err := manager.videoTrack.WriteSample(media.Sample(sample)) - if err != nil && errors.Is(err, io.ErrClosedPipe) { - manager.logger.Warn().Err(err).Msg("video pipeline failed to write") - } - } - }() - - // - // api - // - - if err := manager.initAPI(); err != nil { - manager.logger.Panic().Err(err).Msg("failed to initialize webrtc API") - } - - manager.logger.Info(). - Str("ice_lite", fmt.Sprintf("%t", manager.config.ICELite)). - Str("ice_servers", fmt.Sprintf("%+v", manager.config.ICEServers)). - Str("ephemeral_port_range", fmt.Sprintf("%d-%d", manager.config.EphemeralMin, manager.config.EphemeralMax)). - Str("nat_ips", strings.Join(manager.config.NAT1To1IPs, ",")). - Msgf("webrtc starting") -} - -func (manager *WebRTCManager) Shutdown() error { - manager.logger.Info().Msgf("webrtc shutting down") - return nil -} - -func (manager *WebRTCManager) initAPI() error { - logger := pionlog.New(manager.logger) - - settings := webrtc.SettingEngine{ - LoggerFactory: logger, - } - - settings.SetNAT1To1IPs(manager.config.NAT1To1IPs, webrtc.ICECandidateTypeHost) - settings.SetICETimeouts(6*time.Second, 6*time.Second, 3*time.Second) - settings.SetSRTPReplayProtectionWindow(512) - settings.SetLite(manager.config.ICELite) - - var networkType []webrtc.NetworkType - - // Add TCP Mux - if manager.config.TCPMUX > 0 { - tcpListener, err := net.ListenTCP("tcp", &net.TCPAddr{ - IP: net.IP{0, 0, 0, 0}, - Port: manager.config.TCPMUX, - }) - - if err != nil { - return err - } - - tcpMux := ice.NewTCPMuxDefault(ice.TCPMuxParams{ - Listener: tcpListener, - Logger: logger.NewLogger("ice-tcp"), - ReadBufferSize: 32, // receiving channel size - WriteBufferSize: 4 * 1024 * 1024, // write buffer size, 4MB - }) - settings.SetICETCPMux(tcpMux) - - networkType = append(networkType, webrtc.NetworkTypeTCP4) - manager.logger.Info().Str("listener", tcpListener.Addr().String()).Msg("using TCP MUX") - } - - // Add UDP Mux - if manager.config.UDPMUX > 0 { - udpMux, err := ice.NewMultiUDPMuxFromPort(manager.config.UDPMUX, - ice.UDPMuxFromPortWithLogger(logger.NewLogger("ice-udp")), - ) - - if err != nil { - return err - } - - settings.SetICEUDPMux(udpMux) - - networkType = append(networkType, webrtc.NetworkTypeUDP4) - manager.logger.Info().Int("port", manager.config.UDPMUX).Msg("using UDP MUX") - } else if manager.config.EphemeralMax != 0 { - _ = settings.SetEphemeralUDPPortRange(manager.config.EphemeralMin, manager.config.EphemeralMax) - networkType = append(networkType, - webrtc.NetworkTypeUDP4, - webrtc.NetworkTypeUDP6, - ) - } - - settings.SetNetworkTypes(networkType) - - // Create MediaEngine with selected codecs - engine := webrtc.MediaEngine{} - manager.capture.Audio().Codec().Register(&engine) - manager.capture.Video().Codec().Register(&engine) - - // Register Interceptors - i := &interceptor.Registry{} - if err := webrtc.RegisterDefaultInterceptors(&engine, i); err != nil { - return err - } - - // Create API with MediaEngine and SettingEngine - manager.api = webrtc.NewAPI( - webrtc.WithMediaEngine(&engine), - webrtc.WithSettingEngine(settings), - webrtc.WithInterceptorRegistry(i), - ) - - return nil -} - -func (manager *WebRTCManager) CreatePeer(id string, session types.Session) (types.Peer, error) { - configuration := webrtc.Configuration{ - SDPSemantics: webrtc.SDPSemanticsUnifiedPlanWithFallback, - } - - if !manager.config.ICELite { - configuration.ICEServers = manager.config.ICEServers - } - - // Create new peer connection - connection, err := manager.api.NewPeerConnection(configuration) - if err != nil { - return nil, err - } - - negotiated := true - _, err = connection.CreateDataChannel("data", &webrtc.DataChannelInit{ - Negotiated: &negotiated, - }) - if err != nil { - return nil, err - } - - connection.OnDataChannel(func(d *webrtc.DataChannel) { - d.OnMessage(func(msg webrtc.DataChannelMessage) { - if err = manager.handle(id, msg); err != nil { - manager.logger.Warn().Err(err).Msg("data handle failed") - } - }) - }) - - // Set the handler for ICE connection state - // This will notify you when the peer has connected/disconnected - connection.OnICEConnectionStateChange(func(connectionState webrtc.ICEConnectionState) { - manager.logger.Info(). - Str("connection_state", connectionState.String()). - Msg("connection state has changed") - }) - - rtpVideo, err := connection.AddTrack(manager.videoTrack) - if err != nil { - return nil, err - } - - rtpAudio, err := connection.AddTrack(manager.audioTrack) - if err != nil { - return nil, err - } - - connection.OnConnectionStateChange(func(state webrtc.PeerConnectionState) { - switch state { - case webrtc.PeerConnectionStateDisconnected: - manager.logger.Info().Str("id", id).Msg("peer disconnected") - manager.sessions.Destroy(id) - case webrtc.PeerConnectionStateFailed: - manager.logger.Warn().Str("id", id).Msg("peer failed") - manager.sessions.Destroy(id) - case webrtc.PeerConnectionStateClosed: - manager.logger.Info().Str("id", id).Msg("peer closed") - manager.sessions.Destroy(id) - case webrtc.PeerConnectionStateConnected: - manager.logger.Info().Str("id", id).Msg("peer connected") - if err = session.SetConnected(true); err != nil { - manager.logger.Warn().Err(err).Msg("unable to set connected on peer") - manager.sessions.Destroy(id) - } - } - }) - - peer := &Peer{ - id: id, - manager: manager, - connection: connection, - } - - connection.OnNegotiationNeeded(func() { - manager.logger.Warn().Msg("negotiation is needed") - - sdp, err := peer.CreateOffer() - if err != nil { - manager.logger.Err(err).Msg("creating offer failed") - return - } - - err = session.SignalLocalOffer(sdp) - if err != nil { - manager.logger.Warn().Err(err).Msg("sending SignalLocalOffer failed") - return - } - }) - - connection.OnICECandidate(func(i *webrtc.ICECandidate) { - if i == nil { - manager.logger.Info().Msg("sent all ICECandidates") - return - } - - candidateString, err := json.Marshal(i.ToJSON()) - if err != nil { - manager.logger.Warn().Err(err).Msg("converting ICECandidate to json failed") - return - } - - if err := session.SignalLocalCandidate(string(candidateString)); err != nil { - manager.logger.Warn().Err(err).Msg("sending SignalCandidate failed") - return - } - }) - - if err := session.SetPeer(peer); err != nil { - return nil, err - } - - go func() { - rtcpBuf := make([]byte, 1500) - for { - if _, _, rtcpErr := rtpVideo.Read(rtcpBuf); rtcpErr != nil { - return - } - } - }() - - go func() { - rtcpBuf := make([]byte, 1500) - for { - if _, _, rtcpErr := rtpAudio.Read(rtcpBuf); rtcpErr != nil { - return - } - } - }() - - return peer, nil -} - -func (manager *WebRTCManager) ICELite() bool { - return manager.config.ICELite -} - -func (manager *WebRTCManager) ICEServers() []webrtc.ICEServer { - return manager.config.ICEServers -} - -func (manager *WebRTCManager) ImplicitControl() bool { - return manager.config.ImplicitControl -} diff --git a/internal/websocket/filechooserdialog.go b/server/internal/websocket/filechooserdialog.go similarity index 100% rename from internal/websocket/filechooserdialog.go rename to server/internal/websocket/filechooserdialog.go diff --git a/server/internal/websocket/handler/admin.go b/server/internal/websocket/handler/admin.go deleted file mode 100644 index 45c803ab..00000000 --- a/server/internal/websocket/handler/admin.go +++ /dev/null @@ -1,325 +0,0 @@ -package handler - -import ( - "strings" - - "m1k1o/neko/internal/types" - "m1k1o/neko/internal/types/event" - "m1k1o/neko/internal/types/message" -) - -func (h *MessageHandler) adminLock(id string, session types.Session, payload *message.AdminLock) error { - if !session.Admin() { - h.logger.Debug().Msg("user not admin") - return nil - } - - if h.state.IsLocked(payload.Resource) { - h.logger.Debug().Str("resource", payload.Resource).Msg("resource already locked...") - return nil - } - - // allow only known resources - switch payload.Resource { - case "login": - case "control": - case "file_transfer": - default: - h.logger.Debug().Msg("unknown lock resource") - return nil - } - - // TODO: Handle locks in sessions as flags. - if payload.Resource == "control" { - h.sessions.SetControlLocked(true) - } - - h.state.Lock(payload.Resource, id) - - if err := h.sessions.Broadcast( - message.AdminLock{ - Event: event.ADMIN_LOCK, - ID: id, - Resource: payload.Resource, - }, nil); err != nil { - h.logger.Warn().Err(err).Msgf("broadcasting event %s has failed", event.ADMIN_LOCK) - return err - } - - return nil -} - -func (h *MessageHandler) adminUnlock(id string, session types.Session, payload *message.AdminLock) error { - if !session.Admin() { - h.logger.Debug().Msg("user not admin") - return nil - } - - if !h.state.IsLocked(payload.Resource) { - h.logger.Debug().Str("resource", payload.Resource).Msg("resource not locked...") - return nil - } - - // TODO: Handle locks in sessions as flags. - if payload.Resource == "control" { - h.sessions.SetControlLocked(false) - } - - h.state.Unlock(payload.Resource) - - if err := h.sessions.Broadcast( - message.AdminLock{ - Event: event.ADMIN_UNLOCK, - ID: id, - Resource: payload.Resource, - }, nil); err != nil { - h.logger.Warn().Err(err).Msgf("broadcasting event %s has failed", event.ADMIN_UNLOCK) - return err - } - - return nil -} - -func (h *MessageHandler) adminControl(id string, session types.Session) error { - if !session.Admin() { - h.logger.Debug().Msg("user not admin") - return nil - } - - host, ok := h.sessions.GetHost() - - err := h.sessions.SetHost(id) - if err != nil { - return err - } - - if ok { - if err := h.sessions.Broadcast( - message.AdminTarget{ - Event: event.ADMIN_CONTROL, - ID: id, - Target: host.ID(), - }, nil); err != nil { - h.logger.Warn().Err(err).Msgf("broadcasting event %s has failed", event.ADMIN_CONTROL) - return err - } - } else { - if err := h.sessions.Broadcast( - message.Admin{ - Event: event.ADMIN_CONTROL, - ID: id, - }, nil); err != nil { - h.logger.Warn().Err(err).Msgf("broadcasting event %s has failed", event.ADMIN_CONTROL) - return err - } - } - - return nil -} - -func (h *MessageHandler) AdminRelease(id string, session types.Session) error { - if !session.Admin() { - h.logger.Debug().Msg("user not admin") - return nil - } - - host, ok := h.sessions.GetHost() - - h.sessions.ClearHost() - - if ok { - if err := h.sessions.Broadcast( - message.AdminTarget{ - Event: event.ADMIN_RELEASE, - ID: id, - Target: host.ID(), - }, nil); err != nil { - h.logger.Warn().Err(err).Msgf("broadcasting event %s has failed", event.ADMIN_RELEASE) - return err - } - } else { - if err := h.sessions.Broadcast( - message.Admin{ - Event: event.ADMIN_RELEASE, - ID: id, - }, nil); err != nil { - h.logger.Warn().Err(err).Msgf("broadcasting event %s has failed", event.ADMIN_RELEASE) - return err - } - } - - return nil -} - -func (h *MessageHandler) adminGive(id string, session types.Session, payload *message.Admin) error { - if !session.Admin() { - h.logger.Debug().Msg("user not admin") - return nil - } - - if !h.sessions.Has(payload.ID) { - h.logger.Debug().Str("id", payload.ID).Msg("user does not exist") - return nil - } - - // set host - err := h.sessions.SetHost(payload.ID) - if err != nil { - return err - } - - // let everyone know - if err := h.sessions.Broadcast( - message.AdminTarget{ - Event: event.CONTROL_GIVE, - ID: id, - Target: payload.ID, - }, nil); err != nil { - h.logger.Warn().Err(err).Msgf("broadcasting event %s has failed", event.CONTROL_GIVE) - return err - } - - return nil -} - -func (h *MessageHandler) adminMute(id string, session types.Session, payload *message.Admin) error { - if !session.Admin() { - h.logger.Debug().Msg("user not admin") - return nil - } - - target, ok := h.sessions.Get(payload.ID) - if !ok { - h.logger.Debug().Str("id", payload.ID).Msg("can't find session id") - return nil - } - - if target.Admin() { - h.logger.Debug().Msg("target is an admin, baling") - return nil - } - - target.SetMuted(true) - - if err := h.sessions.Broadcast( - message.AdminTarget{ - Event: event.ADMIN_MUTE, - Target: target.ID(), - ID: id, - }, nil); err != nil { - h.logger.Warn().Err(err).Msgf("broadcasting event %s has failed", event.ADMIN_MUTE) - return err - } - - return nil -} - -func (h *MessageHandler) adminUnmute(id string, session types.Session, payload *message.Admin) error { - if !session.Admin() { - h.logger.Debug().Msg("user not admin") - return nil - } - - target, ok := h.sessions.Get(payload.ID) - if !ok { - h.logger.Debug().Str("id", payload.ID).Msg("can't find target session") - return nil - } - - target.SetMuted(false) - - if err := h.sessions.Broadcast( - message.AdminTarget{ - Event: event.ADMIN_UNMUTE, - Target: target.ID(), - ID: id, - }, nil); err != nil { - h.logger.Warn().Err(err).Msgf("broadcasting event %s has failed", event.ADMIN_UNMUTE) - return err - } - - return nil -} - -func (h *MessageHandler) adminKick(id string, session types.Session, payload *message.Admin) error { - if !session.Admin() { - h.logger.Debug().Msg("user not admin") - return nil - } - - target, ok := h.sessions.Get(payload.ID) - if !ok { - h.logger.Debug().Str("id", payload.ID).Msg("can't find session id") - return nil - } - - if target.Admin() { - h.logger.Debug().Msg("target is an admin, baling") - return nil - } - - if err := target.Kick("kicked"); err != nil { - return err - } - - if err := h.sessions.Broadcast( - message.AdminTarget{ - Event: event.ADMIN_KICK, - Target: target.ID(), - ID: id, - }, []string{payload.ID}); err != nil { - h.logger.Warn().Err(err).Msgf("broadcasting event %s has failed", event.ADMIN_KICK) - return err - } - - return nil -} - -func (h *MessageHandler) adminBan(id string, session types.Session, payload *message.Admin) error { - if !session.Admin() { - h.logger.Debug().Msg("user not admin") - return nil - } - - target, ok := h.sessions.Get(payload.ID) - if !ok { - h.logger.Debug().Str("id", payload.ID).Msg("can't find session id") - return nil - } - - if target.Admin() { - h.logger.Debug().Msg("target is an admin, baling") - return nil - } - - remote := target.Address() - if remote == "" { - h.logger.Debug().Msg("no remote address, baling") - return nil - } - - address := strings.SplitN(remote, ":", -1) - if len(address[0]) < 1 { - h.logger.Debug().Str("address", remote).Msg("no remote address, baling") - return nil - } - - h.logger.Debug().Str("address", remote).Msg("adding address to banned") - h.state.Ban(address[0], id) - - if err := target.Kick("banned"); err != nil { - return err - } - - if err := h.sessions.Broadcast( - message.AdminTarget{ - Event: event.ADMIN_BAN, - Target: target.ID(), - ID: id, - }, []string{payload.ID}); err != nil { - h.logger.Warn().Err(err).Msgf("broadcasting event %s has failed", event.ADMIN_BAN) - return err - } - - return nil -} diff --git a/server/internal/websocket/handler/broadcast.go b/server/internal/websocket/handler/broadcast.go deleted file mode 100644 index 405bfdac..00000000 --- a/server/internal/websocket/handler/broadcast.go +++ /dev/null @@ -1,110 +0,0 @@ -package handler - -import ( - "m1k1o/neko/internal/types" - "m1k1o/neko/internal/types/event" - "m1k1o/neko/internal/types/message" -) - -func (h *MessageHandler) broadcastCreate(session types.Session, payload *message.BroadcastCreate) error { - broadcast := h.capture.Broadcast() - - if !session.Admin() { - h.logger.Debug().Msg("user not admin") - return nil - } - - if payload.URL == "" { - return session.Send( - message.SystemMessage{ - Event: event.SYSTEM_ERROR, - Title: "Error while starting broadcast", - Message: "missing broadcast URL", - }) - } - - if broadcast.Started() { - return session.Send( - message.SystemMessage{ - Event: event.SYSTEM_ERROR, - Title: "Error while starting broadcast", - Message: "server is already broadcasting", - }) - } - - if err := broadcast.Start(payload.URL); err != nil { - if err := session.Send( - message.SystemMessage{ - Event: event.SYSTEM_ERROR, - Title: "Error while starting broadcast", - Message: err.Error(), - }); err != nil { - h.logger.Warn().Err(err).Msgf("sending event %s has failed", event.SYSTEM_ERROR) - return err - } - } - - if err := h.broadcastStatus(nil); err != nil { - return err - } - - return nil -} - -func (h *MessageHandler) broadcastDestroy(session types.Session) error { - broadcast := h.capture.Broadcast() - - if !session.Admin() { - h.logger.Debug().Msg("user not admin") - return nil - } - - if !broadcast.Started() { - return session.Send( - message.SystemMessage{ - Event: event.SYSTEM_ERROR, - Title: "Error while stopping broadcast", - Message: "server is not broadcasting", - }) - } - - broadcast.Stop() - - if err := h.broadcastStatus(nil); err != nil { - return err - } - - return nil -} - -func (h *MessageHandler) broadcastStatus(session types.Session) error { - broadcast := h.capture.Broadcast() - - msg := message.BroadcastStatus{ - Event: event.BROADCAST_STATUS, - IsActive: broadcast.Started(), - URL: broadcast.Url(), - } - - // if no session, broadcast change - if session == nil { - if err := h.sessions.AdminBroadcast(msg, nil); err != nil { - h.logger.Warn().Err(err).Msgf("broadcasting event %s has failed", event.BROADCAST_STATUS) - return err - } - - return nil - } - - if !session.Admin() { - h.logger.Debug().Msg("user not admin") - return nil - } - - if err := session.Send(msg); err != nil { - h.logger.Warn().Err(err).Msgf("sending event %s has failed", event.BROADCAST_STATUS) - return err - } - - return nil -} diff --git a/server/internal/websocket/handler/chat.go b/server/internal/websocket/handler/chat.go deleted file mode 100644 index 5a447b75..00000000 --- a/server/internal/websocket/handler/chat.go +++ /dev/null @@ -1,41 +0,0 @@ -package handler - -import ( - "m1k1o/neko/internal/types" - "m1k1o/neko/internal/types/event" - "m1k1o/neko/internal/types/message" -) - -func (h *MessageHandler) chat(id string, session types.Session, payload *message.ChatReceive) error { - if session.Muted() { - return nil - } - - if err := h.sessions.Broadcast( - message.ChatSend{ - Event: event.CHAT_MESSAGE, - Content: payload.Content, - ID: id, - }, nil); err != nil { - h.logger.Warn().Err(err).Msgf("broadcasting event %s has failed", event.CHAT_MESSAGE) - return err - } - return nil -} - -func (h *MessageHandler) chatEmote(id string, session types.Session, payload *message.EmoteReceive) error { - if session.Muted() { - return nil - } - - if err := h.sessions.Broadcast( - message.EmoteSend{ - Event: event.CHAT_EMOTE, - Emote: payload.Emote, - ID: id, - }, nil); err != nil { - h.logger.Warn().Err(err).Msgf("broadcasting event %s has failed", event.CHAT_EMOTE) - return err - } - return nil -} diff --git a/internal/websocket/handler/clipboard.go b/server/internal/websocket/handler/clipboard.go similarity index 100% rename from internal/websocket/handler/clipboard.go rename to server/internal/websocket/handler/clipboard.go diff --git a/server/internal/websocket/handler/control.go b/server/internal/websocket/handler/control.go index 2a3f4ba6..80e90c66 100644 --- a/server/internal/websocket/handler/control.go +++ b/server/internal/websocket/handler/control.go @@ -1,157 +1,228 @@ package handler import ( - "m1k1o/neko/internal/types" - "m1k1o/neko/internal/types/event" - "m1k1o/neko/internal/types/message" + "errors" + + "github.com/demodesk/neko/pkg/types" + "github.com/demodesk/neko/pkg/types/event" + "github.com/demodesk/neko/pkg/types/message" + "github.com/demodesk/neko/pkg/xorg" ) -func (h *MessageHandler) controlRelease(id string, session types.Session) error { - // check if session is host - if !h.sessions.IsHost(id) { - h.logger.Debug().Str("id", id).Msg("is not the host") - return nil +var ( + ErrIsNotAllowedToHost = errors.New("is not allowed to host") + ErrIsNotTheHost = errors.New("is not the host") + ErrIsAlreadyTheHost = errors.New("is already the host") + ErrIsAlreadyHosted = errors.New("is already hosted") +) + +func (h *MessageHandlerCtx) controlRelease(session types.Session) error { + if !session.Profile().CanHost || session.PrivateModeEnabled() { + return ErrIsNotAllowedToHost } - // release host - h.logger.Debug().Str("id", id).Msgf("host called %s", event.CONTROL_RELEASE) - h.sessions.ClearHost() - - // tell everyone - if err := h.sessions.Broadcast( - message.Control{ - Event: event.CONTROL_RELEASE, - ID: id, - }, nil); err != nil { - h.logger.Warn().Err(err).Msgf("broadcasting event %s has failed", event.CONTROL_RELEASE) - return err + if !session.IsHost() { + return ErrIsNotTheHost } + h.desktop.ResetKeys() + session.ClearHost() + return nil } -func (h *MessageHandler) controlRequest(id string, session types.Session) error { - // check for host - if !h.sessions.HasHost() { - // check if control is locked or user is admin - if h.state.IsLocked("control") && !session.Admin() { - h.logger.Debug().Msg("control is locked") - return nil - } +func (h *MessageHandlerCtx) controlRequest(session types.Session) error { + if !session.Profile().CanHost || session.PrivateModeEnabled() { + return ErrIsNotAllowedToHost + } - // set host - err := h.sessions.SetHost(id) - if err != nil { - return err - } + if session.IsHost() { + return ErrIsAlreadyTheHost + } - // let everyone know - if err := h.sessions.Broadcast( - message.Control{ - Event: event.CONTROL_LOCKED, - ID: id, - }, nil); err != nil { - h.logger.Warn().Err(err).Msgf("broadcasting event %s has failed", event.CONTROL_LOCKED) - return err - } + if h.sessions.Settings().LockedControls && !session.Profile().IsAdmin { + return ErrIsNotAllowedToHost + } + // if implicit hosting is enabled, set session as host without asking + if h.sessions.Settings().ImplicitHosting { + session.SetAsHost() return nil } - // get host - host, ok := h.sessions.GetHost() - if ok { - - // tell session there is a host - if err := session.Send(message.Control{ - Event: event.CONTROL_REQUEST, - ID: host.ID(), - }); err != nil { - h.logger.Warn().Err(err).Str("id", id).Msgf("sending event %s has failed", event.CONTROL_REQUEST) - return err - } - - // tell host session wants to be host - if err := host.Send(message.Control{ - Event: event.CONTROL_REQUESTING, - ID: id, - }); err != nil { - h.logger.Warn().Err(err).Str("id", host.ID()).Msgf("sending event %s has failed", event.CONTROL_REQUESTING) - return err - } - } - - return nil -} - -func (h *MessageHandler) controlGive(id string, session types.Session, payload *message.Control) error { - // check if session is host - if !h.sessions.IsHost(id) { - h.logger.Debug().Str("id", id).Msg("is not the host") + // if there is no host, set session as host + host, hasHost := h.sessions.GetHost() + if !hasHost { + session.SetAsHost() return nil } - if !h.sessions.Has(payload.ID) { - h.logger.Debug().Str("id", payload.ID).Msg("user does not exist") - return nil - } + // TODO: Some throttling mechanism to prevent spamming. - // check if control is locked or giver is admin - if h.state.IsLocked("control") && !session.Admin() { - h.logger.Debug().Msg("control is locked") - return nil - } - - // set host - err := h.sessions.SetHost(payload.ID) - if err != nil { - return err - } - - // let everyone know - if err := h.sessions.Broadcast( - message.ControlTarget{ - Event: event.CONTROL_GIVE, - ID: id, - Target: payload.ID, - }, nil); err != nil { - h.logger.Warn().Err(err).Msgf("broadcasting event %s has failed", event.CONTROL_GIVE) - return err - } - - return nil -} - -func (h *MessageHandler) controlClipboard(id string, session types.Session, payload *message.Clipboard) error { - // check if session can access clipboard - if (!h.webrtc.ImplicitControl() && !h.sessions.IsHost(id)) || (h.webrtc.ImplicitControl() && !h.sessions.CanControl(id)) { - h.logger.Debug().Str("id", id).Msg("cannot access clipboard") - return nil - } - - h.desktop.WriteClipboard(payload.Text) - return nil -} - -func (h *MessageHandler) controlKeyboard(id string, session types.Session, payload *message.Keyboard) error { - // check if session can control keyboard - if (!h.webrtc.ImplicitControl() && !h.sessions.IsHost(id)) || (h.webrtc.ImplicitControl() && !h.sessions.CanControl(id)) { - h.logger.Debug().Str("id", id).Msg("cannot control keyboard") - return nil - } - - h.desktop.SetKeyboardModifiers(types.KeyboardModifiers{ - NumLock: payload.NumLock, - CapsLock: payload.CapsLock, - // TODO: ScrollLock is deprecated. - }) - - // change layout - if payload.Layout != nil { - return h.desktop.SetKeyboardMap(types.KeyboardMap{ - Layout: *payload.Layout, + // let host know that someone wants to take control + host.Send( + event.CONTROL_REQUEST, + message.SessionID{ + ID: session.ID(), }) + + return ErrIsAlreadyHosted +} + +func (h *MessageHandlerCtx) controlMove(session types.Session, payload *message.ControlPos) error { + if err := h.controlRequest(session); err != nil && !errors.Is(err, ErrIsAlreadyTheHost) { + return err } + // handle active cursor movement + h.desktop.Move(payload.X, payload.Y) + h.webrtc.SetCursorPosition(payload.X, payload.Y) return nil } + +func (h *MessageHandlerCtx) controlScroll(session types.Session, payload *message.ControlScroll) error { + if err := h.controlRequest(session); err != nil && !errors.Is(err, ErrIsAlreadyTheHost) { + return err + } + + // TOOD: remove this once the client is fixed + if payload.DeltaX == 0 && payload.DeltaY == 0 { + payload.DeltaX = payload.X + payload.DeltaY = payload.Y + } + + h.desktop.Scroll(payload.DeltaX, payload.DeltaY, payload.ControlKey) + return nil +} + +func (h *MessageHandlerCtx) controlButtonPress(session types.Session, payload *message.ControlButton) error { + if payload.ControlPos != nil { + if err := h.controlMove(session, payload.ControlPos); err != nil { + return err + } + } else if err := h.controlRequest(session); err != nil && !errors.Is(err, ErrIsAlreadyTheHost) { + return err + } + + return h.desktop.ButtonPress(payload.Code) +} + +func (h *MessageHandlerCtx) controlButtonDown(session types.Session, payload *message.ControlButton) error { + if payload.ControlPos != nil { + if err := h.controlMove(session, payload.ControlPos); err != nil { + return err + } + } else if err := h.controlRequest(session); err != nil && !errors.Is(err, ErrIsAlreadyTheHost) { + return err + } + + return h.desktop.ButtonDown(payload.Code) +} + +func (h *MessageHandlerCtx) controlButtonUp(session types.Session, payload *message.ControlButton) error { + if payload.ControlPos != nil { + if err := h.controlMove(session, payload.ControlPos); err != nil { + return err + } + } else if err := h.controlRequest(session); err != nil && !errors.Is(err, ErrIsAlreadyTheHost) { + return err + } + + return h.desktop.ButtonUp(payload.Code) +} + +func (h *MessageHandlerCtx) controlKeyPress(session types.Session, payload *message.ControlKey) error { + if payload.ControlPos != nil { + if err := h.controlMove(session, payload.ControlPos); err != nil { + return err + } + } else if err := h.controlRequest(session); err != nil && !errors.Is(err, ErrIsAlreadyTheHost) { + return err + } + + return h.desktop.KeyPress(payload.Keysym) +} + +func (h *MessageHandlerCtx) controlKeyDown(session types.Session, payload *message.ControlKey) error { + if payload.ControlPos != nil { + if err := h.controlMove(session, payload.ControlPos); err != nil { + return err + } + } else if err := h.controlRequest(session); err != nil && !errors.Is(err, ErrIsAlreadyTheHost) { + return err + } + + return h.desktop.KeyDown(payload.Keysym) +} + +func (h *MessageHandlerCtx) controlKeyUp(session types.Session, payload *message.ControlKey) error { + if payload.ControlPos != nil { + if err := h.controlMove(session, payload.ControlPos); err != nil { + return err + } + } else if err := h.controlRequest(session); err != nil && !errors.Is(err, ErrIsAlreadyTheHost) { + return err + } + + return h.desktop.KeyUp(payload.Keysym) +} + +func (h *MessageHandlerCtx) controlTouchBegin(session types.Session, payload *message.ControlTouch) error { + if err := h.controlRequest(session); err != nil && !errors.Is(err, ErrIsAlreadyTheHost) { + return err + } + return h.desktop.TouchBegin(payload.TouchId, payload.X, payload.Y, payload.Pressure) +} + +func (h *MessageHandlerCtx) controlTouchUpdate(session types.Session, payload *message.ControlTouch) error { + if err := h.controlRequest(session); err != nil && !errors.Is(err, ErrIsAlreadyTheHost) { + return err + } + return h.desktop.TouchUpdate(payload.TouchId, payload.X, payload.Y, payload.Pressure) +} + +func (h *MessageHandlerCtx) controlTouchEnd(session types.Session, payload *message.ControlTouch) error { + if err := h.controlRequest(session); err != nil && !errors.Is(err, ErrIsAlreadyTheHost) { + return err + } + return h.desktop.TouchEnd(payload.TouchId, payload.X, payload.Y, payload.Pressure) +} + +func (h *MessageHandlerCtx) controlCut(session types.Session) error { + if err := h.controlRequest(session); err != nil && !errors.Is(err, ErrIsAlreadyTheHost) { + return err + } + + return h.desktop.KeyPress(xorg.XK_Control_L, xorg.XK_x) +} + +func (h *MessageHandlerCtx) controlCopy(session types.Session) error { + if err := h.controlRequest(session); err != nil && !errors.Is(err, ErrIsAlreadyTheHost) { + return err + } + + return h.desktop.KeyPress(xorg.XK_Control_L, xorg.XK_c) +} + +func (h *MessageHandlerCtx) controlPaste(session types.Session, payload *message.ClipboardData) error { + if err := h.controlRequest(session); err != nil && !errors.Is(err, ErrIsAlreadyTheHost) { + return err + } + + // if there have been set clipboard data, set them first + if payload != nil && payload.Text != "" { + if err := h.clipboardSet(session, payload); err != nil { + return err + } + } + + return h.desktop.KeyPress(xorg.XK_Control_L, xorg.XK_v) +} + +func (h *MessageHandlerCtx) controlSelectAll(session types.Session) error { + if err := h.controlRequest(session); err != nil && !errors.Is(err, ErrIsAlreadyTheHost) { + return err + } + + return h.desktop.KeyPress(xorg.XK_Control_L, xorg.XK_a) +} diff --git a/server/internal/websocket/handler/filetransfer.go b/server/internal/websocket/handler/filetransfer.go deleted file mode 100644 index 8d606805..00000000 --- a/server/internal/websocket/handler/filetransfer.go +++ /dev/null @@ -1,47 +0,0 @@ -package handler - -import ( - "m1k1o/neko/internal/types" - "m1k1o/neko/internal/types/event" - "m1k1o/neko/internal/types/message" - "m1k1o/neko/internal/utils" -) - -func (h *MessageHandler) FileTransferRefresh(session types.Session) error { - if !h.state.FileTransferEnabled() { - return nil - } - - fileTransferPath := h.state.FileTransferPath("") // root - - // allow users only if file transfer is not locked - if session != nil && !(session.Admin() || !h.state.IsLocked("file_transfer")) { - h.logger.Debug().Msg("file transfer is locked for users") - return nil - } - - // TODO: keep list of files in memory and update it on file changes - files, err := utils.ListFiles(fileTransferPath) - if err != nil { - return err - } - - message := message.FileTransferList{ - Event: event.FILETRANSFER_LIST, - Cwd: fileTransferPath, - Files: files, - } - - // send to just one user - if session != nil { - return session.Send(message) - } - - // broadcast to all admins - if h.state.IsLocked("file_transfer") { - return h.sessions.AdminBroadcast(message, nil) - } - - // broadcast to all users - return h.sessions.Broadcast(message, nil) -} diff --git a/server/internal/websocket/handler/handler.go b/server/internal/websocket/handler/handler.go index 628771ca..43ea2a17 100644 --- a/server/internal/websocket/handler/handler.go +++ b/server/internal/websocket/handler/handler.go @@ -1,211 +1,203 @@ package handler import ( - "encoding/json" - - "github.com/pkg/errors" "github.com/rs/zerolog" "github.com/rs/zerolog/log" - "m1k1o/neko/internal/types" - "m1k1o/neko/internal/types/event" - "m1k1o/neko/internal/types/message" - "m1k1o/neko/internal/utils" - "m1k1o/neko/internal/websocket/state" + "github.com/demodesk/neko/pkg/types" + "github.com/demodesk/neko/pkg/types/event" + "github.com/demodesk/neko/pkg/types/message" + "github.com/demodesk/neko/pkg/utils" ) -type MessageHandler struct { - logger zerolog.Logger - sessions types.SessionManager - desktop types.DesktopManager - capture types.CaptureManager - webrtc types.WebRTCManager - state *state.State -} - func New( sessions types.SessionManager, desktop types.DesktopManager, capture types.CaptureManager, webrtc types.WebRTCManager, - state *state.State, -) *MessageHandler { - return &MessageHandler{ +) *MessageHandlerCtx { + return &MessageHandlerCtx{ logger: log.With().Str("module", "websocket").Str("submodule", "handler").Logger(), sessions: sessions, desktop: desktop, capture: capture, webrtc: webrtc, - state: state, } } -func (h *MessageHandler) Connected(admin bool, address string) (bool, string) { - if address == "" { - h.logger.Debug().Msg("no remote address") - } else { - if h.state.IsBanned(address) { - h.logger.Debug().Str("address", address).Msg("banned") - return false, "banned" - } - } - - if h.state.IsLocked("login") && !admin { - h.logger.Debug().Msg("server locked") - return false, "locked" - } - - return true, "" +type MessageHandlerCtx struct { + logger zerolog.Logger + sessions types.SessionManager + webrtc types.WebRTCManager + desktop types.DesktopManager + capture types.CaptureManager } -func (h *MessageHandler) Disconnected(id string) { - h.sessions.Destroy(id) -} +func (h *MessageHandlerCtx) Message(session types.Session, data types.WebSocketMessage) bool { + var err error + switch data.Event { + // System Events + case event.SYSTEM_LOGS: + payload := &message.SystemLogs{} + err = utils.Unmarshal(payload, data.Payload, func() error { + return h.systemLogs(session, payload) + }) -func (h *MessageHandler) Message(id string, raw []byte) error { - header := message.Message{} - if err := json.Unmarshal(raw, &header); err != nil { - return err - } - - session, ok := h.sessions.Get(id) - if !ok { - return errors.Errorf("unknown session id %s", id) - } - - switch header.Event { // Signal Events + case event.SIGNAL_REQUEST: + payload := &message.SignalRequest{} + err = utils.Unmarshal(payload, data.Payload, func() error { + return h.signalRequest(session, payload) + }) + case event.SIGNAL_RESTART: + err = h.signalRestart(session) case event.SIGNAL_OFFER: - payload := &message.SignalOffer{} - return errors.Wrapf( - utils.Unmarshal(payload, raw, func() error { - return h.signalRemoteOffer(id, session, payload) - }), "%s failed", header.Event) + payload := &message.SignalDescription{} + err = utils.Unmarshal(payload, data.Payload, func() error { + return h.signalOffer(session, payload) + }) case event.SIGNAL_ANSWER: - payload := &message.SignalAnswer{} - return errors.Wrapf( - utils.Unmarshal(payload, raw, func() error { - return h.signalRemoteAnswer(id, session, payload) - }), "%s failed", header.Event) + payload := &message.SignalDescription{} + err = utils.Unmarshal(payload, data.Payload, func() error { + return h.signalAnswer(session, payload) + }) case event.SIGNAL_CANDIDATE: payload := &message.SignalCandidate{} - return errors.Wrapf( - utils.Unmarshal(payload, raw, func() error { - return h.signalRemoteCandidate(id, session, payload) - }), "%s failed", header.Event) + err = utils.Unmarshal(payload, data.Payload, func() error { + return h.signalCandidate(session, payload) + }) + case event.SIGNAL_VIDEO: + payload := &message.SignalVideo{} + err = utils.Unmarshal(payload, data.Payload, func() error { + return h.signalVideo(session, payload) + }) + case event.SIGNAL_AUDIO: + payload := &message.SignalAudio{} + err = utils.Unmarshal(payload, data.Payload, func() error { + return h.signalAudio(session, payload) + }) // Control Events case event.CONTROL_RELEASE: - return errors.Wrapf(h.controlRelease(id, session), "%s failed", header.Event) + err = h.controlRelease(session) case event.CONTROL_REQUEST: - return errors.Wrapf(h.controlRequest(id, session), "%s failed", header.Event) - case event.CONTROL_GIVE: - payload := &message.Control{} - return errors.Wrapf( - utils.Unmarshal(payload, raw, func() error { - return h.controlGive(id, session, payload) - }), "%s failed", header.Event) - case event.CONTROL_CLIPBOARD: - payload := &message.Clipboard{} - return errors.Wrapf( - utils.Unmarshal(payload, raw, func() error { - return h.controlClipboard(id, session, payload) - }), "%s failed", header.Event) - case event.CONTROL_KEYBOARD: - payload := &message.Keyboard{} - return errors.Wrapf( - utils.Unmarshal(payload, raw, func() error { - return h.controlKeyboard(id, session, payload) - }), "%s failed", header.Event) - - // Chat Events - case event.CHAT_MESSAGE: - payload := &message.ChatReceive{} - return errors.Wrapf( - utils.Unmarshal(payload, raw, func() error { - return h.chat(id, session, payload) - }), "%s failed", header.Event) - case event.CHAT_EMOTE: - payload := &message.EmoteReceive{} - return errors.Wrapf( - utils.Unmarshal(payload, raw, func() error { - return h.chatEmote(id, session, payload) - }), "%s failed", header.Event) - - // File Transfer Events - case event.FILETRANSFER_REFRESH: - return errors.Wrapf(h.FileTransferRefresh(session), "%s failed", header.Event) + err = h.controlRequest(session) + case event.CONTROL_MOVE: + payload := &message.ControlPos{} + err = utils.Unmarshal(payload, data.Payload, func() error { + return h.controlMove(session, payload) + }) + case event.CONTROL_SCROLL: + payload := &message.ControlScroll{} + err = utils.Unmarshal(payload, data.Payload, func() error { + return h.controlScroll(session, payload) + }) + case event.CONTROL_BUTTONPRESS: + payload := &message.ControlButton{} + err = utils.Unmarshal(payload, data.Payload, func() error { + return h.controlButtonPress(session, payload) + }) + case event.CONTROL_BUTTONDOWN: + payload := &message.ControlButton{} + err = utils.Unmarshal(payload, data.Payload, func() error { + return h.controlButtonDown(session, payload) + }) + case event.CONTROL_BUTTONUP: + payload := &message.ControlButton{} + err = utils.Unmarshal(payload, data.Payload, func() error { + return h.controlButtonUp(session, payload) + }) + case event.CONTROL_KEYPRESS: + payload := &message.ControlKey{} + err = utils.Unmarshal(payload, data.Payload, func() error { + return h.controlKeyPress(session, payload) + }) + case event.CONTROL_KEYDOWN: + payload := &message.ControlKey{} + err = utils.Unmarshal(payload, data.Payload, func() error { + return h.controlKeyDown(session, payload) + }) + case event.CONTROL_KEYUP: + payload := &message.ControlKey{} + err = utils.Unmarshal(payload, data.Payload, func() error { + return h.controlKeyUp(session, payload) + }) + // touch + case event.CONTROL_TOUCHBEGIN: + payload := &message.ControlTouch{} + err = utils.Unmarshal(payload, data.Payload, func() error { + return h.controlTouchBegin(session, payload) + }) + case event.CONTROL_TOUCHUPDATE: + payload := &message.ControlTouch{} + err = utils.Unmarshal(payload, data.Payload, func() error { + return h.controlTouchUpdate(session, payload) + }) + case event.CONTROL_TOUCHEND: + payload := &message.ControlTouch{} + err = utils.Unmarshal(payload, data.Payload, func() error { + return h.controlTouchEnd(session, payload) + }) + // actions + case event.CONTROL_CUT: + err = h.controlCut(session) + case event.CONTROL_COPY: + err = h.controlCopy(session) + case event.CONTROL_PASTE: + payload := &message.ClipboardData{} + err = utils.Unmarshal(payload, data.Payload, func() error { + return h.controlPaste(session, payload) + }) + case event.CONTROL_SELECT_ALL: + err = h.controlSelectAll(session) // Screen Events - case event.SCREEN_RESOLUTION: - return errors.Wrapf(h.screenResolution(id, session), "%s failed", header.Event) - case event.SCREEN_CONFIGURATIONS: - return errors.Wrapf(h.screenConfigurations(id, session), "%s failed", header.Event) case event.SCREEN_SET: - payload := &message.ScreenResolution{} - return errors.Wrapf( - utils.Unmarshal(payload, raw, func() error { - return h.screenSet(id, session, payload) - }), "%s failed", header.Event) + payload := &message.ScreenSize{} + err = utils.Unmarshal(payload, data.Payload, func() error { + return h.screenSet(session, payload) + }) - // Broadcast Events - case event.BROADCAST_CREATE: - payload := &message.BroadcastCreate{} - return errors.Wrapf( - utils.Unmarshal(payload, raw, func() error { - return h.broadcastCreate(session, payload) - }), "%s failed", header.Event) - case event.BROADCAST_DESTROY: - return errors.Wrapf(h.broadcastDestroy(session), "%s failed", header.Event) + // Clipboard Events + case event.CLIPBOARD_SET: + payload := &message.ClipboardData{} + err = utils.Unmarshal(payload, data.Payload, func() error { + return h.clipboardSet(session, payload) + }) - // Admin Events - case event.ADMIN_LOCK: - payload := &message.AdminLock{} - return errors.Wrapf( - utils.Unmarshal(payload, raw, func() error { - return h.adminLock(id, session, payload) - }), "%s failed", header.Event) - case event.ADMIN_UNLOCK: - payload := &message.AdminLock{} - return errors.Wrapf( - utils.Unmarshal(payload, raw, func() error { - return h.adminUnlock(id, session, payload) - }), "%s failed", header.Event) - case event.ADMIN_CONTROL: - return errors.Wrapf(h.adminControl(id, session), "%s failed", header.Event) - case event.ADMIN_RELEASE: - return errors.Wrapf(h.AdminRelease(id, session), "%s failed", header.Event) - case event.ADMIN_GIVE: - payload := &message.Admin{} - return errors.Wrapf( - utils.Unmarshal(payload, raw, func() error { - return h.adminGive(id, session, payload) - }), "%s failed", header.Event) - case event.ADMIN_BAN: - payload := &message.Admin{} - return errors.Wrapf( - utils.Unmarshal(payload, raw, func() error { - return h.adminBan(id, session, payload) - }), "%s failed", header.Event) - case event.ADMIN_KICK: - payload := &message.Admin{} - return errors.Wrapf( - utils.Unmarshal(payload, raw, func() error { - return h.adminKick(id, session, payload) - }), "%s failed", header.Event) - case event.ADMIN_MUTE: - payload := &message.Admin{} - return errors.Wrapf( - utils.Unmarshal(payload, raw, func() error { - return h.adminMute(id, session, payload) - }), "%s failed", header.Event) - case event.ADMIN_UNMUTE: - payload := &message.Admin{} - return errors.Wrapf( - utils.Unmarshal(payload, raw, func() error { - return h.adminUnmute(id, session, payload) - }), "%s failed", header.Event) + // Keyboard Events + case event.KEYBOARD_MAP: + payload := &message.KeyboardMap{} + err = utils.Unmarshal(payload, data.Payload, func() error { + return h.keyboardMap(session, payload) + }) + case event.KEYBOARD_MODIFIERS: + payload := &message.KeyboardModifiers{} + err = utils.Unmarshal(payload, data.Payload, func() error { + return h.keyboardModifiers(session, payload) + }) + + // Send Events + case event.SEND_UNICAST: + payload := &message.SendUnicast{} + err = utils.Unmarshal(payload, data.Payload, func() error { + return h.sendUnicast(session, payload) + }) + case event.SEND_BROADCAST: + payload := &message.SendBroadcast{} + err = utils.Unmarshal(payload, data.Payload, func() error { + return h.sendBroadcast(session, payload) + }) default: - return errors.Errorf("unknown message event %s", header.Event) + return false } + + if err != nil { + h.logger.Warn().Err(err). + Str("event", data.Event). + Str("session_id", session.ID()). + Msg("message handler has failed") + } + + return true } diff --git a/internal/websocket/handler/keyboard.go b/server/internal/websocket/handler/keyboard.go similarity index 100% rename from internal/websocket/handler/keyboard.go rename to server/internal/websocket/handler/keyboard.go diff --git a/server/internal/websocket/handler/screen.go b/server/internal/websocket/handler/screen.go index c4cacc4c..4419cd92 100644 --- a/server/internal/websocket/handler/screen.go +++ b/server/internal/websocket/handler/screen.go @@ -1,70 +1,26 @@ package handler import ( - "m1k1o/neko/internal/types" - "m1k1o/neko/internal/types/event" - "m1k1o/neko/internal/types/message" + "errors" + + "github.com/demodesk/neko/pkg/types" + "github.com/demodesk/neko/pkg/types/event" + "github.com/demodesk/neko/pkg/types/message" ) -func (h *MessageHandler) screenSet(id string, session types.Session, payload *message.ScreenResolution) error { - if !session.Admin() { - h.logger.Debug().Msg("user not admin") - return nil +func (h *MessageHandlerCtx) screenSet(session types.Session, payload *message.ScreenSize) error { + if !session.Profile().IsAdmin { + return errors.New("is not the admin") } - if err := h.desktop.SetScreenSize(types.ScreenSize{ - Width: payload.Width, - Height: payload.Height, - Rate: payload.Rate, - }); err != nil { - h.logger.Warn().Err(err).Msgf("unable to change screen size") - return err - } - - if err := h.sessions.Broadcast( - message.ScreenResolution{ - Event: event.SCREEN_RESOLUTION, - ID: id, - Width: payload.Width, - Height: payload.Height, - Rate: payload.Rate, - }, nil); err != nil { - h.logger.Warn().Err(err).Msgf("broadcasting event %s has failed", event.SCREEN_RESOLUTION) - return err - } - - return nil -} - -func (h *MessageHandler) screenResolution(id string, session types.Session) error { - if size := h.desktop.GetScreenSize(); size != nil { - if err := session.Send(message.ScreenResolution{ - Event: event.SCREEN_RESOLUTION, - Width: size.Width, - Height: size.Height, - Rate: size.Rate, - }); err != nil { - h.logger.Warn().Err(err).Msgf("sending event %s has failed", event.SCREEN_RESOLUTION) - return err - } - } - - return nil -} - -func (h *MessageHandler) screenConfigurations(id string, session types.Session) error { - if !session.Admin() { - h.logger.Debug().Msg("user not admin") - return nil - } - - if err := session.Send(message.ScreenConfigurations{ - Event: event.SCREEN_CONFIGURATIONS, - Configurations: h.desktop.ScreenConfigurations(), - }); err != nil { - h.logger.Warn().Err(err).Msgf("sending event %s has failed", event.SCREEN_CONFIGURATIONS) + size, err := h.desktop.SetScreenSize(payload.ScreenSize) + if err != nil { return err } + h.sessions.Broadcast(event.SCREEN_UPDATED, message.ScreenSizeUpdate{ + ID: session.ID(), + ScreenSize: size, + }) return nil } diff --git a/internal/websocket/handler/send.go b/server/internal/websocket/handler/send.go similarity index 100% rename from internal/websocket/handler/send.go rename to server/internal/websocket/handler/send.go diff --git a/server/internal/websocket/handler/session.go b/server/internal/websocket/handler/session.go index af53d252..321f6342 100644 --- a/server/internal/websocket/handler/session.go +++ b/server/internal/websocket/handler/session.go @@ -1,111 +1,106 @@ package handler import ( - "m1k1o/neko/internal/types" - "m1k1o/neko/internal/types/event" - "m1k1o/neko/internal/types/message" + "github.com/demodesk/neko/pkg/types" + "github.com/demodesk/neko/pkg/types/event" + "github.com/demodesk/neko/pkg/types/message" ) -func (h *MessageHandler) SessionCreated(id string, session types.Session) error { - // send sdp and id over to client - if err := h.signalProvide(id, session); err != nil { - return err - } - - // send initialization information - if err := session.Send(message.SystemInit{ - Event: event.SYSTEM_INIT, - ImplicitHosting: h.webrtc.ImplicitControl(), - Locks: h.state.AllLocked(), - FileTransfer: h.state.FileTransferEnabled(), - }); err != nil { - h.logger.Warn().Str("id", id).Err(err).Msgf("sending event %s has failed", event.SYSTEM_INIT) - return err - } - - if session.Admin() { - // send screen configurations if admin - if err := h.screenConfigurations(id, session); err != nil { - return err - } - - // send broadcast status if admin - if err := h.broadcastStatus(session); err != nil { - return err - } - } - - // send file list if file transfer is enabled - if h.state.FileTransferEnabled() && (session.Admin() || !h.state.IsLocked("file_transfer")) { - if err := h.FileTransferRefresh(session); err != nil { - return err - } - } +func (h *MessageHandlerCtx) SessionCreated(session types.Session) error { + h.sessions.Broadcast( + event.SESSION_CREATED, + message.SessionData{ + ID: session.ID(), + Profile: session.Profile(), + State: session.State(), + }) return nil } -func (h *MessageHandler) SessionConnected(id string, session types.Session) error { - // send list of members to session - if err := session.Send(message.MembersList{ - Event: event.MEMBER_LIST, - Members: h.sessions.Members(), - }); err != nil { - h.logger.Warn().Str("id", id).Err(err).Msgf("sending event %s has failed", event.MEMBER_LIST) - return err - } - - // send screen current resolution - if err := h.screenResolution(id, session); err != nil { - return err - } - - // tell session there is a host - host, ok := h.sessions.GetHost() - if ok { - if err := session.Send(message.Control{ - Event: event.CONTROL_LOCKED, - ID: host.ID(), - }); err != nil { - h.logger.Warn().Str("id", id).Err(err).Msgf("sending event %s has failed", event.CONTROL_LOCKED) - return err - } - } - - // let everyone know there is a new session - if err := h.sessions.Broadcast( - message.Member{ - Event: event.MEMBER_CONNECTED, - Member: session.Member(), - }, nil); err != nil { - h.logger.Warn().Err(err).Msgf("broadcasting event %s has failed", event.MEMBER_CONNECTED) - return err - } +func (h *MessageHandlerCtx) SessionDeleted(session types.Session) error { + h.sessions.Broadcast( + event.SESSION_DELETED, + message.SessionID{ + ID: session.ID(), + }) return nil } -func (h *MessageHandler) SessionDestroyed(id string) error { +func (h *MessageHandlerCtx) SessionConnected(session types.Session) error { + if err := h.systemInit(session); err != nil { + return err + } + + if session.Profile().IsAdmin { + if err := h.systemAdmin(session); err != nil { + return err + } + + // update settings in atomic way + h.sessions.UpdateSettingsFunc(session, func(settings *types.Settings) bool { + // if control protection & locked controls: unlock controls + if settings.LockedControls && settings.ControlProtection { + settings.LockedControls = false + return true // update settings + } + return false // do not update settings + }) + } + + return h.SessionStateChanged(session) +} + +func (h *MessageHandlerCtx) SessionDisconnected(session types.Session) error { // clear host if exists - if h.sessions.IsHost(id) { - h.sessions.ClearHost() - if err := h.sessions.Broadcast(message.Control{ - Event: event.CONTROL_RELEASE, - ID: id, - }, nil); err != nil { - h.logger.Warn().Err(err).Msgf("broadcasting event %s has failed", event.CONTROL_RELEASE) - } + if session.IsHost() { + h.desktop.ResetKeys() + session.ClearHost() } - // let everyone know session disconnected - if err := h.sessions.Broadcast( - message.MemberDisconnected{ - Event: event.MEMBER_DISCONNECTED, - ID: id, - }, nil); err != nil { - h.logger.Warn().Err(err).Msgf("broadcasting event %s has failed", event.MEMBER_DISCONNECTED) - return err + if session.Profile().IsAdmin { + hasAdmin := false + h.sessions.Range(func(s types.Session) bool { + if s.Profile().IsAdmin && s.ID() != session.ID() && s.State().IsConnected { + hasAdmin = true + return false + } + return true + }) + + // update settings in atomic way + h.sessions.UpdateSettingsFunc(session, func(settings *types.Settings) bool { + // if control protection & not locked controls & no admin: lock controls + if !settings.LockedControls && settings.ControlProtection && !hasAdmin { + settings.LockedControls = true + return true // update settings + } + return false // do not update settings + }) } + return h.SessionStateChanged(session) +} + +func (h *MessageHandlerCtx) SessionProfileChanged(session types.Session, new, old types.MemberProfile) error { + h.sessions.Broadcast( + event.SESSION_PROFILE, + message.MemberProfile{ + ID: session.ID(), + MemberProfile: new, + }) + + return nil +} + +func (h *MessageHandlerCtx) SessionStateChanged(session types.Session) error { + h.sessions.Broadcast( + event.SESSION_STATE, + message.SessionState{ + ID: session.ID(), + SessionState: session.State(), + }) + return nil } diff --git a/server/internal/websocket/handler/signal.go b/server/internal/websocket/handler/signal.go index da1f2e0b..e0e5fb88 100644 --- a/server/internal/websocket/handler/signal.go +++ b/server/internal/websocket/handler/signal.go @@ -1,51 +1,162 @@ package handler import ( - "m1k1o/neko/internal/types" - "m1k1o/neko/internal/types/event" - "m1k1o/neko/internal/types/message" + "errors" + + "github.com/demodesk/neko/pkg/types" + "github.com/demodesk/neko/pkg/types/event" + "github.com/demodesk/neko/pkg/types/message" + "github.com/pion/webrtc/v3" ) -func (h *MessageHandler) signalProvide(id string, session types.Session) error { - peer, err := h.webrtc.CreatePeer(id, session) +func (h *MessageHandlerCtx) signalRequest(session types.Session, payload *message.SignalRequest) error { + if !session.Profile().CanWatch { + return errors.New("not allowed to watch") + } + + offer, peer, err := h.webrtc.CreatePeer(session) if err != nil { return err } - sdp, err := peer.CreateOffer() + // set webrtc as paused if session has private mode enabled + if session.PrivateModeEnabled() { + peer.SetPaused(true) + } + + video := payload.Video + + // use default first video, if not provided + if video.Selector == nil { + videos := h.capture.Video().IDs() + video.Selector = &types.StreamSelector{ + ID: videos[0], + Type: types.StreamSelectorTypeExact, + } + } + + // TODO: Remove, used for compatibility with old clients. + if video.Auto == nil { + video.Auto = &payload.Auto + } + + // set video stream + err = peer.SetVideo(video) if err != nil { return err } - if err := session.Send(message.SignalProvide{ - Event: event.SIGNAL_PROVIDE, - ID: id, - SDP: sdp, - Lite: h.webrtc.ICELite(), - ICE: h.webrtc.ICEServers(), - }); err != nil { + audio := payload.Audio + + // enable by default if not requested otherwise + if audio.Disabled == nil { + disabled := false + audio.Disabled = &disabled + } + + // set audio stream + err = peer.SetAudio(audio) + if err != nil { return err } + session.Send( + event.SIGNAL_PROVIDE, + message.SignalProvide{ + SDP: offer.SDP, + ICEServers: h.webrtc.ICEServers(), + + Video: peer.Video(), + Audio: peer.Audio(), + }) + return nil } -func (h *MessageHandler) signalRemoteOffer(id string, session types.Session, payload *message.SignalOffer) error { - return session.SignalRemoteOffer(payload.SDP) -} +func (h *MessageHandlerCtx) signalRestart(session types.Session) error { + peer := session.GetWebRTCPeer() + if peer == nil { + return errors.New("webRTC peer does not exist") + } -func (h *MessageHandler) signalRemoteAnswer(id string, session types.Session, payload *message.SignalAnswer) error { - if err := session.SetName(payload.DisplayName); err != nil { + offer, err := peer.CreateOffer(true) + if err != nil { return err } - if err := session.SignalRemoteAnswer(payload.SDP); err != nil { - return err - } + // TODO: Use offer event instead. + session.Send( + event.SIGNAL_RESTART, + message.SignalDescription{ + SDP: offer.SDP, + }) return nil } -func (h *MessageHandler) signalRemoteCandidate(id string, session types.Session, payload *message.SignalCandidate) error { - return session.SignalRemoteCandidate(payload.Data) +func (h *MessageHandlerCtx) signalOffer(session types.Session, payload *message.SignalDescription) error { + peer := session.GetWebRTCPeer() + if peer == nil { + return errors.New("webRTC peer does not exist") + } + + err := peer.SetRemoteDescription(webrtc.SessionDescription{ + SDP: payload.SDP, + Type: webrtc.SDPTypeOffer, + }) + if err != nil { + return err + } + + answer, err := peer.CreateAnswer() + if err != nil { + return err + } + + session.Send( + event.SIGNAL_ANSWER, + message.SignalDescription{ + SDP: answer.SDP, + }) + + return nil +} + +func (h *MessageHandlerCtx) signalAnswer(session types.Session, payload *message.SignalDescription) error { + peer := session.GetWebRTCPeer() + if peer == nil { + return errors.New("webRTC peer does not exist") + } + + return peer.SetRemoteDescription(webrtc.SessionDescription{ + SDP: payload.SDP, + Type: webrtc.SDPTypeAnswer, + }) +} + +func (h *MessageHandlerCtx) signalCandidate(session types.Session, payload *message.SignalCandidate) error { + peer := session.GetWebRTCPeer() + if peer == nil { + return errors.New("webRTC peer does not exist") + } + + return peer.SetCandidate(payload.ICECandidateInit) +} + +func (h *MessageHandlerCtx) signalVideo(session types.Session, payload *message.SignalVideo) error { + peer := session.GetWebRTCPeer() + if peer == nil { + return errors.New("webRTC peer does not exist") + } + + return peer.SetVideo(payload.PeerVideoRequest) +} + +func (h *MessageHandlerCtx) signalAudio(session types.Session, payload *message.SignalAudio) error { + peer := session.GetWebRTCPeer() + if peer == nil { + return errors.New("webRTC peer does not exist") + } + + return peer.SetAudio(payload.PeerAudioRequest) } diff --git a/internal/websocket/handler/system.go b/server/internal/websocket/handler/system.go similarity index 88% rename from internal/websocket/handler/system.go rename to server/internal/websocket/handler/system.go index adf375d2..3ec60489 100644 --- a/internal/websocket/handler/system.go +++ b/server/internal/websocket/handler/system.go @@ -22,13 +22,6 @@ func (h *MessageHandlerCtx) systemInit(session types.Session) error { HostID: hostID, } - size := h.desktop.GetScreenSize() - screenSize := message.ScreenSize{ - Width: size.Width, - Height: size.Height, - Rate: size.Rate, - } - sessions := map[string]message.SessionData{} for _, session := range h.sessions.List() { sessionId := session.ID() @@ -44,7 +37,7 @@ func (h *MessageHandlerCtx) systemInit(session types.Session) error { message.SystemInit{ SessionId: session.ID(), ControlHost: controlHost, - ScreenSize: screenSize, + ScreenSize: h.desktop.GetScreenSize(), Sessions: sessions, Settings: h.sessions.Settings(), TouchEvents: h.desktop.HasTouchSupport(), @@ -60,9 +53,9 @@ func (h *MessageHandlerCtx) systemInit(session types.Session) error { func (h *MessageHandlerCtx) systemAdmin(session types.Session) error { configurations := h.desktop.ScreenConfigurations() - list := make([]message.ScreenSize, 0, len(configurations)) + list := make([]types.ScreenSize, 0, len(configurations)) for _, conf := range configurations { - list = append(list, message.ScreenSize{ + list = append(list, types.ScreenSize{ Width: conf.Width, Height: conf.Height, Rate: conf.Rate, diff --git a/internal/websocket/manager.go b/server/internal/websocket/manager.go similarity index 94% rename from internal/websocket/manager.go rename to server/internal/websocket/manager.go index 9443aa6a..d2721721 100644 --- a/internal/websocket/manager.go +++ b/server/internal/websocket/manager.go @@ -96,10 +96,12 @@ func (manager *WebSocketManagerCtx) Start() { Msg("session disconnected") }) - manager.sessions.OnProfileChanged(func(session types.Session) { - err := manager.handler.SessionProfileChanged(session) + manager.sessions.OnProfileChanged(func(session types.Session, new, old types.MemberProfile) { + err := manager.handler.SessionProfileChanged(session, new, old) manager.logger.Err(err). Str("session_id", session.ID()). + Interface("new", new). + Interface("old", old). Msg("session profile changed") }) @@ -110,24 +112,26 @@ func (manager *WebSocketManagerCtx) Start() { Msg("session state changed") }) - manager.sessions.OnHostChanged(func(session types.Session) { + manager.sessions.OnHostChanged(func(session, host types.Session) { payload := message.ControlHost{ - HasHost: session != nil, + ID: session.ID(), + HasHost: host != nil, } if payload.HasHost { - payload.HostID = session.ID() + payload.HostID = host.ID() } manager.sessions.Broadcast(event.CONTROL_HOST, payload) manager.logger.Info(). + Str("session_id", session.ID()). Bool("has_host", payload.HasHost). Str("host_id", payload.HostID). Msg("session host changed") }) - manager.sessions.OnSettingsChanged(func(new types.Settings, old types.Settings) { + manager.sessions.OnSettingsChanged(func(session types.Session, new, old types.Settings) { // start inactive cursors if new.InactiveCursors && !old.InactiveCursors { manager.startInactiveCursors() @@ -138,8 +142,13 @@ func (manager *WebSocketManagerCtx) Start() { manager.stopInactiveCursors() } - manager.sessions.Broadcast(event.SYSTEM_SETTINGS, new) + manager.sessions.Broadcast(event.SYSTEM_SETTINGS, message.SystemSettingsUpdate{ + ID: session.ID(), + Settings: new, + }) + manager.logger.Info(). + Str("session_id", session.ID()). Interface("new", new). Interface("old", old). Msg("settings changed") diff --git a/internal/websocket/peer.go b/server/internal/websocket/peer.go similarity index 100% rename from internal/websocket/peer.go rename to server/internal/websocket/peer.go diff --git a/server/internal/websocket/socket.go b/server/internal/websocket/socket.go deleted file mode 100644 index c9875bfd..00000000 --- a/server/internal/websocket/socket.go +++ /dev/null @@ -1,55 +0,0 @@ -package websocket - -import ( - "encoding/json" - "strings" - "sync" - - "github.com/gorilla/websocket" -) - -type WebSocket struct { - id string - address string - ws *WebSocketHandler - connection *websocket.Conn - mu sync.Mutex -} - -func (socket *WebSocket) Address() string { - //remote := socket.connection.RemoteAddr() - address := strings.SplitN(socket.address, ":", -1) - if len(address[0]) < 1 { - return socket.address - } - return address[0] -} - -func (socket *WebSocket) Send(v interface{}) error { - socket.mu.Lock() - defer socket.mu.Unlock() - if socket.connection == nil { - return nil - } - - raw, err := json.Marshal(v) - if err != nil { - return err - } - - socket.ws.logger.Debug(). - Str("session", socket.id). - Str("address", socket.connection.RemoteAddr().String()). - Str("raw", string(raw)). - Msg("sending message to client") - - return socket.connection.WriteMessage(websocket.TextMessage, raw) -} - -func (socket *WebSocket) Destroy() error { - if socket.connection == nil { - return nil - } - - return socket.connection.Close() -} diff --git a/server/internal/websocket/state/state.go b/server/internal/websocket/state/state.go deleted file mode 100644 index a348c19c..00000000 --- a/server/internal/websocket/state/state.go +++ /dev/null @@ -1,84 +0,0 @@ -package state - -import "path/filepath" - -type State struct { - banned map[string]string // IP -> session ID (that banned it) - locked map[string]string // resource name -> session ID (that locked it) - - fileTransferEnabled bool - fileTransferPath string // path where files are located -} - -func New(fileTransferEnabled bool, fileTransferPath string) *State { - return &State{ - banned: make(map[string]string), - locked: make(map[string]string), - - fileTransferEnabled: fileTransferEnabled, - fileTransferPath: fileTransferPath, - } -} - -// Ban - -func (s *State) Ban(ip, id string) { - s.banned[ip] = id -} - -func (s *State) Unban(ip string) { - delete(s.banned, ip) -} - -func (s *State) IsBanned(ip string) bool { - _, ok := s.banned[ip] - return ok -} - -func (s *State) GetBanned(ip string) (string, bool) { - id, ok := s.banned[ip] - return id, ok -} - -func (s *State) AllBanned() map[string]string { - return s.banned -} - -// Lock - -func (s *State) Lock(resource, id string) { - s.locked[resource] = id -} - -func (s *State) Unlock(resource string) { - delete(s.locked, resource) -} - -func (s *State) IsLocked(resource string) bool { - _, ok := s.locked[resource] - return ok -} - -func (s *State) GetLocked(resource string) (string, bool) { - id, ok := s.locked[resource] - return id, ok -} - -func (s *State) AllLocked() map[string]string { - return s.locked -} - -// File transfer - -func (s *State) FileTransferPath(filename string) string { - if filename == "" { - return s.fileTransferPath - } - - cleanPath := filepath.Clean(filename) - return filepath.Join(s.fileTransferPath, cleanPath) -} - -func (s *State) FileTransferEnabled() bool { - return s.fileTransferEnabled -} diff --git a/server/internal/websocket/websocket.go b/server/internal/websocket/websocket.go deleted file mode 100644 index e22ab0de..00000000 --- a/server/internal/websocket/websocket.go +++ /dev/null @@ -1,471 +0,0 @@ -package websocket - -import ( - "fmt" - "net/http" - "os" - "sync" - "sync/atomic" - "time" - - "github.com/fsnotify/fsnotify" - "github.com/gorilla/websocket" - "github.com/rs/zerolog" - "github.com/rs/zerolog/log" - - "m1k1o/neko/internal/config" - "m1k1o/neko/internal/types" - "m1k1o/neko/internal/types/event" - "m1k1o/neko/internal/types/message" - "m1k1o/neko/internal/utils" - "m1k1o/neko/internal/websocket/handler" - "m1k1o/neko/internal/websocket/state" -) - -const CONTROL_PROTECTION_SESSION = "by_control_protection" - -func New(sessions types.SessionManager, desktop types.DesktopManager, capture types.CaptureManager, webrtc types.WebRTCManager, conf *config.WebSocket) *WebSocketHandler { - logger := log.With().Str("module", "websocket").Logger() - - state := state.New(conf.FileTransferEnabled, conf.FileTransferPath) - - // if control protection is enabled - if conf.ControlProtection { - state.Lock("control", CONTROL_PROTECTION_SESSION) - logger.Info().Msgf("control locked on behalf of control protection") - } - - // create file transfer directory if not exists - if conf.FileTransferEnabled { - if _, err := os.Stat(conf.FileTransferPath); os.IsNotExist(err) { - err = os.Mkdir(conf.FileTransferPath, os.ModePerm) - logger.Err(err).Msg("creating file transfer directory") - } - } - - // apply default locks - for _, lock := range conf.Locks { - state.Lock(lock, "") // empty session ID - } - - if len(conf.Locks) > 0 { - logger.Info().Msgf("locked resources: %+v", conf.Locks) - } - - handler := handler.New( - sessions, - desktop, - capture, - webrtc, - state, - ) - - return &WebSocketHandler{ - logger: logger, - shutdown: make(chan interface{}), - conf: conf, - sessions: sessions, - desktop: desktop, - webrtc: webrtc, - state: state, - upgrader: websocket.Upgrader{ - CheckOrigin: func(r *http.Request) bool { - return true - }, - }, - handler: handler, - serverStartedAt: time.Now(), - } -} - -// Send pings to peer with this period. Must be less than pongWait. -const pingPeriod = 60 * time.Second - -type WebSocketHandler struct { - logger zerolog.Logger - wg sync.WaitGroup - shutdown chan interface{} - upgrader websocket.Upgrader - sessions types.SessionManager - desktop types.DesktopManager - webrtc types.WebRTCManager - state *state.State - conf *config.WebSocket - handler *handler.MessageHandler - - // stats - conns uint32 - serverStartedAt time.Time - lastAdminLeftAt *time.Time - lastUserLeftAt *time.Time -} - -func (ws *WebSocketHandler) Start() { - go func() { - for { - e, ok := <-ws.sessions.GetEventsChannel() - if !ok { - ws.logger.Info().Msg("session channel was closed") - return - } - - switch e.Type { - case types.SESSION_CREATED: - if err := ws.handler.SessionCreated(e.Id, e.Session); err != nil { - ws.logger.Warn().Str("id", e.Id).Err(err).Msg("session created with and error") - } else { - ws.logger.Debug().Str("id", e.Id).Msg("session created") - } - case types.SESSION_CONNECTED: - if err := ws.handler.SessionConnected(e.Id, e.Session); err != nil { - ws.logger.Warn().Str("id", e.Id).Err(err).Msg("session connected with and error") - } else { - ws.logger.Debug().Str("id", e.Id).Msg("session connected") - } - - // if control protection is enabled and at least one admin - // and if room was locked on behalf control protection, unlock - sess, ok := ws.state.GetLocked("control") - if ok && ws.conf.ControlProtection && sess == CONTROL_PROTECTION_SESSION && len(ws.sessions.Admins()) > 0 { - ws.state.Unlock("control") - ws.sessions.SetControlLocked(false) // TODO: Handle locks in sessions as flags. - ws.logger.Info().Msgf("control unlocked on behalf of control protection") - - if err := ws.sessions.Broadcast( - message.AdminLock{ - Event: event.ADMIN_UNLOCK, - ID: e.Id, - Resource: "control", - }, nil); err != nil { - ws.logger.Warn().Err(err).Msgf("broadcasting event %s has failed", event.ADMIN_UNLOCK) - } - } - - // remove outdated stats - if e.Session.Admin() { - ws.lastAdminLeftAt = nil - } else { - ws.lastUserLeftAt = nil - } - case types.SESSION_DESTROYED: - if err := ws.handler.SessionDestroyed(e.Id); err != nil { - ws.logger.Warn().Str("id", e.Id).Err(err).Msg("session destroyed with and error") - } else { - ws.logger.Debug().Str("id", e.Id).Msg("session destroyed") - } - - membersCount := len(ws.sessions.Members()) - adminCount := len(ws.sessions.Admins()) - - // if control protection is enabled and no admin - // and room is not locked, lock - ok := ws.state.IsLocked("control") - if !ok && ws.conf.ControlProtection && adminCount == 0 { - ws.state.Lock("control", CONTROL_PROTECTION_SESSION) - ws.sessions.SetControlLocked(true) // TODO: Handle locks in sessions as flags. - ws.logger.Info().Msgf("control locked and released on behalf of control protection") - ws.handler.AdminRelease(e.Id, e.Session) - - if err := ws.sessions.Broadcast( - message.AdminLock{ - Event: event.ADMIN_LOCK, - ID: e.Id, - Resource: "control", - }, nil); err != nil { - ws.logger.Warn().Err(err).Msgf("broadcasting event %s has failed", event.ADMIN_LOCK) - } - } - - // if this was the last admin - if e.Session.Admin() && adminCount == 0 { - now := time.Now() - ws.lastAdminLeftAt = &now - } - - // if this was the last user - if !e.Session.Admin() && membersCount-adminCount == 0 { - now := time.Now() - ws.lastUserLeftAt = &now - } - case types.SESSION_HOST_SET: - // TODO: Unused. - case types.SESSION_HOST_CLEARED: - // TODO: Unused. - } - } - }() - - go func() { - for { - _, ok := <-ws.desktop.GetClipboardUpdatedChannel() - if !ok { - ws.logger.Info().Msg("clipboard update channel closed") - return - } - - session, ok := ws.sessions.GetHost() - if !ok { - return - } - - err := session.Send(message.Clipboard{ - Event: event.CONTROL_CLIPBOARD, - Text: ws.desktop.ReadClipboard(), - }) - - ws.logger.Err(err).Msg("sync clipboard") - } - }() - - // watch for file changes and send file list if file transfer is enabled - if ws.conf.FileTransferEnabled { - watcher, err := fsnotify.NewWatcher() - if err != nil { - ws.logger.Err(err).Msg("unable to start file transfer dir watcher") - return - } - - go func() { - for { - select { - case e, ok := <-watcher.Events: - if !ok { - ws.logger.Info().Msg("file transfer dir watcher closed") - return - } - if e.Has(fsnotify.Create) || e.Has(fsnotify.Remove) || e.Has(fsnotify.Rename) { - ws.logger.Debug().Str("event", e.String()).Msg("file transfer dir watcher event") - ws.handler.FileTransferRefresh(nil) - } - case err := <-watcher.Errors: - ws.logger.Err(err).Msg("error in file transfer dir watcher") - } - } - }() - - if err := watcher.Add(ws.conf.FileTransferPath); err != nil { - ws.logger.Err(err).Msg("unable to add file transfer path to watcher") - } - } -} - -func (ws *WebSocketHandler) Shutdown() error { - close(ws.shutdown) - ws.wg.Wait() - return nil -} - -func (ws *WebSocketHandler) Upgrade(w http.ResponseWriter, r *http.Request) error { - ws.logger.Debug().Msg("attempting to upgrade connection") - - id, err := utils.NewUID(32) - if err != nil { - ws.logger.Error().Err(err).Msg("failed to generate user id") - return err - } - - connection, err := ws.upgrader.Upgrade(w, r, nil) - if err != nil { - ws.logger.Error().Err(err).Msg("failed to upgrade connection") - return err - } - - admin, err := ws.authenticate(r) - if err != nil { - ws.logger.Warn().Err(err).Msg("authentication failed") - - if err = connection.WriteJSON(message.SystemMessage{ - Event: event.SYSTEM_DISCONNECT, - Message: "invalid_password", - }); err != nil { - ws.logger.Error().Err(err).Msg("failed to send disconnect") - } - - if err = connection.Close(); err != nil { - return err - } - return nil - } - - socket := &WebSocket{ - id: id, - ws: ws, - address: r.RemoteAddr, - connection: connection, - } - - ok, reason := ws.handler.Connected(admin, socket.Address()) - if !ok { - if err = connection.WriteJSON(message.SystemMessage{ - Event: event.SYSTEM_DISCONNECT, - Message: reason, - }); err != nil { - ws.logger.Error().Err(err).Msg("failed to send disconnect") - } - - if err = connection.Close(); err != nil { - return err - } - - return nil - } - - ws.sessions.New(id, admin, socket) - - ws.logger. - Debug(). - Str("session", id). - Str("address", connection.RemoteAddr().String()). - Msg("new connection created") - - atomic.AddUint32(&ws.conns, uint32(1)) - - defer func() { - ws.logger. - Debug(). - Str("session", id). - Str("address", connection.RemoteAddr().String()). - Msg("session ended") - - atomic.AddUint32(&ws.conns, ^uint32(0)) - }() - - ws.handle(connection, id) - return nil -} - -func (ws *WebSocketHandler) Stats() types.Stats { - host := "" - session, ok := ws.sessions.GetHost() - if ok { - host = session.ID() - } - - return types.Stats{ - Connections: atomic.LoadUint32(&ws.conns), - Host: host, - Members: ws.sessions.Members(), - - Banned: ws.state.AllBanned(), - Locked: ws.state.AllLocked(), - - ServerStartedAt: ws.serverStartedAt, - LastAdminLeftAt: ws.lastAdminLeftAt, - LastUserLeftAt: ws.lastUserLeftAt, - - ControlProtection: ws.conf.ControlProtection, - ImplicitControl: ws.webrtc.ImplicitControl(), - } -} - -func (ws *WebSocketHandler) IsLocked(resource string) bool { - return ws.state.IsLocked(resource) -} - -func (ws *WebSocketHandler) IsAdmin(password string) (bool, error) { - if password == ws.conf.AdminPassword { - return true, nil - } - - if password == ws.conf.Password { - return false, nil - } - - return false, fmt.Errorf("invalid password") -} - -func (ws *WebSocketHandler) authenticate(r *http.Request) (bool, error) { - passwords, ok := r.URL.Query()["password"] - if !ok || len(passwords[0]) < 1 { - return false, fmt.Errorf("no password provided") - } - - return ws.IsAdmin(passwords[0]) -} - -func (ws *WebSocketHandler) handle(connection *websocket.Conn, id string) { - bytes := make(chan []byte) - cancel := make(chan struct{}) - ticker := time.NewTicker(pingPeriod) - - ws.wg.Add(1) - go func() { - defer func() { - ticker.Stop() - ws.logger.Debug().Str("address", connection.RemoteAddr().String()).Msg("handle socket ending") - ws.handler.Disconnected(id) - ws.wg.Done() - }() - - for { - _, raw, err := connection.ReadMessage() - if err != nil { - if websocket.IsUnexpectedCloseError(err, websocket.CloseGoingAway, websocket.CloseAbnormalClosure) { - ws.logger.Warn().Err(err).Msg("read message error") - } else { - ws.logger.Debug().Err(err).Msg("read message error") - } - close(cancel) - break - } - bytes <- raw - } - }() - - for { - select { - case raw := <-bytes: - ws.logger.Debug(). - Str("session", id). - Str("address", connection.RemoteAddr().String()). - Str("raw", string(raw)). - Msg("received message from client") - if err := ws.handler.Message(id, raw); err != nil { - ws.logger.Error().Err(err).Msg("message handler has failed") - } - case <-ws.shutdown: - if err := connection.WriteJSON(message.SystemMessage{ - Event: event.SYSTEM_DISCONNECT, - Message: "server_shutdown", - }); err != nil { - ws.logger.Err(err).Msg("failed to send disconnect") - } - - if err := connection.Close(); err != nil { - ws.logger.Err(err).Msg("connection closed with an error") - } - return - case <-cancel: - return - case <-ticker.C: - if err := connection.WriteMessage(websocket.PingMessage, nil); err != nil { - return - } - } - } -} - -// -// File transfer -// - -func (ws *WebSocketHandler) CanTransferFiles(password string) (bool, error) { - if !ws.conf.FileTransferEnabled { - return false, nil - } - - isAdmin, err := ws.IsAdmin(password) - if err != nil { - return false, err - } - - return isAdmin || !ws.state.IsLocked("file_transfer"), nil -} - -func (ws *WebSocketHandler) FileTransferPath(filename string) string { - return ws.state.FileTransferPath(filename) -} - -func (ws *WebSocketHandler) FileTransferEnabled() bool { - return ws.conf.FileTransferEnabled -} diff --git a/server/neko.go b/server/neko.go index ab6189a9..d25ee6a7 100644 --- a/server/neko.go +++ b/server/neko.go @@ -2,25 +2,11 @@ package neko import ( "fmt" - "os" - "os/signal" "runtime" "strings" - - "m1k1o/neko/internal/capture" - "m1k1o/neko/internal/config" - "m1k1o/neko/internal/desktop" - "m1k1o/neko/internal/http" - "m1k1o/neko/internal/session" - "m1k1o/neko/internal/webrtc" - "m1k1o/neko/internal/websocket" - - "github.com/rs/zerolog" - "github.com/rs/zerolog/log" - "github.com/spf13/cobra" ) -const Header = `&34 +const Header = `&34 _ __ __ / | / /__ / /______ \ /\ / |/ / _ \/ //_/ __ \ ) ( ') @@ -40,29 +26,17 @@ var ( gitTag = "dev" ) -var Service *Neko - -func init() { - Service = &Neko{ - Version: &Version{ - GitCommit: gitCommit, - GitBranch: gitBranch, - GitTag: gitTag, - BuildDate: buildDate, - GoVersion: runtime.Version(), - Compiler: runtime.Compiler, - Platform: fmt.Sprintf("%s/%s", runtime.GOOS, runtime.GOARCH), - }, - Root: &config.Root{}, - Server: &config.Server{}, - Capture: &config.Capture{}, - Desktop: &config.Desktop{}, - WebRTC: &config.WebRTC{}, - WebSocket: &config.WebSocket{}, - } +var Version = &version{ + GitCommit: gitCommit, + GitBranch: gitBranch, + GitTag: gitTag, + BuildDate: buildDate, + GoVersion: runtime.Version(), + Compiler: runtime.Compiler, + Platform: fmt.Sprintf("%s/%s", runtime.GOOS, runtime.GOARCH), } -type Version struct { +type version struct { GitCommit string GitBranch string GitTag string @@ -72,7 +46,7 @@ type Version struct { Platform string } -func (i *Version) String() string { +func (i *version) String() string { version := i.GitTag if version == "" || version == "dev" { version = i.GitBranch @@ -81,7 +55,7 @@ func (i *Version) String() string { return fmt.Sprintf("%s@%s", version, i.GitCommit) } -func (i *Version) Details() string { +func (i *version) Details() string { return "\n" + strings.Join([]string{ fmt.Sprintf("Version %s", i.String()), fmt.Sprintf("GitCommit %s", i.GitCommit), @@ -93,84 +67,3 @@ func (i *Version) Details() string { fmt.Sprintf("Platform %s", i.Platform), }, "\n") + "\n" } - -type Neko struct { - Version *Version - Root *config.Root - Capture *config.Capture - Desktop *config.Desktop - Server *config.Server - WebRTC *config.WebRTC - WebSocket *config.WebSocket - - logger zerolog.Logger - server *http.Server - sessionManager *session.SessionManager - captureManager *capture.CaptureManagerCtx - desktopManager *desktop.DesktopManagerCtx - webRTCManager *webrtc.WebRTCManager - webSocketHandler *websocket.WebSocketHandler -} - -func (neko *Neko) Preflight() { - neko.logger = log.With().Str("service", "neko").Logger() -} - -func (neko *Neko) Start() { - desktopManager := desktop.New(neko.Desktop) - desktopManager.Start() - - captureManager := capture.New(desktopManager, neko.Capture) - captureManager.Start() - - sessionManager := session.New(captureManager) - - webRTCManager := webrtc.New(sessionManager, captureManager, desktopManager, neko.WebRTC) - webRTCManager.Start() - - webSocketHandler := websocket.New(sessionManager, desktopManager, captureManager, webRTCManager, neko.WebSocket) - webSocketHandler.Start() - - server := http.New(neko.Server, webSocketHandler, desktopManager) - server.Start() - - neko.sessionManager = sessionManager - neko.captureManager = captureManager - neko.desktopManager = desktopManager - neko.webRTCManager = webRTCManager - neko.webSocketHandler = webSocketHandler - neko.server = server -} - -func (neko *Neko) Shutdown() { - var err error - - err = neko.server.Shutdown() - neko.logger.Err(err).Msg("server shutdown") - - err = neko.webSocketHandler.Shutdown() - neko.logger.Err(err).Msg("websocket handler shutdown") - - err = neko.webRTCManager.Shutdown() - neko.logger.Err(err).Msg("webrtc manager shutdown") - - err = neko.captureManager.Shutdown() - neko.logger.Err(err).Msg("capture manager shutdown") - - err = neko.desktopManager.Shutdown() - neko.logger.Err(err).Msg("desktop manager shutdown") -} - -func (neko *Neko) ServeCommand(cmd *cobra.Command, args []string) { - neko.logger.Info().Msg("starting neko server") - neko.Start() - neko.logger.Info().Msg("neko ready") - - quit := make(chan os.Signal, 1) - signal.Notify(quit, os.Interrupt) - sig := <-quit - - neko.logger.Warn().Msgf("received %s, attempting graceful shutdown", sig) - neko.Shutdown() - neko.logger.Info().Msg("shutdown complete") -} diff --git a/openapi.yaml b/server/openapi.yaml similarity index 92% rename from openapi.yaml rename to server/openapi.yaml index 2fa9e787..974b2760 100644 --- a/openapi.yaml +++ b/server/openapi.yaml @@ -13,8 +13,8 @@ servers: url: http://localhost:3000 tags: - - name: session - description: Session management. + - name: sessions + description: Sessions management. - name: room description: Room releated operations. - name: members @@ -61,13 +61,11 @@ paths: required: true # - # session + # current session # /api/login: post: - tags: - - session summary: login operationId: login security: [] @@ -90,8 +88,6 @@ paths: required: true /api/logout: post: - tags: - - session summary: logout operationId: logout responses: @@ -101,8 +97,6 @@ paths: $ref: '#/components/responses/Unauthorized' /api/whoami: get: - tags: - - session summary: whoami operationId: whoami responses: @@ -116,10 +110,30 @@ paths: $ref: '#/components/responses/Unauthorized' '403': $ref: '#/components/responses/Forbidden' + /api/profile: + post: + summary: update current profile without syncing it with member profile (experimental) + operationId: profile + responses: + '204': + description: OK + '401': + $ref: '#/components/responses/Unauthorized' + requestBody: + content: + application/json: + schema: + $ref: '#/components/schemas/MemberProfile' + required: true + + # + # sessions + # + /api/sessions: get: tags: - - session + - sessions summary: get sessions operationId: sessionsGet responses: @@ -135,6 +149,75 @@ paths: $ref: '#/components/responses/Unauthorized' '403': $ref: '#/components/responses/Forbidden' + /api/sessions/{sessionId}: + get: + tags: + - sessions + summary: get session + operationId: sessionGet + parameters: + - in: path + name: sessionId + description: session identifier + required: true + schema: + type: string + responses: + '200': + description: OK + content: + application/json: + schema: + $ref: '#/components/schemas/SessionData' + '401': + $ref: '#/components/responses/Unauthorized' + '403': + $ref: '#/components/responses/Forbidden' + '404': + $ref: '#/components/responses/NotFound' + delete: + tags: + - sessions + summary: remove session + operationId: sessionRemove + parameters: + - in: path + name: sessionId + description: session identifier + required: true + schema: + type: string + responses: + '204': + description: OK + '401': + $ref: '#/components/responses/Unauthorized' + '403': + $ref: '#/components/responses/Forbidden' + '404': + $ref: '#/components/responses/NotFound' + /api/sessions/{sessionId}/disconnect: + post: + tags: + - sessions + summary: disconnect session + operationId: sessionDisconnect + parameters: + - in: path + name: sessionId + description: session identifier + required: true + schema: + type: string + responses: + '204': + description: OK + '401': + $ref: '#/components/responses/Unauthorized' + '403': + $ref: '#/components/responses/Forbidden' + '404': + $ref: '#/components/responses/NotFound' # # room @@ -197,7 +280,7 @@ paths: tags: - room summary: start broadcast - operationId: boradcastStart + operationId: broadcastStart responses: '204': description: OK @@ -234,7 +317,7 @@ paths: tags: - room summary: stop broadcast - operationId: boradcastStop + operationId: broadcastStop responses: '204': description: OK @@ -1023,7 +1106,7 @@ components: type: integer # - # session + # sessions # SessionLogin: diff --git a/pkg/auth/auth.go b/server/pkg/auth/auth.go similarity index 90% rename from pkg/auth/auth.go rename to server/pkg/auth/auth.go index e578e05b..e3e6ddd4 100644 --- a/pkg/auth/auth.go +++ b/server/pkg/auth/auth.go @@ -40,6 +40,15 @@ func HostsOnly(w http.ResponseWriter, r *http.Request) (context.Context, error) return nil, nil } +func HostsOrAdminsOnly(w http.ResponseWriter, r *http.Request) (context.Context, error) { + session, ok := GetSession(r) + if !ok || (!session.IsHost() && !session.Profile().IsAdmin) { + return nil, utils.HttpForbidden("session is not host or admin") + } + + return nil, nil +} + func CanWatchOnly(w http.ResponseWriter, r *http.Request) (context.Context, error) { session, ok := GetSession(r) if !ok || !session.Profile().CanWatch { diff --git a/pkg/auth/auth_test.go b/server/pkg/auth/auth_test.go similarity index 97% rename from pkg/auth/auth_test.go rename to server/pkg/auth/auth_test.go index fa9b89e3..dd22d472 100644 --- a/pkg/auth/auth_test.go +++ b/server/pkg/auth/auth_test.go @@ -97,7 +97,7 @@ func TestHostsOnly(t *testing.T) { } // r2 is host - sessionManager.SetHost(session) + session.SetAsHost() r3, _, err := rWithSession(types.MemberProfile{CanHost: false}) if err != nil { @@ -224,9 +224,11 @@ func TestCanHostOnly(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - settings := sessionManager.Settings() - settings.PrivateMode = tt.privateMode - sessionManager.UpdateSettings(settings) + session, _ := GetSession(tt.r) + sessionManager.UpdateSettingsFunc(session, func(s *types.Settings) bool { + s.PrivateMode = tt.privateMode + return true + }) _, err := CanHostOnly(nil, tt.r) if (err != nil) != tt.wantErr { diff --git a/pkg/drop/drop.c b/server/pkg/drop/drop.c similarity index 100% rename from pkg/drop/drop.c rename to server/pkg/drop/drop.c diff --git a/pkg/drop/drop.go b/server/pkg/drop/drop.go similarity index 100% rename from pkg/drop/drop.go rename to server/pkg/drop/drop.go diff --git a/pkg/drop/drop.h b/server/pkg/drop/drop.h similarity index 100% rename from pkg/drop/drop.h rename to server/pkg/drop/drop.h diff --git a/pkg/gst/gst.c b/server/pkg/gst/gst.c similarity index 100% rename from pkg/gst/gst.c rename to server/pkg/gst/gst.c diff --git a/pkg/gst/gst.go b/server/pkg/gst/gst.go similarity index 100% rename from pkg/gst/gst.go rename to server/pkg/gst/gst.go diff --git a/pkg/gst/gst.h b/server/pkg/gst/gst.h similarity index 100% rename from pkg/gst/gst.h rename to server/pkg/gst/gst.h diff --git a/pkg/types/api.go b/server/pkg/types/api.go similarity index 100% rename from pkg/types/api.go rename to server/pkg/types/api.go diff --git a/pkg/types/capture.go b/server/pkg/types/capture.go similarity index 100% rename from pkg/types/capture.go rename to server/pkg/types/capture.go diff --git a/pkg/types/codec/codecs.go b/server/pkg/types/codec/codecs.go similarity index 90% rename from pkg/types/codec/codecs.go rename to server/pkg/types/codec/codecs.go index 07f51818..372975a2 100644 --- a/pkg/types/codec/codecs.go +++ b/server/pkg/types/codec/codecs.go @@ -31,6 +31,8 @@ func ParseStr(codecName string) (codec RTPCodec, ok bool) { codec = VP8() case VP9().Name: codec = VP9() + case AV1().Name: + codec = AV1() case H264().Name: codec = H264() case Opus().Name: @@ -134,6 +136,25 @@ func H264() RTPCodec { } } +// TODO: Profile ID. +func AV1() RTPCodec { + return RTPCodec{ + Name: "av1", + PayloadType: 96, + Type: webrtc.RTPCodecTypeVideo, + Capability: webrtc.RTPCodecCapability{ + MimeType: webrtc.MimeTypeAV1, + ClockRate: 90000, + Channels: 0, + SDPFmtpLine: "", + RTCPFeedback: RTCPFeedback, + }, + // https://gstreamer.freedesktop.org/documentation/av1/av1enc.html + // gstreamer1.0-plugins-bad + Pipeline: "av1enc target-bitrate=4096 cpu-used=4 end-usage=cbr undershoot=95 keyframe-max-dist=15 min-quantizer=4 max-quantizer=20", + } +} + func Opus() RTPCodec { return RTPCodec{ Name: "opus", diff --git a/pkg/types/desktop.go b/server/pkg/types/desktop.go similarity index 96% rename from pkg/types/desktop.go rename to server/pkg/types/desktop.go index b7699096..0cc4cae9 100644 --- a/pkg/types/desktop.go +++ b/server/pkg/types/desktop.go @@ -15,9 +15,9 @@ type CursorImage struct { } type ScreenSize struct { - Width int - Height int - Rate int16 + Width int `json:"width"` + Height int `json:"height"` + Rate int16 `json:"rate"` } func (s ScreenSize) String() string { diff --git a/pkg/types/event/events.go b/server/pkg/types/event/events.go similarity index 98% rename from pkg/types/event/events.go rename to server/pkg/types/event/events.go index 32857b9e..0259d4d7 100644 --- a/pkg/types/event/events.go +++ b/server/pkg/types/event/events.go @@ -70,7 +70,7 @@ const ( ) const ( - BORADCAST_STATUS = "broadcast/status" + BROADCAST_STATUS = "broadcast/status" ) const ( diff --git a/pkg/types/http.go b/server/pkg/types/http.go similarity index 100% rename from pkg/types/http.go rename to server/pkg/types/http.go diff --git a/pkg/types/member.go b/server/pkg/types/member.go similarity index 97% rename from pkg/types/member.go rename to server/pkg/types/member.go index 4463f4dc..e3558060 100644 --- a/pkg/types/member.go +++ b/server/pkg/types/member.go @@ -23,7 +23,7 @@ type MemberProfile struct { CanSeeInactiveCursors bool `json:"can_see_inactive_cursors" mapstructure:"can_see_inactive_cursors"` // plugin scope - Plugins map[string]any `json:"plugins"` + Plugins PluginSettings `json:"plugins"` } type MemberProvider interface { diff --git a/pkg/types/message/messages.go b/server/pkg/types/message/messages.go similarity index 90% rename from pkg/types/message/messages.go rename to server/pkg/types/message/messages.go index c2121dee..a35c07d7 100644 --- a/pkg/types/message/messages.go +++ b/server/pkg/types/message/messages.go @@ -17,7 +17,7 @@ type SystemWebRTC struct { type SystemInit struct { SessionId string `json:"session_id"` ControlHost ControlHost `json:"control_host"` - ScreenSize ScreenSize `json:"screen_size"` + ScreenSize types.ScreenSize `json:"screen_size"` Sessions map[string]SessionData `json:"sessions"` Settings types.Settings `json:"settings"` TouchEvents bool `json:"touch_events"` @@ -26,8 +26,8 @@ type SystemInit struct { } type SystemAdmin struct { - ScreenSizesList []ScreenSize `json:"screen_sizes_list"` - BroadcastStatus BroadcastStatus `json:"broadcast_status"` + ScreenSizesList []types.ScreenSize `json:"screen_sizes_list"` + BroadcastStatus BroadcastStatus `json:"broadcast_status"` } type SystemLogs = []SystemLog @@ -42,6 +42,11 @@ type SystemDisconnect struct { Message string `json:"message"` } +type SystemSettingsUpdate struct { + ID string `json:"id"` + types.Settings +} + ///////////////////////////// // Signal ///////////////////////////// @@ -111,6 +116,7 @@ type SessionCursors struct { ///////////////////////////// type ControlHost struct { + ID string `json:"id"` HasHost bool `json:"has_host"` HostID string `json:"host_id,omitempty"` } @@ -151,9 +157,12 @@ type ControlTouch struct { ///////////////////////////// type ScreenSize struct { - Width int `json:"width"` - Height int `json:"height"` - Rate int16 `json:"rate"` + types.ScreenSize +} + +type ScreenSizeUpdate struct { + ID string `json:"id"` + types.ScreenSize } ///////////////////////////// diff --git a/pkg/types/plugins.go b/server/pkg/types/plugins.go similarity index 59% rename from pkg/types/plugins.go rename to server/pkg/types/plugins.go index fa57b8f1..c94131ef 100644 --- a/pkg/types/plugins.go +++ b/server/pkg/types/plugins.go @@ -2,10 +2,17 @@ package types import ( "errors" + "fmt" + "strings" + "github.com/demodesk/neko/pkg/utils" "github.com/spf13/cobra" ) +var ( + ErrPluginSettingsNotFound = errors.New("plugin settings not found") +) + type Plugin interface { Name() string Config() PluginConfig @@ -61,3 +68,26 @@ func (p *PluginManagers) Validate() error { return nil } + +type PluginSettings map[string]any + +func (p PluginSettings) Unmarshal(name string, def any) error { + if p == nil { + return fmt.Errorf("%w: %s", ErrPluginSettingsNotFound, name) + } + + // loop through the plugin settings and take only the one that starts with the name + // because the settings are stored in a map["plugin_name.setting_name"] = value + newMap := make(map[string]any) + for k, v := range p { + if strings.HasPrefix(k, name+".") { + newMap[strings.TrimPrefix(k, name+".")] = v + } + } + + if len(newMap) == 0 { + return fmt.Errorf("%w: %s", ErrPluginSettingsNotFound, name) + } + + return utils.Decode(newMap, def) +} diff --git a/pkg/types/session.go b/server/pkg/types/session.go similarity index 82% rename from pkg/types/session.go rename to server/pkg/types/session.go index e901ad7a..e03ef235 100644 --- a/pkg/types/session.go +++ b/server/pkg/types/session.go @@ -11,6 +11,7 @@ var ( ErrSessionAlreadyExists = errors.New("session already exists") ErrSessionAlreadyConnected = errors.New("session is already connected") ErrSessionLoginDisabled = errors.New("session login disabled") + ErrSessionLoginsLocked = errors.New("session logins locked") ) type Cursor struct { @@ -40,13 +41,15 @@ type SessionState struct { type Settings struct { PrivateMode bool `json:"private_mode"` + LockedLogins bool `json:"locked_logins"` LockedControls bool `json:"locked_controls"` + ControlProtection bool `json:"control_protection"` ImplicitHosting bool `json:"implicit_hosting"` InactiveCursors bool `json:"inactive_cursors"` MercifulReconnect bool `json:"merciful_reconnect"` // plugin scope - Plugins map[string]any `json:"plugins"` + Plugins PluginSettings `json:"plugins"` } type Session interface { @@ -54,6 +57,10 @@ type Session interface { Profile() MemberProfile State() SessionState IsHost() bool + LegacyIsHost() bool + SetAsHost() + SetAsHostBy(session Session) + ClearHost() PrivateModeEnabled() bool // cursor @@ -75,13 +82,13 @@ type SessionManager interface { Create(id string, profile MemberProfile) (Session, string, error) Update(id string, profile MemberProfile) error Delete(id string) error + Disconnect(id string) error Get(id string) (Session, bool) GetByToken(token string) (Session, bool) List() []Session + Range(func(Session) bool) - SetHost(host Session) GetHost() (Session, bool) - ClearHost() SetCursor(cursor Cursor, session Session) PopCursors() map[Session][]Cursor @@ -94,12 +101,12 @@ type SessionManager interface { OnDeleted(listener func(session Session)) OnConnected(listener func(session Session)) OnDisconnected(listener func(session Session)) - OnProfileChanged(listener func(session Session)) + OnProfileChanged(listener func(session Session, new, old MemberProfile)) OnStateChanged(listener func(session Session)) - OnHostChanged(listener func(session Session)) - OnSettingsChanged(listener func(new Settings, old Settings)) + OnHostChanged(listener func(session, host Session)) + OnSettingsChanged(listener func(session Session, new, old Settings)) - UpdateSettings(Settings) + UpdateSettingsFunc(session Session, f func(settings *Settings) bool) Settings() Settings CookieEnabled() bool diff --git a/pkg/types/webrtc.go b/server/pkg/types/webrtc.go similarity index 100% rename from pkg/types/webrtc.go rename to server/pkg/types/webrtc.go diff --git a/pkg/types/websocket.go b/server/pkg/types/websocket.go similarity index 100% rename from pkg/types/websocket.go rename to server/pkg/types/websocket.go diff --git a/pkg/utils/array.go b/server/pkg/utils/array.go similarity index 100% rename from pkg/utils/array.go rename to server/pkg/utils/array.go diff --git a/pkg/utils/color.go b/server/pkg/utils/color.go similarity index 100% rename from pkg/utils/color.go rename to server/pkg/utils/color.go diff --git a/pkg/utils/json.go b/server/pkg/utils/deocde.go similarity index 81% rename from pkg/utils/json.go rename to server/pkg/utils/deocde.go index ff4883b0..12aeaec7 100644 --- a/pkg/utils/json.go +++ b/server/pkg/utils/deocde.go @@ -3,8 +3,14 @@ package utils import ( "encoding/json" "reflect" + + "github.com/mitchellh/mapstructure" ) +func Decode(input interface{}, output interface{}) error { + return mapstructure.Decode(input, output) +} + func Unmarshal(in any, raw []byte, callback func() error) error { if err := json.Unmarshal(raw, &in); err != nil { return err diff --git a/pkg/utils/http.go b/server/pkg/utils/http.go similarity index 100% rename from pkg/utils/http.go rename to server/pkg/utils/http.go diff --git a/pkg/utils/image.go b/server/pkg/utils/image.go similarity index 100% rename from pkg/utils/image.go rename to server/pkg/utils/image.go diff --git a/pkg/utils/request.go b/server/pkg/utils/request.go similarity index 100% rename from pkg/utils/request.go rename to server/pkg/utils/request.go diff --git a/pkg/utils/trenddetector.go b/server/pkg/utils/trenddetector.go similarity index 100% rename from pkg/utils/trenddetector.go rename to server/pkg/utils/trenddetector.go diff --git a/pkg/utils/uid.go b/server/pkg/utils/uid.go similarity index 100% rename from pkg/utils/uid.go rename to server/pkg/utils/uid.go diff --git a/pkg/utils/zip.go b/server/pkg/utils/zip.go similarity index 100% rename from pkg/utils/zip.go rename to server/pkg/utils/zip.go diff --git a/pkg/xevent/xevent.c b/server/pkg/xevent/xevent.c similarity index 100% rename from pkg/xevent/xevent.c rename to server/pkg/xevent/xevent.c diff --git a/pkg/xevent/xevent.go b/server/pkg/xevent/xevent.go similarity index 100% rename from pkg/xevent/xevent.go rename to server/pkg/xevent/xevent.go diff --git a/pkg/xevent/xevent.h b/server/pkg/xevent/xevent.h similarity index 100% rename from pkg/xevent/xevent.h rename to server/pkg/xevent/xevent.h diff --git a/pkg/xinput/dummy.go b/server/pkg/xinput/dummy.go similarity index 100% rename from pkg/xinput/dummy.go rename to server/pkg/xinput/dummy.go diff --git a/pkg/xinput/types.go b/server/pkg/xinput/types.go similarity index 100% rename from pkg/xinput/types.go rename to server/pkg/xinput/types.go diff --git a/pkg/xinput/xinput.go b/server/pkg/xinput/xinput.go similarity index 100% rename from pkg/xinput/xinput.go rename to server/pkg/xinput/xinput.go diff --git a/pkg/xorg/keysymdef.go b/server/pkg/xorg/keysymdef.go similarity index 100% rename from pkg/xorg/keysymdef.go rename to server/pkg/xorg/keysymdef.go diff --git a/pkg/xorg/keysymdef.sh b/server/pkg/xorg/keysymdef.sh similarity index 100% rename from pkg/xorg/keysymdef.sh rename to server/pkg/xorg/keysymdef.sh diff --git a/pkg/xorg/xorg.c b/server/pkg/xorg/xorg.c similarity index 100% rename from pkg/xorg/xorg.c rename to server/pkg/xorg/xorg.c diff --git a/pkg/xorg/xorg.go b/server/pkg/xorg/xorg.go similarity index 100% rename from pkg/xorg/xorg.go rename to server/pkg/xorg/xorg.go diff --git a/pkg/xorg/xorg.h b/server/pkg/xorg/xorg.h similarity index 100% rename from pkg/xorg/xorg.h rename to server/pkg/xorg/xorg.h diff --git a/plugins/.gitkeep b/server/plugins/.gitkeep similarity index 100% rename from plugins/.gitkeep rename to server/plugins/.gitkeep diff --git a/runtime/.Xresources b/server/runtime/.Xresources similarity index 100% rename from runtime/.Xresources rename to server/runtime/.Xresources diff --git a/runtime/dbus b/server/runtime/dbus similarity index 100% rename from runtime/dbus rename to server/runtime/dbus diff --git a/runtime/default.pa b/server/runtime/default.pa similarity index 100% rename from runtime/default.pa rename to server/runtime/default.pa diff --git a/runtime/fontconfig/75-emoji.conf b/server/runtime/fontconfig/75-emoji.conf similarity index 100% rename from runtime/fontconfig/75-emoji.conf rename to server/runtime/fontconfig/75-emoji.conf diff --git a/runtime/fonts/.gitkeep b/server/runtime/fonts/.gitkeep similarity index 100% rename from runtime/fonts/.gitkeep rename to server/runtime/fonts/.gitkeep diff --git a/runtime/icon-theme/.gitkeep b/server/runtime/icon-theme/.gitkeep similarity index 100% rename from runtime/icon-theme/.gitkeep rename to server/runtime/icon-theme/.gitkeep diff --git a/runtime/supervisord.conf b/server/runtime/supervisord.conf similarity index 100% rename from runtime/supervisord.conf rename to server/runtime/supervisord.conf diff --git a/runtime/supervisord.dbus.conf b/server/runtime/supervisord.dbus.conf similarity index 100% rename from runtime/supervisord.dbus.conf rename to server/runtime/supervisord.dbus.conf diff --git a/runtime/xorg.conf b/server/runtime/xorg.conf similarity index 100% rename from runtime/xorg.conf rename to server/runtime/xorg.conf diff --git a/xorg/xf86-input-neko/.gitignore b/server/xorg/xf86-input-neko/.gitignore similarity index 100% rename from xorg/xf86-input-neko/.gitignore rename to server/xorg/xf86-input-neko/.gitignore diff --git a/xorg/xf86-input-neko/80-neko.conf b/server/xorg/xf86-input-neko/80-neko.conf similarity index 100% rename from xorg/xf86-input-neko/80-neko.conf rename to server/xorg/xf86-input-neko/80-neko.conf diff --git a/xorg/xf86-input-neko/COPYING b/server/xorg/xf86-input-neko/COPYING similarity index 100% rename from xorg/xf86-input-neko/COPYING rename to server/xorg/xf86-input-neko/COPYING diff --git a/xorg/xf86-input-neko/Dockerfile b/server/xorg/xf86-input-neko/Dockerfile similarity index 100% rename from xorg/xf86-input-neko/Dockerfile rename to server/xorg/xf86-input-neko/Dockerfile diff --git a/xorg/xf86-input-neko/Makefile.am b/server/xorg/xf86-input-neko/Makefile.am similarity index 100% rename from xorg/xf86-input-neko/Makefile.am rename to server/xorg/xf86-input-neko/Makefile.am diff --git a/xorg/xf86-input-neko/README.md b/server/xorg/xf86-input-neko/README.md similarity index 100% rename from xorg/xf86-input-neko/README.md rename to server/xorg/xf86-input-neko/README.md diff --git a/xorg/xf86-input-neko/autogen-clean.sh b/server/xorg/xf86-input-neko/autogen-clean.sh similarity index 100% rename from xorg/xf86-input-neko/autogen-clean.sh rename to server/xorg/xf86-input-neko/autogen-clean.sh diff --git a/xorg/xf86-input-neko/autogen.sh b/server/xorg/xf86-input-neko/autogen.sh similarity index 100% rename from xorg/xf86-input-neko/autogen.sh rename to server/xorg/xf86-input-neko/autogen.sh diff --git a/xorg/xf86-input-neko/configure.ac b/server/xorg/xf86-input-neko/configure.ac similarity index 100% rename from xorg/xf86-input-neko/configure.ac rename to server/xorg/xf86-input-neko/configure.ac diff --git a/xorg/xf86-input-neko/m4/.gitkeep b/server/xorg/xf86-input-neko/m4/.gitkeep similarity index 100% rename from xorg/xf86-input-neko/m4/.gitkeep rename to server/xorg/xf86-input-neko/m4/.gitkeep diff --git a/xorg/xf86-input-neko/release.sh b/server/xorg/xf86-input-neko/release.sh similarity index 100% rename from xorg/xf86-input-neko/release.sh rename to server/xorg/xf86-input-neko/release.sh diff --git a/xorg/xf86-input-neko/src/Makefile.am b/server/xorg/xf86-input-neko/src/Makefile.am similarity index 100% rename from xorg/xf86-input-neko/src/Makefile.am rename to server/xorg/xf86-input-neko/src/Makefile.am diff --git a/xorg/xf86-input-neko/src/neko.c b/server/xorg/xf86-input-neko/src/neko.c similarity index 100% rename from xorg/xf86-input-neko/src/neko.c rename to server/xorg/xf86-input-neko/src/neko.c diff --git a/xorg/xf86-input-neko/xorg-neko.pc.in b/server/xorg/xf86-input-neko/xorg-neko.pc.in similarity index 100% rename from xorg/xf86-input-neko/xorg-neko.pc.in rename to server/xorg/xf86-input-neko/xorg-neko.pc.in diff --git a/xorg/xf86-video-dummy/01_v0.3.8_xdummy-randr.patch b/server/xorg/xf86-video-dummy/01_v0.3.8_xdummy-randr.patch similarity index 100% rename from xorg/xf86-video-dummy/01_v0.3.8_xdummy-randr.patch rename to server/xorg/xf86-video-dummy/01_v0.3.8_xdummy-randr.patch diff --git a/xorg/xf86-video-dummy/README.md b/server/xorg/xf86-video-dummy/README.md similarity index 100% rename from xorg/xf86-video-dummy/README.md rename to server/xorg/xf86-video-dummy/README.md diff --git a/xorg/xf86-video-dummy/v0.3.8/COPYING b/server/xorg/xf86-video-dummy/v0.3.8/COPYING similarity index 100% rename from xorg/xf86-video-dummy/v0.3.8/COPYING rename to server/xorg/xf86-video-dummy/v0.3.8/COPYING diff --git a/xorg/xf86-video-dummy/v0.3.8/ChangeLog b/server/xorg/xf86-video-dummy/v0.3.8/ChangeLog similarity index 100% rename from xorg/xf86-video-dummy/v0.3.8/ChangeLog rename to server/xorg/xf86-video-dummy/v0.3.8/ChangeLog diff --git a/xorg/xf86-video-dummy/v0.3.8/Makefile.am b/server/xorg/xf86-video-dummy/v0.3.8/Makefile.am similarity index 100% rename from xorg/xf86-video-dummy/v0.3.8/Makefile.am rename to server/xorg/xf86-video-dummy/v0.3.8/Makefile.am diff --git a/xorg/xf86-video-dummy/v0.3.8/Makefile.in b/server/xorg/xf86-video-dummy/v0.3.8/Makefile.in similarity index 100% rename from xorg/xf86-video-dummy/v0.3.8/Makefile.in rename to server/xorg/xf86-video-dummy/v0.3.8/Makefile.in diff --git a/xorg/xf86-video-dummy/v0.3.8/README b/server/xorg/xf86-video-dummy/v0.3.8/README similarity index 100% rename from xorg/xf86-video-dummy/v0.3.8/README rename to server/xorg/xf86-video-dummy/v0.3.8/README diff --git a/xorg/xf86-video-dummy/v0.3.8/aclocal.m4 b/server/xorg/xf86-video-dummy/v0.3.8/aclocal.m4 similarity index 100% rename from xorg/xf86-video-dummy/v0.3.8/aclocal.m4 rename to server/xorg/xf86-video-dummy/v0.3.8/aclocal.m4 diff --git a/xorg/xf86-video-dummy/v0.3.8/compile b/server/xorg/xf86-video-dummy/v0.3.8/compile similarity index 100% rename from xorg/xf86-video-dummy/v0.3.8/compile rename to server/xorg/xf86-video-dummy/v0.3.8/compile diff --git a/xorg/xf86-video-dummy/v0.3.8/config.guess b/server/xorg/xf86-video-dummy/v0.3.8/config.guess similarity index 100% rename from xorg/xf86-video-dummy/v0.3.8/config.guess rename to server/xorg/xf86-video-dummy/v0.3.8/config.guess diff --git a/xorg/xf86-video-dummy/v0.3.8/config.h.in b/server/xorg/xf86-video-dummy/v0.3.8/config.h.in similarity index 100% rename from xorg/xf86-video-dummy/v0.3.8/config.h.in rename to server/xorg/xf86-video-dummy/v0.3.8/config.h.in diff --git a/xorg/xf86-video-dummy/v0.3.8/config.sub b/server/xorg/xf86-video-dummy/v0.3.8/config.sub similarity index 100% rename from xorg/xf86-video-dummy/v0.3.8/config.sub rename to server/xorg/xf86-video-dummy/v0.3.8/config.sub diff --git a/xorg/xf86-video-dummy/v0.3.8/configure b/server/xorg/xf86-video-dummy/v0.3.8/configure similarity index 100% rename from xorg/xf86-video-dummy/v0.3.8/configure rename to server/xorg/xf86-video-dummy/v0.3.8/configure diff --git a/xorg/xf86-video-dummy/v0.3.8/configure.ac b/server/xorg/xf86-video-dummy/v0.3.8/configure.ac similarity index 100% rename from xorg/xf86-video-dummy/v0.3.8/configure.ac rename to server/xorg/xf86-video-dummy/v0.3.8/configure.ac diff --git a/xorg/xf86-video-dummy/v0.3.8/depcomp b/server/xorg/xf86-video-dummy/v0.3.8/depcomp similarity index 100% rename from xorg/xf86-video-dummy/v0.3.8/depcomp rename to server/xorg/xf86-video-dummy/v0.3.8/depcomp diff --git a/xorg/xf86-video-dummy/v0.3.8/install-sh b/server/xorg/xf86-video-dummy/v0.3.8/install-sh similarity index 100% rename from xorg/xf86-video-dummy/v0.3.8/install-sh rename to server/xorg/xf86-video-dummy/v0.3.8/install-sh diff --git a/xorg/xf86-video-dummy/v0.3.8/ltmain.sh b/server/xorg/xf86-video-dummy/v0.3.8/ltmain.sh similarity index 100% rename from xorg/xf86-video-dummy/v0.3.8/ltmain.sh rename to server/xorg/xf86-video-dummy/v0.3.8/ltmain.sh diff --git a/xorg/xf86-video-dummy/v0.3.8/missing b/server/xorg/xf86-video-dummy/v0.3.8/missing similarity index 100% rename from xorg/xf86-video-dummy/v0.3.8/missing rename to server/xorg/xf86-video-dummy/v0.3.8/missing diff --git a/xorg/xf86-video-dummy/v0.3.8/src/Makefile.am b/server/xorg/xf86-video-dummy/v0.3.8/src/Makefile.am similarity index 100% rename from xorg/xf86-video-dummy/v0.3.8/src/Makefile.am rename to server/xorg/xf86-video-dummy/v0.3.8/src/Makefile.am diff --git a/xorg/xf86-video-dummy/v0.3.8/src/Makefile.in b/server/xorg/xf86-video-dummy/v0.3.8/src/Makefile.in similarity index 100% rename from xorg/xf86-video-dummy/v0.3.8/src/Makefile.in rename to server/xorg/xf86-video-dummy/v0.3.8/src/Makefile.in diff --git a/xorg/xf86-video-dummy/v0.3.8/src/compat-api.h b/server/xorg/xf86-video-dummy/v0.3.8/src/compat-api.h similarity index 100% rename from xorg/xf86-video-dummy/v0.3.8/src/compat-api.h rename to server/xorg/xf86-video-dummy/v0.3.8/src/compat-api.h diff --git a/xorg/xf86-video-dummy/v0.3.8/src/dummy.h b/server/xorg/xf86-video-dummy/v0.3.8/src/dummy.h similarity index 100% rename from xorg/xf86-video-dummy/v0.3.8/src/dummy.h rename to server/xorg/xf86-video-dummy/v0.3.8/src/dummy.h diff --git a/xorg/xf86-video-dummy/v0.3.8/src/dummy_cursor.c b/server/xorg/xf86-video-dummy/v0.3.8/src/dummy_cursor.c similarity index 100% rename from xorg/xf86-video-dummy/v0.3.8/src/dummy_cursor.c rename to server/xorg/xf86-video-dummy/v0.3.8/src/dummy_cursor.c diff --git a/xorg/xf86-video-dummy/v0.3.8/src/dummy_dga.c b/server/xorg/xf86-video-dummy/v0.3.8/src/dummy_dga.c similarity index 100% rename from xorg/xf86-video-dummy/v0.3.8/src/dummy_dga.c rename to server/xorg/xf86-video-dummy/v0.3.8/src/dummy_dga.c diff --git a/xorg/xf86-video-dummy/v0.3.8/src/dummy_driver.c b/server/xorg/xf86-video-dummy/v0.3.8/src/dummy_driver.c similarity index 100% rename from xorg/xf86-video-dummy/v0.3.8/src/dummy_driver.c rename to server/xorg/xf86-video-dummy/v0.3.8/src/dummy_driver.c