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.
This commit is contained in:
Miroslav Šedivý
2023-10-21 21:26:02 +02:00
committed by GitHub
parent 93c1e6fef8
commit 47ee523205
9 changed files with 545 additions and 210 deletions
+3
View File
@@ -467,6 +467,9 @@ components:
RoomSettings: RoomSettings:
type: object type: object
properties: properties:
api_version:
type: number
description: if not set, version is taken from neko_image
name: name:
type: string type: string
example: foobar example: foobar
+6
View File
@@ -394,6 +394,12 @@ export interface RoomResources {
* @interface RoomSettings * @interface RoomSettings
*/ */
export interface RoomSettings { export interface RoomSettings {
/**
*
* @type {number}
* @memberof RoomSettings
*/
'api_version'?: number;
/** /**
* *
* @type {string} * @type {string}
+7
View File
@@ -20,6 +20,7 @@ type Server struct {
Key string Key string
Bind string Bind string
Proxy bool Proxy bool
CORS bool
PProf bool PProf bool
Admin Admin Admin Admin
@@ -46,6 +47,11 @@ func (Server) Init(cmd *cobra.Command) error {
return err 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") cmd.PersistentFlags().Bool("pprof", false, "enable pprof endpoint available at /debug/pprof")
if err := viper.BindPFlag("pprof", cmd.PersistentFlags().Lookup("pprof")); err != nil { if err := viper.BindPFlag("pprof", cmd.PersistentFlags().Lookup("pprof")); err != nil {
return err return err
@@ -86,6 +92,7 @@ func (s *Server) Set() {
s.Key = viper.GetString("key") s.Key = viper.GetString("key")
s.Bind = viper.GetString("bind") s.Bind = viper.GetString("bind")
s.Proxy = viper.GetBool("proxy") s.Proxy = viper.GetBool("proxy")
s.CORS = viper.GetBool("cors")
s.PProf = viper.GetBool("pprof") s.PProf = viper.GetBool("pprof")
s.Admin.Static = viper.GetString("admin.static") s.Admin.Static = viper.GetString("admin.static")
+34 -15
View File
@@ -12,11 +12,13 @@ import (
var labelRegex = regexp.MustCompile(`^[a-z0-9.-]+$`) var labelRegex = regexp.MustCompile(`^[a-z0-9.-]+$`)
type RoomLabels struct { type RoomLabels struct {
Name string Name string
URL string URL string
Mux bool Mux bool
Epr EprPorts Epr EprPorts
NekoImage string
NekoImage string
ApiVersion int
BrowserPolicy *BrowserPolicyLabels BrowserPolicy *BrowserPolicyLabels
UserDefined map[string]string 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") //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 mux bool
var epr EprPorts 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 var browserPolicy *BrowserPolicyLabels
if val, ok := labels["m1k1o.neko_rooms.browser_policy"]; ok && val == "true" { if val, ok := labels["m1k1o.neko_rooms.browser_policy"]; ok && val == "true" {
policyType, ok := labels["m1k1o.neko_rooms.browser_policy.type"] policyType, ok := labels["m1k1o.neko_rooms.browser_policy.type"]
@@ -115,11 +127,13 @@ func (manager *RoomManagerCtx) extractLabels(labels map[string]string) (*RoomLab
} }
return &RoomLabels{ return &RoomLabels{
Name: name, Name: name,
URL: url, URL: url,
NekoImage: nekoImage, Mux: mux,
Mux: mux, Epr: epr,
Epr: epr,
NekoImage: nekoImage,
ApiVersion: apiVersion,
BrowserPolicy: browserPolicy, BrowserPolicy: browserPolicy,
UserDefined: userDefined, UserDefined: userDefined,
@@ -134,6 +148,11 @@ func (manager *RoomManagerCtx) serializeLabels(labels RoomLabels) map[string]str
"m1k1o.neko_rooms.neko_image": labels.NekoImage, "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 { if labels.Mux && labels.Epr.Min == labels.Epr.Max {
labelsMap["m1k1o.neko_rooms.mux"] = fmt.Sprintf("%d", labels.Epr.Min) labelsMap["m1k1o.neko_rooms.mux"] = fmt.Sprintf("%d", labels.Epr.Min)
} else { } else {
+128 -17
View File
@@ -10,7 +10,9 @@ import (
"os" "os"
"path" "path"
"path/filepath" "path/filepath"
"strconv"
"strings" "strings"
"time"
"github.com/docker/cli/opts" "github.com/docker/cli/opts"
dockerTypes "github.com/docker/docker/api/types" 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) 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. // TODO: Check if path name exists.
roomName := settings.Name roomName := settings.Name
if roomName == "" { if roomName == "" {
@@ -315,10 +351,12 @@ func (manager *RoomManagerCtx) Create(ctx context.Context, settings types.RoomSe
} }
labels := manager.serializeLabels(RoomLabels{ labels := manager.serializeLabels(RoomLabels{
Name: roomName, Name: roomName,
Mux: manager.config.Mux, Mux: manager.config.Mux,
Epr: epr, Epr: epr,
NekoImage: settings.NekoImage,
NekoImage: settings.NekoImage,
ApiVersion: settings.ApiVersion,
BrowserPolicy: browserPolicyLabels, BrowserPolicy: browserPolicyLabels,
UserDefined: settings.Labels, UserDefined: settings.Labels,
@@ -394,11 +432,16 @@ func (manager *RoomManagerCtx) Create(ctx context.Context, settings types.RoomSe
// Set environment variables // Set environment variables
// //
env := settings.ToEnv(manager.config, types.PortSettings{ env, err := settings.ToEnv(
FrontendPort: frontendPort, manager.config,
EprMin: epr.Min, types.PortSettings{
EprMax: epr.Max, FrontendPort: frontendPort,
}) EprMin: epr.Min,
EprMax: epr.Max,
})
if err != nil {
return "", err
}
// //
// Set browser policies // Set browser policies
@@ -830,7 +873,7 @@ func (manager *RoomManagerCtx) GetSettings(ctx context.Context, id string) (*typ
settings.MaxConnections = 0 settings.MaxConnections = 0
} }
err = settings.FromEnv(container.Config.Env) err = settings.FromEnv(labels.ApiVersion, container.Config.Env)
return &settings, err return &settings, err
} }
@@ -840,22 +883,90 @@ func (manager *RoomManagerCtx) GetStats(ctx context.Context, id string) (*types.
return nil, err return nil, err
} }
settings := types.RoomSettings{} labels, err := manager.extractLabels(container.Config.Labels)
err = settings.FromEnv(container.Config.Env)
if err != nil { if err != nil {
return nil, err return nil, err
} }
output, err := manager.containerExec(ctx, id, []string{ settings := types.RoomSettings{}
"wget", "-q", "-O-", "http://127.0.0.1:8080/stats?pwd=" + url.QueryEscape(settings.AdminPass), err = settings.FromEnv(labels.ApiVersion, container.Config.Env)
})
if err != nil { if err != nil {
return nil, err return nil, err
} }
var stats types.RoomStats var stats types.RoomStats
if err := json.Unmarshal([]byte(output), &stats); err != nil { switch labels.ApiVersion {
return nil, err 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 return &stats, nil
+12 -8
View File
@@ -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 router.Use(middleware.Recoverer) // Recover from panics without crashing server
// Basic CORS // Basic CORS
// for more ideas, see: https://developer.github.com/v3/#cross-origin-resource-sharing if config.CORS {
router.Use(cors.Handler(cors.Options{ // for more ideas, see: https://developer.github.com/v3/#cross-origin-resource-sharing
AllowedOrigins: []string{"*"}, router.Use(cors.Handler(cors.Options{
AllowedMethods: []string{"GET", "POST", "PUT", "DELETE", "OPTIONS"}, AllowOriginFunc: func(r *http.Request, origin string) bool {
AllowedHeaders: []string{"Accept", "Authorization", "Content-Type", "X-CSRF-Token"}, return true
AllowCredentials: true, },
MaxAge: 300, // Maximum value not ignored by any of major browsers 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 // mount pprof endpoint
if config.PProf { if config.PProf {
+24 -170
View File
@@ -3,31 +3,11 @@ package types
import ( import (
"context" "context"
"fmt" "fmt"
"strconv"
"strings"
"time" "time"
"github.com/m1k1o/neko-rooms/internal/config" "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 { type RoomsConfig struct {
Connections uint16 `json:"connections"` Connections uint16 `json:"connections"`
NekoImages []string `json:"neko_images"` NekoImages []string `json:"neko_images"`
@@ -73,6 +53,8 @@ type RoomResources struct {
} }
type RoomSettings struct { type RoomSettings struct {
ApiVersion int `json:"api_version"`
Name string `json:"name"` Name string `json:"name"`
NekoImage string `json:"neko_image"` NekoImage string `json:"neko_image"`
MaxConnections uint16 `json:"max_connections"` // 0 when using mux MaxConnections uint16 `json:"max_connections"` // 0 when using mux
@@ -106,161 +88,33 @@ type RoomSettings struct {
BrowserPolicy *BrowserPolicy `json:"browser_policy,omitempty"` BrowserPolicy *BrowserPolicy `json:"browser_policy,omitempty"`
} }
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)
}
}
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)
}
}
type PortSettings struct { type PortSettings struct {
FrontendPort uint16 FrontendPort uint16
EprMin, EprMax 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))
}
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
}
}
return nil
}
type RoomStats struct { type RoomStats struct {
Connections uint32 `json:"connections"` Connections uint32 `json:"connections"`
Host string `json:"host"` Host string `json:"host"`
+160
View File
@@ -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
}
+171
View File
@@ -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
}