API + Data changes to support split up of stream keys and admin passwords

This commit is contained in:
Gabe Kangas 2022-11-22 22:08:25 -08:00
parent 1645451faa
commit c9e3ccad45
11 changed files with 116 additions and 37 deletions

View file

@ -20,7 +20,8 @@ type Defaults struct {
WebServerPort int WebServerPort int
WebServerIP string WebServerIP string
RTMPServerPort int RTMPServerPort int
StreamKey string AdminPassword string
StreamKeys []string
YPEnabled bool YPEnabled bool
YPServer string YPServer string
@ -42,6 +43,8 @@ func GetDefaults() Defaults {
Summary: "This is a new live video streaming server powered by Owncast.", Summary: "This is a new live video streaming server powered by Owncast.",
ServerWelcomeMessage: "", ServerWelcomeMessage: "",
Logo: "logo.svg", Logo: "logo.svg",
AdminPassword: "abc123",
StreamKeys: []string{"abc123"},
Tags: []string{ Tags: []string{
"owncast", "owncast",
"streaming", "streaming",
@ -71,7 +74,6 @@ func GetDefaults() Defaults {
WebServerPort: 8080, WebServerPort: 8080,
WebServerIP: "0.0.0.0", WebServerIP: "0.0.0.0",
RTMPServerPort: 1935, RTMPServerPort: 1935,
StreamKey: "abc123",
ChatEstablishedUserModeTimeDuration: time.Minute * 15, ChatEstablishedUserModeTimeDuration: time.Minute * 15,

View file

@ -198,8 +198,8 @@ func SetExtraPageContent(w http.ResponseWriter, r *http.Request) {
controllers.WriteSimpleResponse(w, true, "changed") controllers.WriteSimpleResponse(w, true, "changed")
} }
// SetStreamKey will handle the web config request to set the server stream key. // SetAdminPassword will handle the web config request to set the server admin password.
func SetStreamKey(w http.ResponseWriter, r *http.Request) { func SetAdminPassword(w http.ResponseWriter, r *http.Request) {
if !requirePOST(w, r) { if !requirePOST(w, r) {
return return
} }
@ -209,7 +209,7 @@ func SetStreamKey(w http.ResponseWriter, r *http.Request) {
return 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()) controllers.WriteSimpleResponse(w, false, err.Error())
return return
} }
@ -789,3 +789,27 @@ func getValuesFromRequest(w http.ResponseWriter, r *http.Request) ([]ConfigValue
return values, true 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")
}

View file

@ -49,7 +49,8 @@ func GetServerConfig(w http.ResponseWriter, r *http.Request) {
AppearanceVariables: data.GetCustomColorVariableValues(), AppearanceVariables: data.GetCustomColorVariableValues(),
}, },
FFmpegPath: ffmpeg, FFmpegPath: ffmpeg,
StreamKey: data.GetStreamKey(), AdminPassword: data.GetAdminPassword(),
StreamKeys: data.GetStreamKeys(),
WebServerPort: config.WebServerPort, WebServerPort: config.WebServerPort,
WebServerIP: config.WebServerIP, WebServerIP: config.WebServerIP,
RTMPServerPort: data.GetRTMPPortNumber(), RTMPServerPort: data.GetRTMPPortNumber(),
@ -98,7 +99,8 @@ func GetServerConfig(w http.ResponseWriter, r *http.Request) {
type serverConfigAdminResponse struct { type serverConfigAdminResponse struct {
InstanceDetails webConfigResponse `json:"instanceDetails"` InstanceDetails webConfigResponse `json:"instanceDetails"`
FFmpegPath string `json:"ffmpegPath"` FFmpegPath string `json:"ffmpegPath"`
StreamKey string `json:"streamKey"` AdminPassword string `json:"adminPassword"`
StreamKeys []string `json:"streamKeys"`
WebServerPort int `json:"webServerPort"` WebServerPort int `json:"webServerPort"`
WebServerIP string `json:"webServerIP"` WebServerIP string `json:"webServerIP"`
RTMPServerPort int `json:"rtmpServerPort"` RTMPServerPort int `json:"rtmpServerPort"`

View file

@ -18,7 +18,7 @@ import (
const ( const (
extraContentKey = "extra_page_content" extraContentKey = "extra_page_content"
streamTitleKey = "stream_title" streamTitleKey = "stream_title"
streamKeyKey = "stream_key" adminPasswordKey = "admin_password_key"
logoPathKey = "logo_path" logoPathKey = "logo_path"
logoUniquenessKey = "logo_uniqueness" logoUniquenessKey = "logo_uniqueness"
serverSummaryKey = "server_summary" serverSummaryKey = "server_summary"
@ -68,6 +68,7 @@ const (
hideViewerCountKey = "hide_viewer_count" hideViewerCountKey = "hide_viewer_count"
customOfflineMessageKey = "custom_offline_message" customOfflineMessageKey = "custom_offline_message"
customColorVariableValuesKey = "custom_color_variable_values" customColorVariableValuesKey = "custom_color_variable_values"
streamKeysKey = "stream_keys"
) )
// GetExtraPageBodyContent will return the user-supplied body content. // GetExtraPageBodyContent will return the user-supplied body content.
@ -101,20 +102,15 @@ func SetStreamTitle(title string) error {
return _datastore.SetString(streamTitleKey, title) return _datastore.SetString(streamTitleKey, title)
} }
// GetStreamKey will return the inbound streaming password. // GetAdminPassword will return the admin password.
func GetStreamKey() string { func GetAdminPassword() string {
key, err := _datastore.GetString(streamKeyKey) key, _ := _datastore.GetString(adminPasswordKey)
if err != nil {
log.Traceln(streamKeyKey, err)
return config.GetDefaults().StreamKey
}
return key return key
} }
// SetStreamKey will set the inbound streaming password. // SetAdminPassword will set the admin password.
func SetStreamKey(key string) error { func SetAdminPassword(key string) error {
return _datastore.SetString(streamKeyKey, key) return _datastore.SetString(adminPasswordKey, key)
} }
// GetLogoPath will return the path for the logo, relative to webroot. // 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. // VerifySettings will perform a sanity check for specific settings values.
func VerifySettings() error { 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") 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() logoPath := GetLogoPath()
if !utils.DoesFileExists(filepath.Join(config.DataDirectory, logoPath)) { if !utils.DoesFileExists(filepath.Join(config.DataDirectory, logoPath)) {
log.Traceln(logoPath, "not found in the data directory. copying a default logo.") 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) values, _ := _datastore.GetStringMap(customColorVariableValuesKey)
return values 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)
}

View file

@ -7,18 +7,23 @@ import (
) )
const ( const (
datastoreValuesVersion = 1 datastoreValuesVersion = 2
datastoreValueVersionKey = "DATA_STORE_VERSION" datastoreValueVersionKey = "DATA_STORE_VERSION"
) )
func migrateDatastoreValues(datastore *Datastore) { func migrateDatastoreValues(datastore *Datastore) {
currentVersion, _ := _datastore.GetNumber(datastoreValueVersionKey) currentVersion, _ := _datastore.GetNumber(datastoreValueVersionKey)
if currentVersion == 0 {
currentVersion = datastoreValuesVersion
}
for v := currentVersion; v < datastoreValuesVersion; v++ { 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 { switch v {
case 0: case 0:
migrateToDatastoreValues1(datastore) migrateToDatastoreValues1(datastore)
case 1:
migrateToDatastoreValues2(datastore)
default: default:
log.Fatalln("missing datastore values migration step") 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})
}

View file

@ -32,7 +32,8 @@ func PopulateDefaults() {
return return
} }
_ = SetStreamKey(defaults.StreamKey) _ = SetAdminPassword(defaults.AdminPassword)
_ = SetStreamKeys(defaults.StreamKeys)
_ = SetHTTPPortNumber(float64(defaults.WebServerPort)) _ = SetHTTPPortNumber(float64(defaults.WebServerPort))
_ = SetRTMPPortNumber(float64(defaults.RTMPServerPort)) _ = SetRTMPPortNumber(float64(defaults.RTMPServerPort))
_ = SetLogoPath(defaults.Logo) _ = SetLogoPath(defaults.Logo)
@ -40,7 +41,6 @@ func PopulateDefaults() {
_ = SetServerSummary(defaults.Summary) _ = SetServerSummary(defaults.Summary)
_ = SetServerWelcomeMessage("") _ = SetServerWelcomeMessage("")
_ = SetServerName(defaults.Name) _ = SetServerName(defaults.Name)
_ = SetStreamKey(defaults.StreamKey)
_ = SetExtraPageBodyContent(defaults.PageBodyContent) _ = SetExtraPageBodyContent(defaults.PageBodyContent)
_ = SetFederationGoLiveMessage(defaults.FederationGoLiveMessage) _ = SetFederationGoLiveMessage(defaults.FederationGoLiveMessage)
_ = SetSocialHandles([]models.SocialHandle{ _ = SetSocialHandles([]models.SocialHandle{

View file

@ -15,15 +15,17 @@ import (
"github.com/owncast/owncast/models" "github.com/owncast/owncast/models"
) )
var _hasInboundRTMPConnection = false
var ( var (
_hasInboundRTMPConnection = false _pipe *io.PipeWriter
_rtmpConnection net.Conn
) )
var _pipe *io.PipeWriter var (
var _rtmpConnection net.Conn _setStreamAsConnected func(*io.PipeReader)
_setBroadcaster func(models.Broadcaster)
var _setStreamAsConnected func(*io.PipeReader) )
var _setBroadcaster func(models.Broadcaster)
// Start starts the rtmp service, listening on specified RTMP port. // Start starts the rtmp service, listening on specified RTMP port.
func Start(setStreamAsConnected func(*io.PipeReader), setBroadcaster func(models.Broadcaster)) { func Start(setStreamAsConnected func(*io.PipeReader), setBroadcaster func(models.Broadcaster)) {
@ -75,12 +77,28 @@ func HandleConn(c *rtmp.Conn, nc net.Conn) {
return 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") log.Errorln("invalid streaming key; rejecting incoming stream")
_ = nc.Close() _ = nc.Close()
return return
} }
// if !secretMatch(data.GetAdminPassword(), c.URL.Path) {
// log.Errorln("invalid streaming key; rejecting incoming stream")
// _ = nc.Close()
// return
// }
rtmpOut, rtmpIn := io.Pipe() rtmpOut, rtmpIn := io.Pipe()
_pipe = rtmpIn _pipe = rtmpIn
log.Infoln("Inbound stream connected.") log.Infoln("Inbound stream connected.")

View file

@ -42,7 +42,7 @@ func main() {
// Create the data directory if needed // Create the data directory if needed
if !utils.DoesFileExists("data") { 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) log.Fatalln("Cannot create data directory", err)
} }
} }
@ -54,7 +54,7 @@ func main() {
log.Fatalln("Unable to remove temp dir!") 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) log.Fatalln("Unable to create temp dir!", err)
} }
@ -102,7 +102,7 @@ func main() {
func handleCommandLineFlags() { func handleCommandLineFlags() {
if *newStreamKey != "" { 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.Errorln("Error setting your stream key.", err)
log.Exit(1) log.Exit(1)
} else { } else {

View file

@ -22,7 +22,7 @@ type UserAccessTokenHandlerFunc func(user.User, http.ResponseWriter, *http.Reque
func RequireAdminAuth(handler http.HandlerFunc) http.HandlerFunc { func RequireAdminAuth(handler http.HandlerFunc) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) { return func(w http.ResponseWriter, r *http.Request) {
username := "admin" username := "admin"
password := data.GetStreamKey() password := data.GetAdminPassword()
realm := "Owncast Authenticated Request" realm := "Owncast Authenticated Request"
// The following line is kind of a work around. // The following line is kind of a work around.

View file

@ -159,7 +159,10 @@ func Start() error {
// Update config values // Update config values
// Change the current streaming key in memory // 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 // Change the extra page content in memory
http.HandleFunc("/api/admin/config/pagecontent", middleware.RequireAdminAuth(admin.SetExtraPageContent)) http.HandleFunc("/api/admin/config/pagecontent", middleware.RequireAdminAuth(admin.SetExtraPageContent))

View file

@ -7,6 +7,8 @@ const serverSummary = randomString();
const offlineMessage = randomString(); const offlineMessage = randomString();
const pageContent = `<p>${randomString()}</p>`; const pageContent = `<p>${randomString()}</p>`;
const tags = [randomString(), randomString(), randomString()]; const tags = [randomString(), randomString(), randomString()];
const streamKeys = [randomString(), randomString(), randomString()];
const latencyLevel = Math.floor(Math.random() * 4); const latencyLevel = Math.floor(Math.random() * 4);
const appearanceValues = { const appearanceValues = {
variable1: randomString(), variable1: randomString(),
@ -65,6 +67,11 @@ test('set tags', async (done) => {
done(); done();
}); });
test('set stream keys', async (done) => {
const res = await sendConfigChangeRequest('streamkeys', streamKeys);
done();
});
test('set latency level', async (done) => { test('set latency level', async (done) => {
const res = await sendConfigChangeRequest( const res = await sendConfigChangeRequest(
'video/streamlatencylevel', 'video/streamlatencylevel',
@ -157,6 +164,7 @@ test('admin configuration is correct', (done) => {
socialHandles socialHandles
); );
expect(res.body.forbiddenUsernames).toStrictEqual(forbiddenUsernames); expect(res.body.forbiddenUsernames).toStrictEqual(forbiddenUsernames);
expect(res.body.streamKeys).toStrictEqual(streamKeys);
expect(res.body.videoSettings.latencyLevel).toBe(latencyLevel); expect(res.body.videoSettings.latencyLevel).toBe(latencyLevel);
expect(res.body.videoSettings.videoQualityVariants[0].framerate).toBe( 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.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.enabled).toBe(s3Config.enabled);
expect(res.body.s3.endpoint).toBe(s3Config.endpoint); expect(res.body.s3.endpoint).toBe(s3Config.endpoint);