diff --git a/go.mod b/go.mod index 4fa3bbb..8edcd2d 100644 --- a/go.mod +++ b/go.mod @@ -11,4 +11,5 @@ require ( github.com/prometheus/client_golang v1.7.1 github.com/spf13/viper v1.7.1 golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9 + gopkg.in/yaml.v2 v2.3.0 ) diff --git a/internal/config/config.go b/internal/config/config.go new file mode 100644 index 0000000..0e9b719 --- /dev/null +++ b/internal/config/config.go @@ -0,0 +1,133 @@ +package config + +import ( + "bytes" + "log" + "net" + "strings" + + "github.com/spf13/viper" + "gitlab.crans.org/nounous/ghostream/auth" + "gitlab.crans.org/nounous/ghostream/auth/basic" + "gitlab.crans.org/nounous/ghostream/auth/ldap" + "gitlab.crans.org/nounous/ghostream/internal/monitoring" + "gitlab.crans.org/nounous/ghostream/stream/forwarding" + "gitlab.crans.org/nounous/ghostream/stream/srt" + "gitlab.crans.org/nounous/ghostream/stream/webrtc" + "gitlab.crans.org/nounous/ghostream/web" + "gopkg.in/yaml.v2" +) + +// Config holds application configuration +type Config struct { + Auth auth.Options + Forwarding forwarding.Options + Monitoring monitoring.Options + Srt srt.Options + Web web.Options + WebRTC webrtc.Options +} + +// New configuration with default values +func New() *Config { + return &Config{ + Auth: auth.Options{ + Enabled: true, + Backend: "Basic", + Basic: basic.Options{ + Credentials: map[string]string{ + // Demo user with password "demo" + "demo": "$2b$15$LRnG3eIHFlYIguTxZOLH7eHwbQC/vqjnLq6nDFiHSUDKIU.f5/1H6", + }, + }, + LDAP: ldap.Options{ + URI: "ldap://127.0.0.1:389", + UserDn: "cn=users,dc=example,dc=com", + }, + }, + Forwarding: make(map[string][]string), + Monitoring: monitoring.Options{ + Enabled: true, + ListenAddress: ":2112", + }, + Srt: srt.Options{ + Enabled: true, + ListenAddress: ":9710", + MaxClients: 64, + }, + Web: web.Options{ + Enabled: true, + Favicon: "/static/img/favicon.svg", + Hostname: "localhost", + ListenAddress: ":8080", + Name: "Ghostream", + OneStreamPerDomain: false, + ViewersCounterRefreshPeriod: 20000, + }, + WebRTC: webrtc.Options{ + Enabled: true, + MaxPortUDP: 10005, + MinPortUDP: 10000, + STUNServers: []string{"stun:stun.l.google.com:19302"}, + }, + } +} + +// Load global configuration as a struct +func Load() (*Config, error) { + // Viper needs to know if a key exists in order to override it. + // See https://github.com/spf13/viper/issues/188 + b, err := yaml.Marshal(New()) + if err != nil { + return nil, err + } + defaultConfig := bytes.NewReader(b) + viper.SetConfigType("yaml") + if err := viper.MergeConfig(defaultConfig); err != nil { + return nil, err + } + + // Overwrite configuration from file if exists + viper.SetConfigName("ghostream.yml") + viper.SetConfigType("yaml") + viper.AddConfigPath("$HOME/.ghostream") + viper.AddConfigPath("/etc/ghostream") + viper.AddConfigPath(".") + if err := viper.ReadInConfig(); err != nil { + if _, ok := err.(viper.ConfigFileNotFoundError); ok { + // Config file not found, ignore and use defaults + log.Print(err) + } else { + // Config file was found but another error was produced + return nil, err + } + } else { + // Config loaded + log.Printf("Using config file: %s", viper.ConfigFileUsed()) + } + + // Overwrite configuration from environnement variables + // Replace "." to "_" for nested structs + // e.g. GHOSTREAM_LDAP_URI will apply to Config.LDAP.URI + viper.AutomaticEnv() + viper.SetEnvPrefix("ghostream") + viper.SetEnvKeyReplacer(strings.NewReplacer(".", "_")) + + // Load onto struct + cfg := &Config{} + if err := viper.UnmarshalExact(cfg); err != nil { + return nil, err + } + + // Copy STUN configuration to clients + cfg.Web.STUNServers = cfg.WebRTC.STUNServers + + // Copy SRT server port to display it on web page + _, srtPort, err := net.SplitHostPort(cfg.Srt.ListenAddress) + if err != nil { + return nil, err + } + cfg.Web.SRTServerPort = srtPort + + return cfg, nil +} diff --git a/internal/config/config_test.go b/internal/config/config_test.go new file mode 100644 index 0000000..751588c --- /dev/null +++ b/internal/config/config_test.go @@ -0,0 +1,12 @@ +package config + +import ( + "testing" +) + +func TestLoad(t *testing.T) { + _, err := Load() + if err != nil { + t.Error("Failed to load configuration:", err) + } +} diff --git a/main.go b/main.go index 8944379..deb5a12 100644 --- a/main.go +++ b/main.go @@ -6,11 +6,9 @@ package main import ( "log" - "net" - "strings" - "github.com/spf13/viper" "gitlab.crans.org/nounous/ghostream/auth" + "gitlab.crans.org/nounous/ghostream/internal/config" "gitlab.crans.org/nounous/ghostream/internal/monitoring" "gitlab.crans.org/nounous/ghostream/stream/forwarding" "gitlab.crans.org/nounous/ghostream/stream/srt" @@ -18,89 +16,14 @@ import ( "gitlab.crans.org/nounous/ghostream/web" ) -func loadConfiguration() { - // Load configuration from environnement variables - // Replace "." to "_" for nested structs - // e.g. GHOSTREAM_LDAP_URI will apply to Config.LDAP.URI - viper.SetEnvPrefix("ghostream") - replacer := strings.NewReplacer(".", "_") - viper.SetEnvKeyReplacer(replacer) - viper.AutomaticEnv() - - // Load configuration file if exists - viper.SetConfigName("ghostream.yml") - viper.SetConfigType("yaml") - viper.AddConfigPath("$HOME/.ghostream") - viper.AddConfigPath("/etc/ghostream") - viper.AddConfigPath(".") - if err := viper.ReadInConfig(); err != nil { - if _, ok := err.(viper.ConfigFileNotFoundError); ok { - // Config file not found, ignore and use defaults - log.Print(err) - } else { - // Config file was found but another error was produced - log.Fatal(err) - } - } else { - // Config loaded - log.Printf("Using config file: %s", viper.ConfigFileUsed()) - } - - // Define configuration default values - viper.SetDefault("Auth.Enabled", true) - viper.SetDefault("Auth.Backend", "Basic") - viper.SetDefault("Auth.Basic.Credentials", map[string]string{ - // Demo user with password "demo" - "demo": "$2b$15$LRnG3eIHFlYIguTxZOLH7eHwbQC/vqjnLq6nDFiHSUDKIU.f5/1H6", - }) - viper.SetDefault("Auth.LDAP.URI", "ldap://127.0.0.1:389") - viper.SetDefault("Auth.LDAP.UserDn", "cn=users,dc=example,dc=com") - viper.SetDefault("Forwarding", make(map[string][]string)) - viper.SetDefault("Monitoring.Enabled", true) - viper.SetDefault("Monitoring.ListenAddress", ":2112") - viper.SetDefault("Srt.Enabled", true) - viper.SetDefault("Srt.ListenAddress", ":9710") - viper.SetDefault("Srt.MaxClients", 64) - viper.SetDefault("Web.Enabled", true) - viper.SetDefault("Web.Favicon", "/static/img/favicon.svg") - viper.SetDefault("Web.Hostname", "localhost") - viper.SetDefault("Web.ListenAddress", ":8080") - viper.SetDefault("Web.Name", "Ghostream") - viper.SetDefault("Web.OneStreamPerDomain", false) - viper.SetDefault("Web.ViewersCounterRefreshPeriod", 20000) - viper.SetDefault("WebRTC.Enabled", true) - viper.SetDefault("WebRTC.MaxPortUDP", 10005) - viper.SetDefault("WebRTC.MinPortUDP", 10000) - viper.SetDefault("WebRTC.STUNServers", []string{"stun:stun.l.google.com:19302"}) - - // Copy STUN configuration to clients - viper.Set("Web.STUNServers", viper.Get("WebRTC.STUNServers")) - - // Copy SRT server port to display it on web page - hostport := viper.GetString("Srt.ListenAddress") - _, srtPort, err := net.SplitHostPort(hostport) - if err != nil { - log.Fatalf("Failed to split host and port from %s", hostport) - } - viper.Set("Web.SRTServerPort", srtPort) -} - func main() { // Configure logger log.SetFlags(log.Ldate | log.Ltime | log.Lshortfile) // Load configuration - loadConfiguration() - cfg := struct { - Auth auth.Options - Forwarding forwarding.Options - Monitoring monitoring.Options - Srt srt.Options - Web web.Options - WebRTC webrtc.Options - }{} - if err := viper.Unmarshal(&cfg); err != nil { - log.Fatalln("Failed to load settings", err) + cfg, err := config.Load() + if err != nil { + log.Fatalln("Failed to load configuration:", err) } // Init authentification @@ -123,7 +46,7 @@ func main() { forwardingChannel := make(chan srt.Packet, 65536) webrtcChannel := make(chan srt.Packet, 65536) - // Start stream, web and monitoring server, and stream forwarding + // Start routines go forwarding.Serve(forwardingChannel, cfg.Forwarding) go monitoring.Serve(&cfg.Monitoring) go srt.Serve(&cfg.Srt, authBackend, forwardingChannel, webrtcChannel) diff --git a/main_test.go b/main_test.go index e56643e..06ab7d0 100644 --- a/main_test.go +++ b/main_test.go @@ -1,29 +1 @@ package main - -import ( - "testing" - - "github.com/spf13/viper" - "gitlab.crans.org/nounous/ghostream/auth" - "gitlab.crans.org/nounous/ghostream/internal/monitoring" - "gitlab.crans.org/nounous/ghostream/stream/forwarding" - "gitlab.crans.org/nounous/ghostream/stream/srt" - "gitlab.crans.org/nounous/ghostream/stream/webrtc" - "gitlab.crans.org/nounous/ghostream/web" -) - -// TestLoadConfiguration tests the configuration file loading and the init of some parameters -func TestLoadConfiguration(t *testing.T) { - loadConfiguration() - cfg := struct { - Auth auth.Options - Forwarding forwarding.Options - Monitoring monitoring.Options - Srt srt.Options - Web web.Options - WebRTC webrtc.Options - }{} - if err := viper.Unmarshal(&cfg); err != nil { - t.Fatal("Failed to load settings", err) - } -} diff --git a/stream/forwarding/forwarding.go b/stream/forwarding/forwarding.go index b45a445..c6fdf9b 100644 --- a/stream/forwarding/forwarding.go +++ b/stream/forwarding/forwarding.go @@ -16,6 +16,11 @@ type Options map[string][]string // Serve handles incoming packets from SRT and forward them to other external services func Serve(inputChannel chan srt.Packet, cfg Options) { + if len(cfg) < 1 { + // No forwarding, ignore + return + } + log.Printf("Stream forwarding initialized") ffmpegInstances := make(map[string]*exec.Cmd) ffmpegInputStreams := make(map[string]*io.WriteCloser)