From 47ee52320577427babdddfd1a5c65a1ef22f7674 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Miroslav=20=C5=A0ediv=C3=BD?= Date: Sat, 21 Oct 2023 21:26:02 +0200 Subject: [PATCH] Allow API versioning (#85) * add api v3 partial support. * fix codecs in env. * fix default codecs. * split files. * add session stats for api v3. * fix API token env. * fix null array in json. * add CORS. * save api version to labels. * irgnore api token env. * remove duplicate api version. * move api version to settings. --- OpenApi.yml | 3 + client/src/api/api.ts | 6 ++ internal/config/server.go | 7 ++ internal/room/labels.go | 49 ++++++--- internal/room/manager.go | 145 +++++++++++++++++++++++--- internal/server/manager.go | 20 ++-- internal/types/room.go | 190 ++++------------------------------ internal/types/room_api_v2.go | 160 ++++++++++++++++++++++++++++ internal/types/room_api_v3.go | 171 ++++++++++++++++++++++++++++++ 9 files changed, 543 insertions(+), 208 deletions(-) create mode 100644 internal/types/room_api_v2.go create mode 100644 internal/types/room_api_v3.go diff --git a/OpenApi.yml b/OpenApi.yml index b8128ad..f7a426c 100644 --- a/OpenApi.yml +++ b/OpenApi.yml @@ -467,6 +467,9 @@ components: RoomSettings: type: object properties: + api_version: + type: number + description: if not set, version is taken from neko_image name: type: string example: foobar diff --git a/client/src/api/api.ts b/client/src/api/api.ts index 390dd14..ea275cf 100644 --- a/client/src/api/api.ts +++ b/client/src/api/api.ts @@ -394,6 +394,12 @@ export interface RoomResources { * @interface RoomSettings */ export interface RoomSettings { + /** + * + * @type {number} + * @memberof RoomSettings + */ + 'api_version'?: number; /** * * @type {string} diff --git a/internal/config/server.go b/internal/config/server.go index 1efc2f0..1bc4dd6 100644 --- a/internal/config/server.go +++ b/internal/config/server.go @@ -20,6 +20,7 @@ type Server struct { Key string Bind string Proxy bool + CORS bool PProf bool Admin Admin @@ -46,6 +47,11 @@ func (Server) Init(cmd *cobra.Command) error { return err } + cmd.PersistentFlags().Bool("cors", false, "enable CORS") + if err := viper.BindPFlag("cors", cmd.PersistentFlags().Lookup("cors")); err != nil { + return err + } + cmd.PersistentFlags().Bool("pprof", false, "enable pprof endpoint available at /debug/pprof") if err := viper.BindPFlag("pprof", cmd.PersistentFlags().Lookup("pprof")); err != nil { return err @@ -86,6 +92,7 @@ func (s *Server) Set() { s.Key = viper.GetString("key") s.Bind = viper.GetString("bind") s.Proxy = viper.GetBool("proxy") + s.CORS = viper.GetBool("cors") s.PProf = viper.GetBool("pprof") s.Admin.Static = viper.GetString("admin.static") diff --git a/internal/room/labels.go b/internal/room/labels.go index 4617172..303dc51 100644 --- a/internal/room/labels.go +++ b/internal/room/labels.go @@ -12,11 +12,13 @@ import ( var labelRegex = regexp.MustCompile(`^[a-z0-9.-]+$`) type RoomLabels struct { - Name string - URL string - Mux bool - Epr EprPorts - NekoImage string + Name string + URL string + Mux bool + Epr EprPorts + + NekoImage string + ApiVersion int BrowserPolicy *BrowserPolicyLabels UserDefined map[string]string @@ -40,11 +42,6 @@ func (manager *RoomManagerCtx) extractLabels(labels map[string]string) (*RoomLab //return nil, fmt.Errorf("damaged container labels: url not found") } - nekoImage, ok := labels["m1k1o.neko_rooms.neko_image"] - if !ok { - return nil, fmt.Errorf("damaged container labels: neko_image not found") - } - var mux bool var epr EprPorts @@ -88,6 +85,21 @@ func (manager *RoomManagerCtx) extractLabels(labels map[string]string) (*RoomLab } } + nekoImage, ok := labels["m1k1o.neko_rooms.neko_image"] + if !ok { + return nil, fmt.Errorf("damaged container labels: neko_image not found") + } + + apiVersion := 2 // default, prior to api versioning + apiVersionStr, ok := labels["m1k1o.neko_rooms.api_version"] + if ok { + var err error + apiVersion, err = strconv.Atoi(apiVersionStr) + if err != nil { + return nil, err + } + } + var browserPolicy *BrowserPolicyLabels if val, ok := labels["m1k1o.neko_rooms.browser_policy"]; ok && val == "true" { policyType, ok := labels["m1k1o.neko_rooms.browser_policy.type"] @@ -115,11 +127,13 @@ func (manager *RoomManagerCtx) extractLabels(labels map[string]string) (*RoomLab } return &RoomLabels{ - Name: name, - URL: url, - NekoImage: nekoImage, - Mux: mux, - Epr: epr, + Name: name, + URL: url, + Mux: mux, + Epr: epr, + + NekoImage: nekoImage, + ApiVersion: apiVersion, BrowserPolicy: browserPolicy, UserDefined: userDefined, @@ -134,6 +148,11 @@ func (manager *RoomManagerCtx) serializeLabels(labels RoomLabels) map[string]str "m1k1o.neko_rooms.neko_image": labels.NekoImage, } + // api version 2 is currently default + if labels.ApiVersion != 2 { + labelsMap["m1k1o.neko_rooms.api_version"] = fmt.Sprintf("%d", labels.ApiVersion) + } + if labels.Mux && labels.Epr.Min == labels.Epr.Max { labelsMap["m1k1o.neko_rooms.mux"] = fmt.Sprintf("%d", labels.Epr.Min) } else { diff --git a/internal/room/manager.go b/internal/room/manager.go index e2ceeed..0e6bca5 100644 --- a/internal/room/manager.go +++ b/internal/room/manager.go @@ -10,7 +10,9 @@ import ( "os" "path" "path/filepath" + "strconv" "strings" + "time" "github.com/docker/cli/opts" dockerTypes "github.com/docker/docker/api/types" @@ -248,6 +250,40 @@ func (manager *RoomManagerCtx) Create(ctx context.Context, settings types.RoomSe isPrivilegedImage, _ := utils.ArrayIn(settings.NekoImage, manager.config.NekoPrivilegedImages) + // if api version is not set, try to detect it + if settings.ApiVersion == 0 { + inspect, _, err := manager.client.ImageInspectWithRaw(ctx, settings.NekoImage) + if err != nil { + return "", err + } + + // based on image label + if val, ok := inspect.Config.Labels["m1k1o.neko_rooms.api_version"]; ok { + var err error + settings.ApiVersion, err = strconv.Atoi(val) + if err != nil { + return "", err + } + } else + + // based on opencontainers image url label + if val, ok := inspect.Config.Labels["org.opencontainers.image.url"]; ok { + switch val { + case "https://github.com/m1k1o/neko": + settings.ApiVersion = 2 + case "https://github.com/demodesk/neko": + settings.ApiVersion = 3 + } + } else + + // unable to detect api version + { + // TODO: this should be removed in future, but since we have a lot of v2 images, we need to support it + log.Warn().Str("image", settings.NekoImage).Msg("unable to detect api version, fallback to v2") + settings.ApiVersion = 2 + } + } + // TODO: Check if path name exists. roomName := settings.Name if roomName == "" { @@ -315,10 +351,12 @@ func (manager *RoomManagerCtx) Create(ctx context.Context, settings types.RoomSe } labels := manager.serializeLabels(RoomLabels{ - Name: roomName, - Mux: manager.config.Mux, - Epr: epr, - NekoImage: settings.NekoImage, + Name: roomName, + Mux: manager.config.Mux, + Epr: epr, + + NekoImage: settings.NekoImage, + ApiVersion: settings.ApiVersion, BrowserPolicy: browserPolicyLabels, UserDefined: settings.Labels, @@ -394,11 +432,16 @@ func (manager *RoomManagerCtx) Create(ctx context.Context, settings types.RoomSe // Set environment variables // - env := settings.ToEnv(manager.config, types.PortSettings{ - FrontendPort: frontendPort, - EprMin: epr.Min, - EprMax: epr.Max, - }) + env, err := settings.ToEnv( + manager.config, + types.PortSettings{ + FrontendPort: frontendPort, + EprMin: epr.Min, + EprMax: epr.Max, + }) + if err != nil { + return "", err + } // // Set browser policies @@ -830,7 +873,7 @@ func (manager *RoomManagerCtx) GetSettings(ctx context.Context, id string) (*typ settings.MaxConnections = 0 } - err = settings.FromEnv(container.Config.Env) + err = settings.FromEnv(labels.ApiVersion, container.Config.Env) return &settings, err } @@ -840,22 +883,90 @@ func (manager *RoomManagerCtx) GetStats(ctx context.Context, id string) (*types. return nil, err } - settings := types.RoomSettings{} - err = settings.FromEnv(container.Config.Env) + labels, err := manager.extractLabels(container.Config.Labels) if err != nil { return nil, err } - output, err := manager.containerExec(ctx, id, []string{ - "wget", "-q", "-O-", "http://127.0.0.1:8080/stats?pwd=" + url.QueryEscape(settings.AdminPass), - }) + settings := types.RoomSettings{} + err = settings.FromEnv(labels.ApiVersion, container.Config.Env) if err != nil { return nil, err } var stats types.RoomStats - if err := json.Unmarshal([]byte(output), &stats); err != nil { - return nil, err + switch labels.ApiVersion { + case 2: + output, err := manager.containerExec(ctx, id, []string{ + "wget", "-q", "-O-", "http://127.0.0.1:8080/stats?pwd=" + url.QueryEscape(settings.AdminPass), + }) + if err != nil { + return nil, err + } + + if err := json.Unmarshal([]byte(output), &stats); err != nil { + return nil, err + } + case 3: + output, err := manager.containerExec(ctx, id, []string{ + "wget", "-q", "-O-", "http://127.0.0.1:8080/api/sessions?token=" + url.QueryEscape(settings.AdminPass), + }) + if err != nil { + return nil, err + } + + var sessions []struct { + ID string `json:"id"` + Profile struct { + Name string `json:"name"` + IsAdmin bool `json:"is_admin"` + } `json:"profile"` + State struct { + IsConnected bool `json:"is_connected"` + NotConnectedSince *time.Time `json:"not_connected_since"` + } `json:"state"` + } + + if err := json.Unmarshal([]byte(output), &sessions); err != nil { + return nil, err + } + + // create empty array so that it's not null in json + stats.Members = []*types.RoomMember{} + + for _, session := range sessions { + if session.State.IsConnected { + stats.Connections++ + // append members + stats.Members = append(stats.Members, &types.RoomMember{ + ID: session.ID, + Name: session.Profile.Name, + Admin: session.Profile.IsAdmin, + Muted: false, // not supported + }) + } else if session.State.NotConnectedSince != nil { + // populate last admin left time + if session.Profile.IsAdmin && (stats.LastAdminLeftAt == nil || (*session.State.NotConnectedSince).After(*stats.LastAdminLeftAt)) { + stats.LastAdminLeftAt = session.State.NotConnectedSince + } + // populate last user left time + if !session.Profile.IsAdmin && (stats.LastUserLeftAt == nil || (*session.State.NotConnectedSince).After(*stats.LastUserLeftAt)) { + stats.LastUserLeftAt = session.State.NotConnectedSince + } + } + } + + // parse started time + if container.State.StartedAt != "" { + stats.ServerStartedAt, err = time.Parse(time.RFC3339, container.State.StartedAt) + if err != nil { + return nil, err + } + } + + // TODO: settings & host + default: + return nil, fmt.Errorf("unsupported API version: %d", labels.ApiVersion) } return &stats, nil diff --git a/internal/server/manager.go b/internal/server/manager.go index 1263165..312453e 100644 --- a/internal/server/manager.go +++ b/internal/server/manager.go @@ -45,14 +45,18 @@ func New(ApiManager types.ApiManager, roomConfig *config.Room, config *config.Se router.Use(middleware.Recoverer) // Recover from panics without crashing server // Basic CORS - // for more ideas, see: https://developer.github.com/v3/#cross-origin-resource-sharing - router.Use(cors.Handler(cors.Options{ - AllowedOrigins: []string{"*"}, - AllowedMethods: []string{"GET", "POST", "PUT", "DELETE", "OPTIONS"}, - AllowedHeaders: []string{"Accept", "Authorization", "Content-Type", "X-CSRF-Token"}, - AllowCredentials: true, - MaxAge: 300, // Maximum value not ignored by any of major browsers - })) + if config.CORS { + // for more ideas, see: https://developer.github.com/v3/#cross-origin-resource-sharing + router.Use(cors.Handler(cors.Options{ + AllowOriginFunc: func(r *http.Request, origin string) bool { + return true + }, + AllowedMethods: []string{"GET", "POST", "PUT", "DELETE", "OPTIONS"}, + AllowedHeaders: []string{"Accept", "Authorization", "Content-Type", "X-CSRF-Token"}, + AllowCredentials: true, + MaxAge: 300, // Maximum value not ignored by any of major browsers + })) + } // mount pprof endpoint if config.PProf { diff --git a/internal/types/room.go b/internal/types/room.go index 46437be..fb9f103 100644 --- a/internal/types/room.go +++ b/internal/types/room.go @@ -3,31 +3,11 @@ package types import ( "context" "fmt" - "strconv" - "strings" "time" "github.com/m1k1o/neko-rooms/internal/config" - "github.com/m1k1o/neko-rooms/internal/utils" ) -var blacklistedEnvs = []string{ - // ignore bunch of default envs - "DEBIAN_FRONTEND", - "PULSE_SERVER", - "DISPLAY", - "USER", - "PATH", - - // ignore bunch of envs managed by neko-rooms - "NEKO_BIND", - "NEKO_EPR", - "NEKO_UDPMUX", - "NEKO_TCPMUX", - "NEKO_NAT1TO1", - "NEKO_ICELITE", -} - type RoomsConfig struct { Connections uint16 `json:"connections"` NekoImages []string `json:"neko_images"` @@ -73,6 +53,8 @@ type RoomResources struct { } type RoomSettings struct { + ApiVersion int `json:"api_version"` + Name string `json:"name"` NekoImage string `json:"neko_image"` MaxConnections uint16 `json:"max_connections"` // 0 when using mux @@ -106,159 +88,31 @@ type RoomSettings struct { BrowserPolicy *BrowserPolicy `json:"browser_policy,omitempty"` } -type PortSettings struct { - FrontendPort uint16 - EprMin, EprMax uint16 -} - -func (settings *RoomSettings) ToEnv(config *config.Room, ports PortSettings) []string { - env := []string{ - fmt.Sprintf("NEKO_BIND=:%d", ports.FrontendPort), - "NEKO_ICELITE=true", - - // from settings - fmt.Sprintf("NEKO_PASSWORD=%s", settings.UserPass), - fmt.Sprintf("NEKO_PASSWORD_ADMIN=%s", settings.AdminPass), - fmt.Sprintf("NEKO_SCREEN=%s", settings.Screen), - fmt.Sprintf("NEKO_MAX_FPS=%d", settings.VideoMaxFPS), - } - - if config.Mux { - env = append(env, - fmt.Sprintf("NEKO_UDPMUX=%d", ports.EprMin), - fmt.Sprintf("NEKO_TCPMUX=%d", ports.EprMin), - ) - } else { - env = append(env, - fmt.Sprintf("NEKO_EPR=%d-%d", ports.EprMin, ports.EprMax), - ) - } - - // optional nat mapping - if len(config.NAT1To1IPs) > 0 { - env = append(env, fmt.Sprintf("NEKO_NAT1TO1=%s", strings.Join(config.NAT1To1IPs, ","))) - } - - if settings.ControlProtection { - env = append(env, "NEKO_CONTROL_PROTECTION=true") - } - - if settings.ImplicitControl { - env = append(env, "NEKO_IMPLICIT_CONTROL=true") - } - - if settings.VideoCodec == "VP8" || settings.VideoCodec == "VP9" || settings.VideoCodec == "H264" { - env = append(env, fmt.Sprintf("NEKO_%s=true", strings.ToUpper(settings.VideoCodec))) - } - - if settings.VideoBitrate != 0 { - env = append(env, fmt.Sprintf("NEKO_VIDEO_BITRATE=%d", settings.VideoBitrate)) - } - - if settings.VideoPipeline != "" { - env = append(env, fmt.Sprintf("NEKO_VIDEO=%s", settings.VideoPipeline)) - } - - if settings.AudioCodec == "OPUS" || settings.AudioCodec == "G722" || settings.AudioCodec == "PCMU" || settings.AudioCodec == "PCMA" { - env = append(env, fmt.Sprintf("NEKO_%s=true", strings.ToUpper(settings.AudioCodec))) - } - - if settings.AudioBitrate != 0 { - env = append(env, fmt.Sprintf("NEKO_AUDIO_BITRATE=%d", settings.AudioBitrate)) - } - - if settings.AudioPipeline != "" { - env = append(env, fmt.Sprintf("NEKO_AUDIO=%s", settings.AudioPipeline)) - } - - if settings.BroadcastPipeline != "" { - env = append(env, fmt.Sprintf("NEKO_BROADCAST_PIPELINE=%s", settings.BroadcastPipeline)) +func (settings *RoomSettings) ToEnv(config *config.Room, ports PortSettings) ([]string, error) { + switch settings.ApiVersion { + case 2: + return settings.toEnvV2(config, ports), nil + case 3: + return settings.toEnvV3(config, ports), nil + default: + return nil, fmt.Errorf("unsupported API version: %d", settings.ApiVersion) } - - for key, val := range settings.Envs { - if in, _ := utils.ArrayIn(key, blacklistedEnvs); !in { - env = append(env, fmt.Sprintf("%s=%s", key, val)) - } - } - - return env } -func (settings *RoomSettings) FromEnv(envs []string) error { - settings.Envs = map[string]string{} - - var err error - for _, env := range envs { - r := strings.SplitN(env, "=", 2) - key, val := r[0], r[1] - - switch key { - case "NEKO_PASSWORD": - settings.UserPass = val - case "NEKO_PASSWORD_ADMIN": - settings.AdminPass = val - case "NEKO_CONTROL_PROTECTION": - if ok, _ := strconv.ParseBool(val); ok { - settings.ControlProtection = true - } - case "NEKO_IMPLICIT_CONTROL": - if ok, _ := strconv.ParseBool(val); ok { - settings.ImplicitControl = true - } - case "NEKO_SCREEN": - settings.Screen = val - case "NEKO_MAX_FPS": - settings.VideoMaxFPS, err = strconv.Atoi(val) - case "NEKO_BROADCAST_PIPELINE": - settings.BroadcastPipeline = val - case "NEKO_VP8": - if ok, _ := strconv.ParseBool(val); ok { - settings.VideoCodec = "VP8" - } - case "NEKO_VP9": - if ok, _ := strconv.ParseBool(val); ok { - settings.VideoCodec = "VP9" - } - case "NEKO_H264": - if ok, _ := strconv.ParseBool(val); ok { - settings.VideoCodec = "H264" - } - case "NEKO_VIDEO_BITRATE": - settings.VideoBitrate, err = strconv.Atoi(val) - case "NEKO_VIDEO": - settings.VideoPipeline = val - case "NEKO_OPUS": - if ok, _ := strconv.ParseBool(val); ok { - settings.AudioCodec = "OPUS" - } - case "NEKO_G722": - if ok, _ := strconv.ParseBool(val); ok { - settings.AudioCodec = "G722" - } - case "NEKO_PCMU": - if ok, _ := strconv.ParseBool(val); ok { - settings.AudioCodec = "PCMU" - } - case "NEKO_PCMA": - if ok, _ := strconv.ParseBool(val); ok { - settings.AudioCodec = "PCMA" - } - case "NEKO_AUDIO_BITRATE": - settings.AudioBitrate, err = strconv.Atoi(val) - case "NEKO_AUDIO": - settings.AudioPipeline = val - default: - if in, _ := utils.ArrayIn(key, blacklistedEnvs); !in { - settings.Envs[key] = val - } - } - - if err != nil { - return err - } +func (settings *RoomSettings) FromEnv(apiVersion int, envs []string) error { + switch apiVersion { + case 2: + return settings.fromEnvV2(envs) + case 3: + return settings.fromEnvV3(envs) + default: + return fmt.Errorf("unsupported API version: %d", apiVersion) } +} - return nil +type PortSettings struct { + FrontendPort uint16 + EprMin, EprMax uint16 } type RoomStats struct { diff --git a/internal/types/room_api_v2.go b/internal/types/room_api_v2.go new file mode 100644 index 0000000..03768ec --- /dev/null +++ b/internal/types/room_api_v2.go @@ -0,0 +1,160 @@ +package types + +import ( + "fmt" + "strconv" + "strings" + + "github.com/m1k1o/neko-rooms/internal/config" + "github.com/m1k1o/neko-rooms/internal/utils" +) + +// +// m1k1o/neko v2 envs API +// + +var blacklistedEnvsV2 = []string{ + // ignore bunch of default envs + "DEBIAN_FRONTEND", + "PULSE_SERVER", + "XDG_RUNTIME_DIR", + "DISPLAY", + "USER", + "PATH", + + // ignore bunch of envs managed by neko-rooms + "NEKO_BIND", + "NEKO_EPR", + "NEKO_UDPMUX", + "NEKO_TCPMUX", + "NEKO_NAT1TO1", + "NEKO_ICELITE", +} + +func (settings *RoomSettings) toEnvV2(config *config.Room, ports PortSettings) []string { + env := []string{ + fmt.Sprintf("NEKO_BIND=:%d", ports.FrontendPort), + "NEKO_ICELITE=true", + + // from settings + fmt.Sprintf("NEKO_PASSWORD=%s", settings.UserPass), + fmt.Sprintf("NEKO_PASSWORD_ADMIN=%s", settings.AdminPass), + fmt.Sprintf("NEKO_SCREEN=%s", settings.Screen), + fmt.Sprintf("NEKO_MAX_FPS=%d", settings.VideoMaxFPS), + } + + if config.Mux { + env = append(env, + fmt.Sprintf("NEKO_UDPMUX=%d", ports.EprMin), + fmt.Sprintf("NEKO_TCPMUX=%d", ports.EprMin), + ) + } else { + env = append(env, + fmt.Sprintf("NEKO_EPR=%d-%d", ports.EprMin, ports.EprMax), + ) + } + + // optional nat mapping + if len(config.NAT1To1IPs) > 0 { + env = append(env, fmt.Sprintf("NEKO_NAT1TO1=%s", strings.Join(config.NAT1To1IPs, ","))) + } + + if settings.ControlProtection { + env = append(env, "NEKO_CONTROL_PROTECTION=true") + } + + if settings.ImplicitControl { + env = append(env, "NEKO_IMPLICIT_CONTROL=true") + } + + if settings.VideoCodec != "VP8" { // VP8 is default + env = append(env, fmt.Sprintf("NEKO_VIDEO_CODEC=%s", strings.ToLower(settings.VideoCodec))) + } + + if settings.VideoBitrate != 0 { + env = append(env, fmt.Sprintf("NEKO_VIDEO_BITRATE=%d", settings.VideoBitrate)) + } + + if settings.VideoPipeline != "" { + env = append(env, fmt.Sprintf("NEKO_VIDEO=%s", settings.VideoPipeline)) + } + + if settings.AudioCodec != "OPUS" { // OPUS is default + env = append(env, fmt.Sprintf("NEKO_AUDIO_CODEC=%s", strings.ToLower(settings.AudioCodec))) + } + + if settings.AudioBitrate != 0 { + env = append(env, fmt.Sprintf("NEKO_AUDIO_BITRATE=%d", settings.AudioBitrate)) + } + + if settings.AudioPipeline != "" { + env = append(env, fmt.Sprintf("NEKO_AUDIO=%s", settings.AudioPipeline)) + } + + if settings.BroadcastPipeline != "" { + env = append(env, fmt.Sprintf("NEKO_BROADCAST_PIPELINE=%s", settings.BroadcastPipeline)) + } + + for key, val := range settings.Envs { + if in, _ := utils.ArrayIn(key, blacklistedEnvsV2); !in { + env = append(env, fmt.Sprintf("%s=%s", key, val)) + } + } + + return env +} + +func (settings *RoomSettings) fromEnvV2(envs []string) error { + settings.Envs = map[string]string{} + settings.VideoCodec = "VP8" // default + settings.AudioCodec = "OPUS" // default + + var err error + for _, env := range envs { + r := strings.SplitN(env, "=", 2) + key, val := r[0], r[1] + + switch key { + case "NEKO_PASSWORD": + settings.UserPass = val + case "NEKO_PASSWORD_ADMIN": + settings.AdminPass = val + case "NEKO_CONTROL_PROTECTION": + if ok, _ := strconv.ParseBool(val); ok { + settings.ControlProtection = true + } + case "NEKO_IMPLICIT_CONTROL": + if ok, _ := strconv.ParseBool(val); ok { + settings.ImplicitControl = true + } + case "NEKO_SCREEN": + settings.Screen = val + case "NEKO_MAX_FPS": + settings.VideoMaxFPS, err = strconv.Atoi(val) + case "NEKO_BROADCAST_PIPELINE": + settings.BroadcastPipeline = val + case "NEKO_VIDEO_CODEC": + settings.VideoCodec = strings.ToUpper(val) + case "NEKO_VIDEO_BITRATE": + settings.VideoBitrate, err = strconv.Atoi(val) + case "NEKO_VIDEO": + settings.VideoPipeline = val + case "NEKO_AUDIO_CODEC": + settings.VideoCodec = strings.ToUpper(val) + case "NEKO_AUDIO_BITRATE": + settings.AudioBitrate, err = strconv.Atoi(val) + case "NEKO_AUDIO": + settings.AudioPipeline = val + default: + if in, _ := utils.ArrayIn(key, blacklistedEnvsV2); !in { + settings.Envs[key] = val + } + } + + if err != nil { + return err + } + } + + return nil +} diff --git a/internal/types/room_api_v3.go b/internal/types/room_api_v3.go new file mode 100644 index 0000000..50cd86a --- /dev/null +++ b/internal/types/room_api_v3.go @@ -0,0 +1,171 @@ +package types + +import ( + "fmt" + "strconv" + "strings" + + "github.com/m1k1o/neko-rooms/internal/config" + "github.com/m1k1o/neko-rooms/internal/utils" +) + +// +// demodesk/neko v3 envs API +// + +var blacklistedEnvsV3 = []string{ + // ignore bunch of default envs + "DEBIAN_FRONTEND", + "PULSE_SERVER", + "XDG_RUNTIME_DIR", + "DISPLAY", + "USER", + "PATH", + + // ignore bunch of envs managed by neko + "NEKO_PLUGINS_ENABLED", + "NEKO_PLUGINS_DIR", + + // ignore bunch of envs managed by neko-rooms + "NEKO_SERVER_BIND", + "NEKO_SESSION_API_TOKEN", + "NEKO_MEMBER_PROVIDER", + "NEKO_WEBRTC_EPR", + "NEKO_WEBRTC_UDPMUX", + "NEKO_WEBRTC_TCPMUX", + "NEKO_WEBRTC_NAT1TO1", + "NEKO_WEBRTC_ICELITE", +} + +func (settings *RoomSettings) toEnvV3(config *config.Room, ports PortSettings) []string { + env := []string{ + fmt.Sprintf("NEKO_SERVER_BIND=:%d", ports.FrontendPort), + "NEKO_WEBRTC_ICELITE=true", + + // from settings + "NEKO_MEMBER_PROVIDER=multiuser", + fmt.Sprintf("NEKO_MEMBER_MULTIUSER_USER_PASSWORD=%s", settings.UserPass), + fmt.Sprintf("NEKO_MEMBER_MULTIUSER_ADMIN_PASSWORD=%s", settings.AdminPass), + fmt.Sprintf("NEKO_SESSION_API_TOKEN=%s", settings.AdminPass), // TODO: should be random and saved somewhere + fmt.Sprintf("NEKO_DESKTOP_SCREEN=%s", settings.Screen), + //fmt.Sprintf("NEKO_MAX_FPS=%d", settings.VideoMaxFPS), // TODO: not supported yet + } + + if config.Mux { + env = append(env, + fmt.Sprintf("NEKO_WEBRTC_UDPMUX=%d", ports.EprMin), + fmt.Sprintf("NEKO_WEBRTC_TCPMUX=%d", ports.EprMin), + ) + } else { + env = append(env, + fmt.Sprintf("NEKO_WEBRTC_EPR=%d-%d", ports.EprMin, ports.EprMax), + ) + } + + // optional nat mapping + if len(config.NAT1To1IPs) > 0 { + env = append(env, fmt.Sprintf("NEKO_WEBRTC_NAT1TO1=%s", strings.Join(config.NAT1To1IPs, ","))) + } + + //if settings.ControlProtection { + // env = append(env, "NEKO_CONTROL_PROTECTION=true") // TODO: not supported yet + //} + + // implicit control - enabled by default + if !settings.ImplicitControl { + env = append(env, "NEKO_SESSION_IMPLICIT_HOSTING=false") + } + + if settings.VideoCodec != "VP8" { // VP8 is default + env = append(env, fmt.Sprintf("NEKO_CAPTURE_VIDEO_CODEC=%s", strings.ToLower(settings.VideoCodec))) + } + + //if settings.VideoBitrate != 0 { + // env = append(env, fmt.Sprintf("NEKO_VIDEO_BITRATE=%d", settings.VideoBitrate)) // TODO: not supported yet + //} + + if settings.VideoPipeline != "" { + env = append(env, fmt.Sprintf("NEKO_CAPTURE_VIDEO_PIPELINES=%s", settings.VideoPipeline)) // TOOD: multiple pipelines, as JSON + } + + if settings.AudioCodec != "OPUS" { // OPUS is default + env = append(env, fmt.Sprintf("NEKO_CAPTURE_AUDIO_CODEC=%s", strings.ToLower(settings.AudioCodec))) + } + + //if settings.AudioBitrate != 0 { + // env = append(env, fmt.Sprintf("NEKO_AUDIO_BITRATE=%d", settings.AudioBitrate)) // TODO: not supported yet + //} + + if settings.AudioPipeline != "" { + env = append(env, fmt.Sprintf("NEKO_CAPTURE_AUDIO_PIPELINE=%s", settings.AudioPipeline)) + } + + if settings.BroadcastPipeline != "" { + env = append(env, fmt.Sprintf("NEKO_CAPTURE_BROADCAST_PIPELINE=%s", settings.BroadcastPipeline)) + } + + for key, val := range settings.Envs { + if in, _ := utils.ArrayIn(key, blacklistedEnvsV3); !in { + env = append(env, fmt.Sprintf("%s=%s", key, val)) + } + } + + return env +} + +func (settings *RoomSettings) fromEnvV3(envs []string) error { + settings.Envs = map[string]string{} + // enabled implicit control by default + settings.ImplicitControl = true + settings.VideoCodec = "VP8" // default + settings.AudioCodec = "OPUS" // default + + var err error + for _, env := range envs { + r := strings.SplitN(env, "=", 2) + key, val := r[0], r[1] + + switch key { + case "NEKO_MEMBER_MULTIUSER_USER_PASSWORD": + settings.UserPass = val + case "NEKO_MEMBER_MULTIUSER_ADMIN_PASSWORD": + settings.AdminPass = val + //case "NEKO_CONTROL_PROTECTION": // TODO: not supported yet + // if ok, _ := strconv.ParseBool(val); ok { + // settings.ControlProtection = true + // } + case "NEKO_SESSION_IMPLICIT_HOSTING": + if ok, _ := strconv.ParseBool(val); !ok { + settings.ImplicitControl = false + } + case "NEKO_DESKTOP_SCREEN": + settings.Screen = val + //case "NEKO_MAX_FPS": // TODO: not supported yet + // settings.VideoMaxFPS, err = strconv.Atoi(val) + case "NEKO_CAPTURE_BROADCAST_PIPELINE": + settings.BroadcastPipeline = val + case "NEKO_CAPTURE_VIDEO_CODEC": + settings.VideoCodec = strings.ToUpper(val) + //case "NEKO_VIDEO_BITRATE": // TODO: not supported yet + // settings.VideoBitrate, err = strconv.Atoi(val) + case "NEKO_CAPTURE_VIDEO_PIPELINES": // TOOD: multiple pipelines, as JSON + settings.VideoPipeline = val + case "NEKO_CAPTURE_AUDIO_CODEC": + settings.AudioCodec = strings.ToUpper(val) + //case "NEKO_AUDIO_BITRATE": // TODO: not supported yet + // settings.AudioBitrate, err = strconv.Atoi(val) + case "NEKO_CAPTURE_AUDIO_PIPELINE": + settings.AudioPipeline = val + default: + if in, _ := utils.ArrayIn(key, blacklistedEnvsV3); !in { + settings.Envs[key] = val + } + } + + if err != nil { + return err + } + } + + return nil +}