diff --git a/controllers/admin.go b/controllers/admin.go new file mode 100644 index 000000000..106768301 --- /dev/null +++ b/controllers/admin.go @@ -0,0 +1,19 @@ +package controllers + +import ( + "net/http" + + "github.com/gabek/owncast/core" + "github.com/gabek/owncast/core/rtmp" +) + +// DisconnectInboundConnection will force-disconnect an inbound stream +func DisconnectInboundConnection(w http.ResponseWriter, r *http.Request) { + if !core.GetStatus().Online { + writeSimpleResponse(w, false, "no inbound stream connected") + return + } + + rtmp.Disconnect() + writeSimpleResponse(w, true, "inbound stream disconnected") +} diff --git a/controllers/controllers.go b/controllers/controllers.go index 62dc91191..7a54a3f09 100644 --- a/controllers/controllers.go +++ b/controllers/controllers.go @@ -3,6 +3,8 @@ package controllers import ( "encoding/json" "net/http" + + "github.com/gabek/owncast/models" ) type j map[string]interface{} @@ -24,3 +26,12 @@ func badRequestHandler(w http.ResponseWriter, err error) { w.WriteHeader(http.StatusBadRequest) json.NewEncoder(w).Encode(j{"error": err.Error()}) } + +func writeSimpleResponse(w http.ResponseWriter, success bool, message string) { + response := models.BaseAPIResponse{ + Success: success, + Message: message, + } + w.WriteHeader(http.StatusOK) + json.NewEncoder(w).Encode(response) +} diff --git a/core/rtmp/rtmp.go b/core/rtmp/rtmp.go index 64840a4f8..ddab33e5a 100644 --- a/core/rtmp/rtmp.go +++ b/core/rtmp/rtmp.go @@ -28,6 +28,7 @@ var ( var _transcoder ffmpeg.Transcoder var _pipe *os.File +var _rtmpConnection net.Conn //Start starts the rtmp service, listening on port 1935 func Start() { @@ -92,6 +93,7 @@ func HandleConn(c *rtmp.Conn, nc net.Conn) { _isConnected = true core.SetStreamAsConnected() + _rtmpConnection = nc f, err := os.OpenFile(pipePath, os.O_RDWR, os.ModeNamedPipe) _pipe = f @@ -121,9 +123,20 @@ func handleDisconnect(conn net.Conn) { _pipe.Close() _isConnected = false _transcoder.Stop() + _rtmpConnection = nil core.SetStreamAsDisconnected() } +// Disconnect will force disconnect the current inbound RTMP connection. +func Disconnect() { + if _rtmpConnection == nil { + return + } + + log.Infoln("Inbound stream disconnect requested.") + handleDisconnect(_rtmpConnection) +} + //IsConnected gets whether there is an rtmp connection or not //this is only a getter since it is controlled by the rtmp handler func IsConnected() bool { diff --git a/models/baseAPIResponse.go b/models/baseAPIResponse.go new file mode 100644 index 000000000..8c643f272 --- /dev/null +++ b/models/baseAPIResponse.go @@ -0,0 +1,7 @@ +package models + +// BaseAPIResponse is a simple response to API requests. +type BaseAPIResponse struct { + Success bool `json:"success"` + Message string `json:"message"` +} diff --git a/router/middleware/auth.go b/router/middleware/auth.go new file mode 100644 index 000000000..e168b0ac9 --- /dev/null +++ b/router/middleware/auth.go @@ -0,0 +1,34 @@ +package middleware + +import ( + "crypto/subtle" + "net/http" + + "github.com/gabek/owncast/config" + log "github.com/sirupsen/logrus" +) + +// RequireAdminAuth wraps a handler requiring HTTP basic auth for it using the given +// the stream key as the password and and a hardcoded "admin" for username. +func RequireAdminAuth(handler http.HandlerFunc) http.HandlerFunc { + username := "admin" + password := config.Config.VideoSettings.StreamingKey + + return func(w http.ResponseWriter, r *http.Request) { + + user, pass, ok := r.BasicAuth() + realm := "Owncast Authenticated Request" + + // Failed + if !ok || subtle.ConstantTimeCompare([]byte(user), []byte(username)) != 1 || subtle.ConstantTimeCompare([]byte(pass), []byte(password)) != 1 { + w.Header().Set("WWW-Authenticate", `Basic realm="`+realm+`"`) + http.Error(w, "Unauthorized", http.StatusUnauthorized) + log.Warnln("Failed authentication for", r.URL.Path, "from", r.RemoteAddr, r.UserAgent()) + return + } + + // Success + log.Traceln("Authenticated request OK for", r.URL.Path, "from", r.RemoteAddr, r.UserAgent()) + handler(w, r) + } +} diff --git a/router/router.go b/router/router.go index 1c0f74ac9..70ac3afec 100644 --- a/router/router.go +++ b/router/router.go @@ -10,6 +10,7 @@ import ( "github.com/gabek/owncast/controllers" "github.com/gabek/owncast/core/chat" "github.com/gabek/owncast/core/rtmp" + "github.com/gabek/owncast/router/middleware" ) //Start starts the router for the http, ws, and rtmp @@ -43,6 +44,11 @@ func Start() error { http.HandleFunc("/embed/video", controllers.GetVideoEmbed) } + // Authenticated admin requests + + // Disconnect inbound stream + http.HandleFunc("/api/admin/disconnect", middleware.RequireAdminAuth(controllers.DisconnectInboundConnection)) + port := config.Config.GetPublicWebServerPort() log.Infof("Web server running on port: %d", port)