chore(api): reorganize web assets and codegen types+handlers

This commit is contained in:
Gabe Kangas
2024-07-01 20:12:08 -07:00
parent 2ccd3aad87
commit 5cb4850fce
29 changed files with 4352 additions and 6710 deletions

View File

@@ -0,0 +1,48 @@
package middleware
import (
"net/http"
"strings"
"github.com/owncast/owncast/core/data"
"github.com/owncast/owncast/utils"
)
// RequireActivityPubOrRedirect will validate the requested content types and
// redirect to the main Owncast page if it doesn't match.
func RequireActivityPubOrRedirect(handler http.HandlerFunc) http.HandlerFunc {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if !data.GetFederationEnabled() {
w.WriteHeader(http.StatusMethodNotAllowed)
return
}
handleAccepted := func() {
handler(w, r)
}
acceptedContentTypes := []string{"application/json", "application/json+ld", "application/activity+json", `application/ld+json; profile="https://www.w3.org/ns/activitystreams"`}
var accept []string
for _, a := range r.Header.Values("Accept") {
accept = append(accept, strings.Split(a, ",")...)
}
for _, singleType := range accept {
if _, accepted := utils.FindInSlice(acceptedContentTypes, strings.TrimSpace(singleType)); accepted {
handleAccepted()
return
}
}
contentTypeString := r.Header.Get("Content-Type")
contentTypes := strings.Split(contentTypeString, ",")
for _, singleType := range contentTypes {
if _, accepted := utils.FindInSlice(acceptedContentTypes, strings.TrimSpace(singleType)); accepted {
handleAccepted()
return
}
}
http.Redirect(w, r, "/", http.StatusTemporaryRedirect)
})
}

View File

@@ -0,0 +1,156 @@
package middleware
import (
"crypto/subtle"
"net/http"
"strings"
"github.com/owncast/owncast/core/data"
"github.com/owncast/owncast/models"
"github.com/owncast/owncast/persistence/userrepository"
"github.com/owncast/owncast/utils"
log "github.com/sirupsen/logrus"
)
// ExternalAccessTokenHandlerFunc is a function that is called after validing access.
type ExternalAccessTokenHandlerFunc func(models.ExternalAPIUser, http.ResponseWriter, *http.Request)
// UserAccessTokenHandlerFunc is a function that is called after validing user access.
type UserAccessTokenHandlerFunc func(models.User, http.ResponseWriter, *http.Request)
// 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 {
return func(w http.ResponseWriter, r *http.Request) {
username := "admin"
password := data.GetAdminPassword()
realm := "Owncast Authenticated Request"
// Alow CORS only for localhost:3000 to support Owncast development.
validAdminHost := "http://localhost:3000"
w.Header().Set("Access-Control-Allow-Origin", validAdminHost)
w.Header().Set("Access-Control-Allow-Credentials", "true")
w.Header().Set("Access-Control-Allow-Headers", "Origin, X-Requested-With, Content-Type, Accept, Authorization")
// For request needing CORS, send a 204.
if r.Method == "OPTIONS" {
w.WriteHeader(http.StatusNoContent)
return
}
user, pass, ok := r.BasicAuth()
// Failed
if !ok || subtle.ConstantTimeCompare([]byte(user), []byte(username)) != 1 || utils.ComparseHash(password, pass) != nil {
w.Header().Set("WWW-Authenticate", `Basic realm="`+realm+`"`)
http.Error(w, "Unauthorized", http.StatusUnauthorized)
log.Debugln("Failed admin authentication")
return
}
handler(w, r)
}
}
func accessDenied(w http.ResponseWriter) {
w.WriteHeader(http.StatusUnauthorized) //nolint
w.Write([]byte("unauthorized")) //nolint
}
// RequireExternalAPIAccessToken will validate a 3rd party access token.
func RequireExternalAPIAccessToken(scope string, handler ExternalAccessTokenHandlerFunc) http.HandlerFunc {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
// We should accept 3rd party preflight OPTIONS requests.
if r.Method == "OPTIONS" {
// All OPTIONS requests should have a wildcard CORS header.
w.Header().Set("Access-Control-Allow-Origin", "*")
w.WriteHeader(http.StatusNoContent)
return
}
authHeader := r.Header.Get("Authorization")
token := ""
if strings.HasPrefix(strings.ToLower(authHeader), "bearer ") {
token = authHeader[len("bearer "):]
}
if token == "" {
log.Warnln("invalid access token")
accessDenied(w)
return
}
userRepository := userrepository.Get()
integration, err := userRepository.GetExternalAPIUserForAccessTokenAndScope(token, scope)
if integration == nil || err != nil {
accessDenied(w)
return
}
// All auth'ed 3rd party requests should have a wildcard CORS header.
w.Header().Set("Access-Control-Allow-Origin", "*")
handler(*integration, w, r)
if err := userRepository.SetExternalAPIUserAccessTokenAsUsed(token); err != nil {
log.Debugln("token not found when updating last_used timestamp")
}
})
}
// RequireUserAccessToken will validate a provided user's access token and make sure the associated user is enabled.
// Not to be used for validating 3rd party access.
func RequireUserAccessToken(handler UserAccessTokenHandlerFunc) http.HandlerFunc {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
accessToken := r.URL.Query().Get("accessToken")
if accessToken == "" {
accessDenied(w)
return
}
ipAddress := utils.GetIPAddressFromRequest(r)
// Check if this client's IP address is banned.
if blocked, err := data.IsIPAddressBanned(ipAddress); blocked {
log.Debugln("Client ip address has been blocked. Rejecting.")
accessDenied(w)
return
} else if err != nil {
log.Errorln("error determining if IP address is blocked: ", err)
}
userRepository := userrepository.Get()
// A user is required to use the websocket
user := userRepository.GetUserByToken(accessToken)
if user == nil || !user.IsEnabled() {
accessDenied(w)
return
}
handler(*user, w, r)
})
}
// RequireUserModerationScopeAccesstoken will validate a provided user's access token and make sure the associated user is enabled
// and has "MODERATOR" scope assigned to the user.
func RequireUserModerationScopeAccesstoken(handler http.HandlerFunc) http.HandlerFunc {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
accessToken := r.URL.Query().Get("accessToken")
if accessToken == "" {
accessDenied(w)
return
}
userRepository := userrepository.Get()
// A user is required to use the websocket
user := userRepository.GetUserByToken(accessToken)
if user == nil || !user.IsEnabled() || !user.IsModerator() {
accessDenied(w)
return
}
handler(w, r)
})
}

