diff --git a/config/defaults.go b/config/defaults.go index 34e5c7fc7..93e27fb33 100644 --- a/config/defaults.go +++ b/config/defaults.go @@ -20,7 +20,8 @@ type Defaults struct { WebServerPort int WebServerIP string RTMPServerPort int - StreamKey string + AdminPassword string + StreamKeys []string YPEnabled bool YPServer string @@ -42,6 +43,8 @@ func GetDefaults() Defaults { Summary: "This is a new live video streaming server powered by Owncast.", ServerWelcomeMessage: "", Logo: "logo.svg", + AdminPassword: "abc123", + StreamKeys: []string{"abc123"}, Tags: []string{ "owncast", "streaming", @@ -71,7 +74,6 @@ func GetDefaults() Defaults { WebServerPort: 8080, WebServerIP: "0.0.0.0", RTMPServerPort: 1935, - StreamKey: "abc123", ChatEstablishedUserModeTimeDuration: time.Minute * 15, diff --git a/controllers/admin/config.go b/controllers/admin/config.go index 79d5dab0b..13ad19e4c 100644 --- a/controllers/admin/config.go +++ b/controllers/admin/config.go @@ -198,8 +198,8 @@ func SetExtraPageContent(w http.ResponseWriter, r *http.Request) { controllers.WriteSimpleResponse(w, true, "changed") } -// SetStreamKey will handle the web config request to set the server stream key. -func SetStreamKey(w http.ResponseWriter, r *http.Request) { +// SetAdminPassword will handle the web config request to set the server admin password. +func SetAdminPassword(w http.ResponseWriter, r *http.Request) { if !requirePOST(w, r) { return } @@ -209,7 +209,7 @@ func SetStreamKey(w http.ResponseWriter, r *http.Request) { return } - if err := data.SetStreamKey(configValue.Value.(string)); err != nil { + if err := data.SetAdminPassword(configValue.Value.(string)); err != nil { controllers.WriteSimpleResponse(w, false, err.Error()) return } @@ -789,3 +789,27 @@ func getValuesFromRequest(w http.ResponseWriter, r *http.Request) ([]ConfigValue return values, true } + +// SetStreamKeys will set the valid stream keys. +func SetStreamKeys(w http.ResponseWriter, r *http.Request) { + if !requirePOST(w, r) { + return + } + + configValues, success := getValuesFromRequest(w, r) + if !success { + return + } + + streamKeyStrings := make([]string, 0) + for _, key := range configValues { + streamKeyStrings = append(streamKeyStrings, key.Value.(string)) + } + + if err := data.SetStreamKeys(streamKeyStrings); err != nil { + controllers.WriteSimpleResponse(w, false, err.Error()) + return + } + + controllers.WriteSimpleResponse(w, true, "changed") +} diff --git a/controllers/admin/serverConfig.go b/controllers/admin/serverConfig.go index d0129fbfd..d2e8f12c6 100644 --- a/controllers/admin/serverConfig.go +++ b/controllers/admin/serverConfig.go @@ -49,7 +49,8 @@ func GetServerConfig(w http.ResponseWriter, r *http.Request) { AppearanceVariables: data.GetCustomColorVariableValues(), }, FFmpegPath: ffmpeg, - StreamKey: data.GetStreamKey(), + AdminPassword: data.GetAdminPassword(), + StreamKeys: data.GetStreamKeys(), WebServerPort: config.WebServerPort, WebServerIP: config.WebServerIP, RTMPServerPort: data.GetRTMPPortNumber(), @@ -98,7 +99,8 @@ func GetServerConfig(w http.ResponseWriter, r *http.Request) { type serverConfigAdminResponse struct { InstanceDetails webConfigResponse `json:"instanceDetails"` FFmpegPath string `json:"ffmpegPath"` - StreamKey string `json:"streamKey"` + AdminPassword string `json:"adminPassword"` + StreamKeys []string `json:"streamKeys"` WebServerPort int `json:"webServerPort"` WebServerIP string `json:"webServerIP"` RTMPServerPort int `json:"rtmpServerPort"` diff --git a/core/data/config.go b/core/data/config.go index f3f4866e7..13f88dfb0 100644 --- a/core/data/config.go +++ b/core/data/config.go @@ -18,7 +18,7 @@ import ( const ( extraContentKey = "extra_page_content" streamTitleKey = "stream_title" - streamKeyKey = "stream_key" + adminPasswordKey = "admin_password_key" logoPathKey = "logo_path" logoUniquenessKey = "logo_uniqueness" serverSummaryKey = "server_summary" @@ -68,6 +68,7 @@ const ( hideViewerCountKey = "hide_viewer_count" customOfflineMessageKey = "custom_offline_message" customColorVariableValuesKey = "custom_color_variable_values" + streamKeysKey = "stream_keys" ) // GetExtraPageBodyContent will return the user-supplied body content. @@ -101,20 +102,15 @@ func SetStreamTitle(title string) error { return _datastore.SetString(streamTitleKey, title) } -// GetStreamKey will return the inbound streaming password. -func GetStreamKey() string { - key, err := _datastore.GetString(streamKeyKey) - if err != nil { - log.Traceln(streamKeyKey, err) - return config.GetDefaults().StreamKey - } - +// GetAdminPassword will return the admin password. +func GetAdminPassword() string { + key, _ := _datastore.GetString(adminPasswordKey) return key } -// SetStreamKey will set the inbound streaming password. -func SetStreamKey(key string) error { - return _datastore.SetString(streamKeyKey, key) +// SetAdminPassword will set the admin password. +func SetAdminPassword(key string) error { + return _datastore.SetString(adminPasswordKey, key) } // GetLogoPath will return the path for the logo, relative to webroot. @@ -582,10 +578,14 @@ func GetVideoCodec() string { // VerifySettings will perform a sanity check for specific settings values. func VerifySettings() error { - if GetStreamKey() == "" { + if len(GetStreamKeys()) == 0 { return errors.New("no stream key set. Please set one via the admin or command line arguments") } + if GetAdminPassword() == "" { + return errors.New("no admin password set. Please set one via the admin or command line arguments") + } + logoPath := GetLogoPath() if !utils.DoesFileExists(filepath.Join(config.DataDirectory, logoPath)) { log.Traceln(logoPath, "not found in the data directory. copying a default logo.") @@ -944,3 +944,14 @@ func GetCustomColorVariableValues() map[string]string { values, _ := _datastore.GetStringMap(customColorVariableValuesKey) return values } + +// GetStreamKeys will return valid stream keys. +func GetStreamKeys() []string { + keys, _ := _datastore.GetStringSlice(streamKeysKey) + return keys +} + +// SetStreamKeys will set valid stream keys. +func SetStreamKeys(keys []string) error { + return _datastore.SetStringSlice(streamKeysKey, keys) +} diff --git a/core/data/datastoreMigrations.go b/core/data/datastoreMigrations.go index 9165b6384..26086f8e8 100644 --- a/core/data/datastoreMigrations.go +++ b/core/data/datastoreMigrations.go @@ -7,18 +7,23 @@ import ( ) const ( - datastoreValuesVersion = 1 + datastoreValuesVersion = 2 datastoreValueVersionKey = "DATA_STORE_VERSION" ) func migrateDatastoreValues(datastore *Datastore) { currentVersion, _ := _datastore.GetNumber(datastoreValueVersionKey) + if currentVersion == 0 { + currentVersion = datastoreValuesVersion + } for v := currentVersion; v < datastoreValuesVersion; v++ { - log.Tracef("Migration datastore values from %d to %d\n", int(v), int(v+1)) + log.Infof("Migration datastore values from %d to %d\n", int(v), int(v+1)) switch v { case 0: migrateToDatastoreValues1(datastore) + case 1: + migrateToDatastoreValues2(datastore) default: log.Fatalln("missing datastore values migration step") } @@ -47,3 +52,9 @@ func migrateToDatastoreValues1(datastore *Datastore) { } } } + +func migrateToDatastoreValues2(datastore *Datastore) { + oldAdminPassword, _ := datastore.GetString("stream_key") + _ = SetAdminPassword(oldAdminPassword) + _ = SetStreamKeys([]string{oldAdminPassword}) +} diff --git a/core/data/defaults.go b/core/data/defaults.go index c54dc3a49..5a8415765 100644 --- a/core/data/defaults.go +++ b/core/data/defaults.go @@ -32,7 +32,8 @@ func PopulateDefaults() { return } - _ = SetStreamKey(defaults.StreamKey) + _ = SetAdminPassword(defaults.AdminPassword) + _ = SetStreamKeys(defaults.StreamKeys) _ = SetHTTPPortNumber(float64(defaults.WebServerPort)) _ = SetRTMPPortNumber(float64(defaults.RTMPServerPort)) _ = SetLogoPath(defaults.Logo) @@ -40,7 +41,6 @@ func PopulateDefaults() { _ = SetServerSummary(defaults.Summary) _ = SetServerWelcomeMessage("") _ = SetServerName(defaults.Name) - _ = SetStreamKey(defaults.StreamKey) _ = SetExtraPageBodyContent(defaults.PageBodyContent) _ = SetFederationGoLiveMessage(defaults.FederationGoLiveMessage) _ = SetSocialHandles([]models.SocialHandle{ diff --git a/core/rtmp/rtmp.go b/core/rtmp/rtmp.go index 0acd92299..5c0aef8d4 100644 --- a/core/rtmp/rtmp.go +++ b/core/rtmp/rtmp.go @@ -15,15 +15,17 @@ import ( "github.com/owncast/owncast/models" ) +var _hasInboundRTMPConnection = false + var ( - _hasInboundRTMPConnection = false + _pipe *io.PipeWriter + _rtmpConnection net.Conn ) -var _pipe *io.PipeWriter -var _rtmpConnection net.Conn - -var _setStreamAsConnected func(*io.PipeReader) -var _setBroadcaster func(models.Broadcaster) +var ( + _setStreamAsConnected func(*io.PipeReader) + _setBroadcaster func(models.Broadcaster) +) // Start starts the rtmp service, listening on specified RTMP port. func Start(setStreamAsConnected func(*io.PipeReader), setBroadcaster func(models.Broadcaster)) { @@ -75,12 +77,28 @@ func HandleConn(c *rtmp.Conn, nc net.Conn) { return } - if !secretMatch(data.GetStreamKey(), c.URL.Path) { + accessGranted := false + validStreamingKeys := data.GetStreamKeys() + + for _, key := range validStreamingKeys { + if secretMatch(key, c.URL.Path) { + accessGranted = true + break + } + } + + if !accessGranted { log.Errorln("invalid streaming key; rejecting incoming stream") _ = nc.Close() return } + // if !secretMatch(data.GetAdminPassword(), c.URL.Path) { + // log.Errorln("invalid streaming key; rejecting incoming stream") + // _ = nc.Close() + // return + // } + rtmpOut, rtmpIn := io.Pipe() _pipe = rtmpIn log.Infoln("Inbound stream connected.") diff --git a/main.go b/main.go index 567989b79..bf00ed8e0 100644 --- a/main.go +++ b/main.go @@ -42,7 +42,7 @@ func main() { // Create the data directory if needed if !utils.DoesFileExists("data") { - if err := os.Mkdir("./data", 0700); err != nil { + if err := os.Mkdir("./data", 0o700); err != nil { log.Fatalln("Cannot create data directory", err) } } @@ -54,7 +54,7 @@ func main() { log.Fatalln("Unable to remove temp dir!") } } - if err := os.Mkdir(config.TempDir, 0700); err != nil { + if err := os.Mkdir(config.TempDir, 0o700); err != nil { log.Fatalln("Unable to create temp dir!", err) } @@ -102,7 +102,7 @@ func main() { func handleCommandLineFlags() { if *newStreamKey != "" { - if err := data.SetStreamKey(*newStreamKey); err != nil { + if err := data.SetAdminPassword(*newStreamKey); err != nil { log.Errorln("Error setting your stream key.", err) log.Exit(1) } else { diff --git a/router/middleware/auth.go b/router/middleware/auth.go index 6d96a2e36..00e8dfc60 100644 --- a/router/middleware/auth.go +++ b/router/middleware/auth.go @@ -22,7 +22,7 @@ type UserAccessTokenHandlerFunc func(user.User, http.ResponseWriter, *http.Reque func RequireAdminAuth(handler http.HandlerFunc) http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { username := "admin" - password := data.GetStreamKey() + password := data.GetAdminPassword() realm := "Owncast Authenticated Request" // The following line is kind of a work around. diff --git a/router/router.go b/router/router.go index e1fadfc84..352f7d25f 100644 --- a/router/router.go +++ b/router/router.go @@ -159,7 +159,10 @@ func Start() error { // Update config values // Change the current streaming key in memory - http.HandleFunc("/api/admin/config/key", middleware.RequireAdminAuth(admin.SetStreamKey)) + http.HandleFunc("/api/admin/config/adminpass", middleware.RequireAdminAuth(admin.SetAdminPassword)) + + // Set an array of valid stream keys + http.HandleFunc("/api/admin/config/streamkeys", middleware.RequireAdminAuth(admin.SetStreamKeys)) // Change the extra page content in memory http.HandleFunc("/api/admin/config/pagecontent", middleware.RequireAdminAuth(admin.SetExtraPageContent)) diff --git a/test/automated/api/configmanagement.test.js b/test/automated/api/configmanagement.test.js index 1ba17f07c..fa56c2dea 100644 --- a/test/automated/api/configmanagement.test.js +++ b/test/automated/api/configmanagement.test.js @@ -7,6 +7,8 @@ const serverSummary = randomString(); const offlineMessage = randomString(); const pageContent = `
${randomString()}
`; const tags = [randomString(), randomString(), randomString()]; +const streamKeys = [randomString(), randomString(), randomString()]; + const latencyLevel = Math.floor(Math.random() * 4); const appearanceValues = { variable1: randomString(), @@ -65,6 +67,11 @@ test('set tags', async (done) => { done(); }); +test('set stream keys', async (done) => { + const res = await sendConfigChangeRequest('streamkeys', streamKeys); + done(); +}); + test('set latency level', async (done) => { const res = await sendConfigChangeRequest( 'video/streamlatencylevel', @@ -157,6 +164,7 @@ test('admin configuration is correct', (done) => { socialHandles ); expect(res.body.forbiddenUsernames).toStrictEqual(forbiddenUsernames); + expect(res.body.streamKeys).toStrictEqual(streamKeys); expect(res.body.videoSettings.latencyLevel).toBe(latencyLevel); expect(res.body.videoSettings.videoQualityVariants[0].framerate).toBe( @@ -167,7 +175,7 @@ test('admin configuration is correct', (done) => { ); expect(res.body.yp.enabled).toBe(false); - expect(res.body.streamKey).toBe('abc123'); + expect(res.body.adminPassword).toBe('abc123'); expect(res.body.s3.enabled).toBe(s3Config.enabled); expect(res.body.s3.endpoint).toBe(s3Config.endpoint);