View File

@@ -0,0 +1,24 @@
package middleware
import (
"net/http"
"strconv"
"github.com/owncast/owncast/utils"
)
// DisableCache writes the disable cache header on the responses.
func DisableCache(w http.ResponseWriter) {
w.Header().Set("Cache-Control", "no-cache, no-store, must-revalidate")
w.Header().Set("Expires", "Thu, 1 Jan 1970 00:00:00 GMT")
}
func setCacheSeconds(seconds int, w http.ResponseWriter) {
secondsStr := strconv.Itoa(seconds)
w.Header().Set("Cache-Control", "public, max-age="+secondsStr)
}
// SetCachingHeaders will set the cache control header of a response.
func SetCachingHeaders(w http.ResponseWriter, r *http.Request) {
setCacheSeconds(utils.GetCacheDurationSecondsForPath(r.URL.Path), w)
}

View File

@@ -0,0 +1,10 @@
package middleware
import (
"net/http"
)
// EnableCors enables the CORS header on the responses.
func EnableCors(w http.ResponseWriter) {
w.Header().Set("Access-Control-Allow-Origin", "*")
}

View File

@@ -0,0 +1,17 @@
package middleware
import (
"fmt"
"net/http"
"strings"
)
// SetHeaders will set our global headers for web resources.
func SetHeaders(w http.ResponseWriter, nonce string) {
// Content security policy
csp := []string{
fmt.Sprintf("script-src '%s' 'self'", nonce),
"worker-src 'self' blob:", // No single quotes around blob:
}
w.Header().Set("Content-Security-Policy", strings.Join(csp, "; "))
}

View File

@@ -0,0 +1,39 @@
package middleware
import (
"net/http"
"strconv"
)
// PaginatedHandlerFunc is a handler for endpoints that require pagination.
type PaginatedHandlerFunc func(int, int, http.ResponseWriter, *http.Request)
// HandlePagination is a middleware handler that pulls pagination values
// and passes them along.
func HandlePagination(handler PaginatedHandlerFunc) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
// Default 50 items per page
limitString := r.URL.Query().Get("limit")
if limitString == "" {
limitString = "50"
}
limit, err := strconv.Atoi(limitString)
if err != nil {
w.WriteHeader(http.StatusBadRequest)
return
}
// Default first page 0
offsetString := r.URL.Query().Get("offset")
if offsetString == "" {
offsetString = "0"
}
offset, err := strconv.Atoi(offsetString)
if err != nil {
w.WriteHeader(http.StatusBadRequest)
return
}
handler(offset, limit, w, r)
}
}