feat(api): add server-side caching for requests that could benefit (#3463)

* feat(api): add server-side caching for requests that could benefit for them

* fix(tests): do not cache responses while in tests

* fix: remove commented out leftover code

* chore(deps): update dependency html-webpack-plugin to v5.5.4

* Bundle embedded web app

* fix: remove caching for web app assets under test

* chore(tests): re-enable temporarily disabled test

* chore(deps): update dependency typescript to v5.3.3

* Bundle embedded web app

* chore(deps): update dependency npm to v10.2.5

* Bundle embedded web app

---------

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Co-authored-by: Owncast <owncast@owncast.online>
This commit is contained in:
Gabe Kangas 2023-12-09 16:31:50 -08:00 committed by GitHub
parent b6efe49086
commit 2217f0614a
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
21 changed files with 316 additions and 119 deletions

View file

@ -30,4 +30,8 @@ var (
// PublicFilesPath is the optional directory for hosting public files. // PublicFilesPath is the optional directory for hosting public files.
PublicFilesPath = filepath.Join(DataDirectory, "public") PublicFilesPath = filepath.Join(DataDirectory, "public")
// DisableResponseCaching will disable caching of API and resource
// responses. Disable this feature to turn off the optimizations.
DisableResponseCaching = false
) )

View file

@ -6,6 +6,7 @@ import (
"path/filepath" "path/filepath"
"strconv" "strconv"
"strings" "strings"
"time"
"github.com/owncast/owncast/config" "github.com/owncast/owncast/config"
"github.com/owncast/owncast/core" "github.com/owncast/owncast/core"
@ -13,8 +14,19 @@ import (
"github.com/owncast/owncast/models" "github.com/owncast/owncast/models"
"github.com/owncast/owncast/router/middleware" "github.com/owncast/owncast/router/middleware"
"github.com/owncast/owncast/utils" "github.com/owncast/owncast/utils"
log "github.com/sirupsen/logrus"
cache "github.com/victorspringer/http-cache"
"github.com/victorspringer/http-cache/adapter/memory"
) )
type FileServerHandler struct {
HLSPath string
}
func (fsh *FileServerHandler) ServeHTTP(rw http.ResponseWriter, r *http.Request) {
http.ServeFile(rw, r, fsh.HLSPath)
}
// HandleHLSRequest will manage all requests to HLS content. // HandleHLSRequest will manage all requests to HLS content.
func HandleHLSRequest(w http.ResponseWriter, r *http.Request) { func HandleHLSRequest(w http.ResponseWriter, r *http.Request) {
// Sanity check to limit requests to HLS file types. // Sanity check to limit requests to HLS file types.
@ -23,6 +35,26 @@ func HandleHLSRequest(w http.ResponseWriter, r *http.Request) {
return return
} }
responseCache, err := memory.NewAdapter(
memory.AdapterWithAlgorithm(memory.LRU),
memory.AdapterWithCapacity(20),
memory.AdapterWithStorageCapacity(209_715_200),
)
if err != nil {
log.Warn("unable to create web cache", err)
}
// Since HLS segments cannot be changed once they're rendered, we can cache
// individual segments for a long time.
longTermHLSSegmentCache, err := cache.NewClient(
cache.ClientWithAdapter(responseCache),
cache.ClientWithTTL(30*time.Second),
cache.ClientWithExpiresHeader(),
)
if err != nil {
log.Warn("unable to create web cache client", err)
}
requestedPath := r.URL.Path requestedPath := r.URL.Path
relativePath := strings.Replace(requestedPath, "/hls/", "", 1) relativePath := strings.Replace(requestedPath, "/hls/", "", 1)
fullPath := filepath.Join(config.HLSStoragePath, relativePath) fullPath := filepath.Join(config.HLSStoragePath, relativePath)
@ -48,6 +80,10 @@ func HandleHLSRequest(w http.ResponseWriter, r *http.Request) {
} else { } else {
cacheTime := utils.GetCacheDurationSecondsForPath(relativePath) cacheTime := utils.GetCacheDurationSecondsForPath(relativePath)
w.Header().Set("Cache-Control", "public, max-age="+strconv.Itoa(cacheTime)) w.Header().Set("Cache-Control", "public, max-age="+strconv.Itoa(cacheTime))
fileServer := &FileServerHandler{HLSPath: fullPath}
longTermHLSSegmentCache.Middleware(fileServer).ServeHTTP(w, r)
return
} }
middleware.EnableCors(w) middleware.EnableCors(w)

1
go.mod
View file

@ -69,6 +69,7 @@ require (
github.com/jmespath/go-jmespath v0.4.0 // indirect github.com/jmespath/go-jmespath v0.4.0 // indirect
github.com/oschwald/maxminddb-golang v1.11.0 // indirect github.com/oschwald/maxminddb-golang v1.11.0 // indirect
github.com/shoenig/go-m1cpu v0.1.6 // indirect github.com/shoenig/go-m1cpu v0.1.6 // indirect
github.com/victorspringer/http-cache v0.0.0-20231006141456-6446fe59efba // indirect
) )
require ( require (

2
go.sum
View file

@ -138,6 +138,8 @@ github.com/tklauser/numcpus v0.6.1/go.mod h1:1XfjsgE2zo8GVw7POkMbHENHzVg3GzmoZ9f
github.com/ulikunitz/xz v0.5.11/go.mod h1:nbz6k7qbPmH4IRqmfOplQw/tblSgqTqBwxkY0oWt/14= github.com/ulikunitz/xz v0.5.11/go.mod h1:nbz6k7qbPmH4IRqmfOplQw/tblSgqTqBwxkY0oWt/14=
github.com/valyala/gozstd v1.20.1 h1:xPnnnvjmaDDitMFfDxmQ4vpx0+3CdTg2o3lALvXTU/g= github.com/valyala/gozstd v1.20.1 h1:xPnnnvjmaDDitMFfDxmQ4vpx0+3CdTg2o3lALvXTU/g=
github.com/valyala/gozstd v1.20.1/go.mod h1:y5Ew47GLlP37EkTB+B4s7r6A5rdaeB7ftbl9zoYiIPQ= github.com/valyala/gozstd v1.20.1/go.mod h1:y5Ew47GLlP37EkTB+B4s7r6A5rdaeB7ftbl9zoYiIPQ=
github.com/victorspringer/http-cache v0.0.0-20231006141456-6446fe59efba h1:+oqDKQIOdkkvro1psUKtI4oH9WBeKkGY2S8h9/lo288=
github.com/victorspringer/http-cache v0.0.0-20231006141456-6446fe59efba/go.mod h1:D1AD6nlXv7HkIfTVd8ZWK1KQEiXYNy/LbLkx8H9tIQw=
github.com/yuin/goldmark v1.3.7/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k= github.com/yuin/goldmark v1.3.7/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k=
github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
github.com/yuin/goldmark v1.6.0 h1:boZcn2GTjpsynOsC0iJHnBWa4Bi0qzfJjthwauItG68= github.com/yuin/goldmark v1.6.0 h1:boZcn2GTjpsynOsC0iJHnBWa4Bi0qzfJjthwauItG68=

27
main.go
View file

@ -17,17 +17,18 @@ import (
) )
var ( var (
dbFile = flag.String("database", "", "Path to the database file.") dbFile = flag.String("database", "", "Path to the database file.")
logDirectory = flag.String("logdir", "", "Directory where logs will be written to") logDirectory = flag.String("logdir", "", "Directory where logs will be written to")
backupDirectory = flag.String("backupdir", "", "Directory where backups will be written to") backupDirectory = flag.String("backupdir", "", "Directory where backups will be written to")
enableDebugOptions = flag.Bool("enableDebugFeatures", false, "Enable additional debugging options.") enableDebugOptions = flag.Bool("enableDebugFeatures", false, "Enable additional debugging options.")
enableVerboseLogging = flag.Bool("enableVerboseLogging", false, "Enable additional logging.") enableVerboseLogging = flag.Bool("enableVerboseLogging", false, "Enable additional logging.")
restoreDatabaseFile = flag.String("restoreDatabase", "", "Restore an Owncast database backup") restoreDatabaseFile = flag.String("restoreDatabase", "", "Restore an Owncast database backup")
newAdminPassword = flag.String("adminpassword", "", "Set your admin password") newAdminPassword = flag.String("adminpassword", "", "Set your admin password")
newStreamKey = flag.String("streamkey", "", "Set a temporary stream key for this session") newStreamKey = flag.String("streamkey", "", "Set a temporary stream key for this session")
webServerPortOverride = flag.String("webserverport", "", "Force the web server to listen on a specific port") webServerPortOverride = flag.String("webserverport", "", "Force the web server to listen on a specific port")
webServerIPOverride = flag.String("webserverip", "", "Force web server to listen on this IP address") webServerIPOverride = flag.String("webserverip", "", "Force web server to listen on this IP address")
rtmpPortOverride = flag.Int("rtmpport", 0, "Set listen port for the RTMP server") rtmpPortOverride = flag.Int("rtmpport", 0, "Set listen port for the RTMP server")
disableResponseCaching = flag.Bool("disableResponseCaching", false, "Do not optimize performance by caching of web responses")
) )
// nolint:cyclop // nolint:cyclop
@ -42,6 +43,10 @@ func main() {
config.BackupDirectory = *backupDirectory config.BackupDirectory = *backupDirectory
} }
if *disableResponseCaching {
config.DisableResponseCaching = *disableResponseCaching
}
// 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", 0o700); err != nil { if err := os.Mkdir("./data", 0o700); err != nil {

View file

@ -24,52 +24,131 @@ import (
"github.com/owncast/owncast/router/middleware" "github.com/owncast/owncast/router/middleware"
"github.com/owncast/owncast/utils" "github.com/owncast/owncast/utils"
"github.com/owncast/owncast/yp" "github.com/owncast/owncast/yp"
cache "github.com/victorspringer/http-cache"
"github.com/victorspringer/http-cache/adapter/memory"
) )
// Start starts the router for the http, ws, and rtmp. // Start starts the router for the http, ws, and rtmp.
func Start() error { func Start() error {
// Setup a web response cache
enableCache := !config.DisableResponseCaching
responseCache, err := memory.NewAdapter(
memory.AdapterWithAlgorithm(memory.LRU),
memory.AdapterWithCapacity(50),
)
if err != nil {
log.Warn("unable to create web cache", err)
}
superShortCacheClient, err := cache.NewClient(
cache.ClientWithAdapter(responseCache),
cache.ClientWithTTL(3*time.Second),
)
if err != nil {
log.Warn("unable to create web cache client", err)
}
reasonableDurationCacheClient, err := cache.NewClient(
cache.ClientWithAdapter(responseCache),
cache.ClientWithTTL(8*time.Second),
)
if err != nil {
log.Warn("unable to create web cache client", err)
}
longerDurationCacheClient, err := cache.NewClient(
cache.ClientWithAdapter(responseCache),
cache.ClientWithTTL(3*time.Minute),
)
if err != nil {
log.Warn("unable to create web cache client", err)
}
// The primary web app. // The primary web app.
http.HandleFunc("/", controllers.IndexHandler) if enableCache {
http.HandleFunc("/", func(rw http.ResponseWriter, r *http.Request) {
longerDurationCacheClient.Middleware(http.HandlerFunc(controllers.IndexHandler)).ServeHTTP(rw, r)
})
} else {
http.HandleFunc("/", controllers.IndexHandler)
}
// The admin web app. // The admin web app.
http.HandleFunc("/admin/", middleware.RequireAdminAuth(controllers.IndexHandler)) http.HandleFunc("/admin/", middleware.RequireAdminAuth(controllers.IndexHandler))
// Images // Images
http.HandleFunc("/thumbnail.jpg", controllers.GetThumbnail) http.HandleFunc("/thumbnail.jpg", func(rw http.ResponseWriter, r *http.Request) {
http.HandleFunc("/preview.gif", controllers.GetPreview) superShortCacheClient.Middleware(http.HandlerFunc(controllers.GetThumbnail)).ServeHTTP(rw, r)
http.HandleFunc("/logo", controllers.GetLogo) })
http.HandleFunc("/preview.gif", func(rw http.ResponseWriter, r *http.Request) {
superShortCacheClient.Middleware(http.HandlerFunc(controllers.GetPreview)).ServeHTTP(rw, r)
})
http.HandleFunc("/logo", func(rw http.ResponseWriter, r *http.Request) {
longerDurationCacheClient.Middleware(http.HandlerFunc(controllers.GetLogo)).ServeHTTP(rw, r)
})
// Custom Javascript // Custom Javascript
http.HandleFunc("/customjavascript", controllers.ServeCustomJavascript) http.HandleFunc("/customjavascript", func(rw http.ResponseWriter, r *http.Request) {
longerDurationCacheClient.Middleware(http.HandlerFunc(controllers.ServeCustomJavascript)).ServeHTTP(rw, r)
})
// Return a single emoji image. // Return a single emoji image.
http.HandleFunc(config.EmojiDir, controllers.GetCustomEmojiImage) http.HandleFunc(config.EmojiDir, func(rw http.ResponseWriter, r *http.Request) {
longerDurationCacheClient.Middleware(http.HandlerFunc(controllers.GetCustomEmojiImage)).ServeHTTP(rw, r)
})
// return the logo // return the logo
// return a logo that's compatible with external social networks // return a logo that's compatible with external social networks
http.HandleFunc("/logo/external", controllers.GetCompatibleLogo) http.HandleFunc("/logo/external", func(rw http.ResponseWriter, r *http.Request) {
longerDurationCacheClient.Middleware(http.HandlerFunc(controllers.GetCompatibleLogo)).ServeHTTP(rw, r)
})
// robots.txt // robots.txt
http.HandleFunc("/robots.txt", controllers.GetRobotsDotTxt) http.HandleFunc("/robots.txt", func(rw http.ResponseWriter, r *http.Request) {
longerDurationCacheClient.Middleware(http.HandlerFunc(controllers.GetRobotsDotTxt)).ServeHTTP(rw, r)
})
// status of the system // status of the system
http.HandleFunc("/api/status", controllers.GetStatus) if enableCache {
http.HandleFunc("/api/status", func(rw http.ResponseWriter, r *http.Request) {
superShortCacheClient.Middleware(http.HandlerFunc(controllers.GetStatus)).ServeHTTP(rw, r)
})
} else {
http.HandleFunc("/api/status", controllers.GetStatus)
}
// custom emoji supported in the chat // custom emoji supported in the chat
http.HandleFunc("/api/emoji", controllers.GetCustomEmojiList) http.HandleFunc("/api/emoji", func(rw http.ResponseWriter, r *http.Request) {
reasonableDurationCacheClient.Middleware(http.HandlerFunc(controllers.GetCustomEmojiList)).ServeHTTP(rw, r)
})
// chat rest api // chat rest api
http.HandleFunc("/api/chat", middleware.RequireUserAccessToken(controllers.GetChatMessages)) if enableCache {
http.HandleFunc("/api/chat", func(rw http.ResponseWriter, r *http.Request) {
superShortCacheClient.Middleware(middleware.RequireUserAccessToken(controllers.GetChatMessages))
})
} else {
http.HandleFunc("/api/chat", middleware.RequireUserAccessToken(controllers.GetChatMessages))
}
// web config api // web config api
http.HandleFunc("/api/config", controllers.GetWebConfig) if enableCache {
http.HandleFunc("/api/config", func(rw http.ResponseWriter, r *http.Request) {
superShortCacheClient.Middleware(http.HandlerFunc(controllers.GetWebConfig)).ServeHTTP(rw, r)
})
} else {
http.HandleFunc("/api/config", controllers.GetWebConfig)
}
// return the YP protocol data // return the YP protocol data
http.HandleFunc("/api/yp", yp.GetYPResponse) http.HandleFunc("/api/yp", yp.GetYPResponse)
// list of all social platforms // list of all social platforms
http.HandleFunc("/api/socialplatforms", controllers.GetAllSocialPlatforms) http.HandleFunc("/api/socialplatforms", func(rw http.ResponseWriter, r *http.Request) {
reasonableDurationCacheClient.Middleware(http.HandlerFunc(controllers.GetAllSocialPlatforms)).ServeHTTP(rw, r)
})
// return the list of video variants available // return the list of video variants available
http.HandleFunc("/api/video/variants", controllers.GetVideoStreamOutputVariants) http.HandleFunc("/api/video/variants", controllers.GetVideoStreamOutputVariants)
@ -84,7 +163,9 @@ func Start() error {
http.HandleFunc("/api/remotefollow", controllers.RemoteFollow) http.HandleFunc("/api/remotefollow", controllers.RemoteFollow)
// return followers // return followers
http.HandleFunc("/api/followers", middleware.HandlePagination(controllers.GetFollowers)) http.HandleFunc("/api/followers", func(rw http.ResponseWriter, r *http.Request) {
reasonableDurationCacheClient.Middleware(middleware.HandlePagination(controllers.GetFollowers)).ServeHTTP(rw, r)
})
// save client video playback metrics // save client video playback metrics
http.HandleFunc("/api/metrics/playback", controllers.ReportPlaybackMetrics) http.HandleFunc("/api/metrics/playback", controllers.ReportPlaybackMetrics)

File diff suppressed because one or more lines are too long

View file

@ -0,0 +1 @@
self.__SSG_MANIFEST=new Set,self.__SSG_MANIFEST_CB&&self.__SSG_MANIFEST_CB();

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View file

@ -0,0 +1 @@
"use strict";(self.webpackChunk_N_E=self.webpackChunk_N_E||[]).push([[5584],{15584:function(e,t,n){n.r(t);var r,a,s,o,i,l=n(67294);function _extends(){return(_extends=Object.assign?Object.assign.bind():function(e){for(var t=1;t<arguments.length;t++){var n=arguments[t];for(var r in n)Object.prototype.hasOwnProperty.call(n,r)&&(e[r]=n[r])}return e}).apply(this,arguments)}t.default=function(e){return l.createElement("svg",_extends({xmlns:"http://www.w3.org/2000/svg",width:500,height:500,viewBox:"0 0 132.292 132.292"},e),r||(r=l.createElement("linearGradient",{id:"like_svg__a",x1:432.851,x2:464.644,y1:49.977,y2:49.977,gradientUnits:"userSpaceOnUse"},l.createElement("stop",{offset:0,stopColor:"#2087e2"}),l.createElement("stop",{offset:1,stopColor:"#b63fff"}))),a||(a=l.createElement("path",{fill:"url(#like_svg__a)",d:"M438.672 34.08h20.151a5.82 5.82 45 0 1 5.82 5.821v20.151a5.82 5.82 135 0 1-5.82 5.821h-20.15a5.82 5.82 45 0 1-5.822-5.82V39.9a5.82 5.82 135 0 1 5.821-5.82z",transform:"matrix(4.1611 0 0 4.1611 -1801.14 -141.813)"})),s||(s=l.createElement("path",{fill:"#853dd0",d:"M106.243 25.198 110 33.435l5.378 24.12-20.557 29.696-28.676 20.66-35.66-24.468 49.536 48.849h28.048a24.221 24.221 0 0 0 24.222-24.222V44.165z",opacity:.75})),o||(o=l.createElement("path",{fill:"#8392ee",d:"M51.275 39.14s-36.386-7.356-17.999 25.83c13.869 25.032 29.59 23.091 29.59 23.091S47.512 65.822 51.275 39.14"})),i||(i=l.createElement("path",{fill:"none",stroke:"#fff",strokeLinecap:"round",strokeLinejoin:"round",strokeWidth:7.865,d:"M674.216 70.254c-21.056-22.863-45.943 2.215-45.943 2.215s-24.888-25.078-45.943-2.216c-21.056 22.863 16.89 64.133 45.943 78.023 29.053-13.89 66.998-55.16 45.943-78.022",transform:"matrix(.95455 0 0 .95455 -533.57 -33.626)"})))}}}]);

File diff suppressed because one or more lines are too long

View file

@ -0,0 +1 @@
"use strict";(self.webpackChunk_N_E=self.webpackChunk_N_E||[]).push([[7268],{97268:function(e,t,r){r.r(t);var a,n,s,o,l=r(67294);function _extends(){return(_extends=Object.assign?Object.assign.bind():function(e){for(var t=1;t<arguments.length;t++){var r=arguments[t];for(var a in r)Object.prototype.hasOwnProperty.call(r,a)&&(e[a]=r[a])}return e}).apply(this,arguments)}t.default=function(e){return l.createElement("svg",_extends({xmlns:"http://www.w3.org/2000/svg",width:500,height:500,viewBox:"0 0 132.292 132.292"},e),a||(a=l.createElement("linearGradient",{id:"repost_svg__a",x1:432.851,x2:464.644,y1:49.977,y2:49.977,gradientUnits:"userSpaceOnUse"},l.createElement("stop",{offset:0,stopColor:"#2087e2"}),l.createElement("stop",{offset:1,stopColor:"#b63fff"}))),n||(n=l.createElement("path",{fill:"url(#repost_svg__a)",d:"M438.672 34.08h20.151a5.82 5.82 45 0 1 5.82 5.821v20.151a5.82 5.82 135 0 1-5.82 5.821h-20.15a5.82 5.82 45 0 1-5.822-5.82V39.9a5.82 5.82 135 0 1 5.821-5.82z",transform:"matrix(4.16112 0 0 4.1611 -1801.146 -141.813)"})),s||(s=l.createElement("path",{fill:"#7f40cf",d:"m103.028 50.073-.794 41.033-10.18 12.882-49.412 3.477 26.027 24.827h39.4c13.378 0 24.223-10.845 24.222-24.222V68.265l-9.86-12.31z",opacity:.75})),o||(o=l.createElement("g",{fill:"none",stroke:"#fff",strokeLinecap:"round",strokeLinejoin:"round",strokeWidth:7.865},l.createElement("path",{d:"M741.453 94.965h-41.418a14.744 14.744 0 0 0-14.777 14.777v41.418m14.777 14.777h41.418c8.187 0 14.777-6.59 14.777-14.777v-41.418",transform:"translate(-696.642 -71.915)scale(1.05833)"}),l.createElement("path",{d:"m670.076 143.371 15.182 13.79 15.5-13.6",transform:"translate(-696.642 -71.915)scale(1.05833)"}),l.createElement("path",{d:"m670.076 143.371 15.182 13.79 15.5-13.6",transform:"rotate(180 414.466 105.278)scale(1.05833)"}))))}}}]);

File diff suppressed because one or more lines are too long

View file

@ -0,0 +1 @@
"use strict";(self.webpackChunk_N_E=self.webpackChunk_N_E||[]).push([[9069],{49069:function(e,t,r){r.r(t);var a,n,o,l,s=r(67294);function _extends(){return(_extends=Object.assign?Object.assign.bind():function(e){for(var t=1;t<arguments.length;t++){var r=arguments[t];for(var a in r)Object.prototype.hasOwnProperty.call(r,a)&&(e[a]=r[a])}return e}).apply(this,arguments)}t.default=function(e){return s.createElement("svg",_extends({xmlns:"http://www.w3.org/2000/svg",width:500,height:500,viewBox:"0 0 132.292 132.292"},e),a||(a=s.createElement("linearGradient",{id:"follow_svg__a",x1:432.851,x2:464.644,y1:49.977,y2:49.977,gradientUnits:"userSpaceOnUse"},s.createElement("stop",{offset:0,stopColor:"#2087e2"}),s.createElement("stop",{offset:1,stopColor:"#b63fff"}))),n||(n=s.createElement("path",{fill:"url(#follow_svg__a)",d:"M438.672 34.08h20.151a5.82 5.82 45 0 1 5.82 5.821v20.151a5.82 5.82 135 0 1-5.82 5.821h-20.15a5.82 5.82 45 0 1-5.822-5.82V39.9a5.82 5.82 135 0 1 5.821-5.82z",transform:"matrix(4.16112 0 0 4.1611 -1801.146 -141.813)"})),o||(o=s.createElement("path",{fill:"#8842da",d:"m99.29 73.002-1.238 22.769-22.423.995 25.259 35.526h7.183c13.377-.001 24.22-10.845 24.22-24.222V89.683z",opacity:.85})),l||(l=s.createElement("g",{stroke:"#fff",strokeLinecap:"round",strokeLinejoin:"round"},s.createElement("circle",{cx:876.218,cy:118.03,r:21.554,fill:"none",strokeWidth:8.788,transform:"matrix(.90817 0 0 .9124 -737.017 -65.428)"}),s.createElement("path",{fill:"none",strokeWidth:6.641,d:"M845.107 163.996c0-16.543 13.41-29.953 29.953-29.953a29.953 29.953 0 0 1 19.632 7.331",transform:"matrix(1.14743 0 0 1.26483 -944.188 -103.004)"}),s.createElement("g",{fill:"#fff",strokeWidth:7.559},s.createElement("path",{d:"m881.641 159.874 34.92.28",transform:"translate(-853.609 -74.031)scale(1.05833)"}),s.createElement("path",{d:"m881.641 159.874 34.92.28",transform:"rotate(90 561.76 -294.47)scale(1.05833)"})))))}}}]);

File diff suppressed because one or more lines are too long

View file

@ -0,0 +1 @@
(self.webpackChunk_N_E=self.webpackChunk_N_E||[]).push([[9262],{70918:function(e,t,a){(window.__NEXT_P=window.__NEXT_P||[]).push(["/admin/upgrade",function(){return a(7494)}])},7494:function(e,t,a){"use strict";a.r(t);var n=a(85893),s=a(67294),l=a(59899),r=a(2307),d=a(53740),u=a(92863),i=a(15578);let{Title:o}=d.default,AssetTable=e=>{let t=Object.values(e);return(0,n.jsx)(r.Z,{dataSource:t,columns:[{title:"Name",dataIndex:"name",key:"name",render:(e,t)=>(0,n.jsx)("a",{href:t.browser_download_url,children:e})},{title:"Size",dataIndex:"size",key:"size",render:e=>"".concat((e/1024/1024).toFixed(2)," MB")}],rowKey:e=>e.id,size:"large",pagination:!1})},Logs=()=>{let[e,t]=(0,s.useState)({html_url:"",name:"",created_at:null,body:"",assets:[]}),getRelease=async()=>{try{let e=await (0,u.Kt)();t(e)}catch(e){console.log("==== error",e)}};return((0,s.useEffect)(()=>{getRelease()},[]),e)?(0,n.jsxs)("div",{className:"upgrade-page",children:[(0,n.jsx)(o,{level:2,children:(0,n.jsx)("a",{href:e.html_url,children:e.name})}),(0,n.jsx)(o,{level:5,children:new Date(e.created_at).toDateString()}),(0,n.jsx)(l.U,{children:e.body}),(0,n.jsx)("h3",{children:"Downloads"}),(0,n.jsx)(AssetTable,{...e.assets})]}):null};Logs.getLayout=function(e){return(0,n.jsx)(i.l,{page:e})},t.default=Logs}},function(e){e.O(0,[5596,1130,4104,9403,1024,3942,971,6697,1664,1749,1700,2122,7752,5891,2891,4749,6627,8966,7521,5578,9774,2888,179],function(){return e(e.s=70918)}),_N_E=e.O()}]);

File diff suppressed because one or more lines are too long

View file

@ -9,9 +9,9 @@ request = request('http://127.0.0.1:8080');
var ajv = new Ajv(); var ajv = new Ajv();
var nodeInfoSchema = jsonfile.readFileSync('schema/nodeinfo_2.0.json'); var nodeInfoSchema = jsonfile.readFileSync('schema/nodeinfo_2.0.json');
const serverName = 'owncast.server.test' const serverName = 'owncast.server.test';
const serverURL = 'http://' + serverName const serverURL = 'http://' + serverName;
const fediUsername = 'streamer' const fediUsername = 'streamer';
test('disable federation', async (done) => { test('disable federation', async (done) => {
const res = await sendAdminRequest('config/federation/enable', false); const res = await sendAdminRequest('config/federation/enable', false);
@ -59,10 +59,7 @@ test('verify responses of /federation/ when federation is disabled', async (done
}); });
test('set required parameters and enable federation', async (done) => { test('set required parameters and enable federation', async (done) => {
const res1 = await sendAdminRequest( const res1 = await sendAdminRequest('config/serverurl', serverURL);
'config/serverurl',
serverURL
);
const res2 = await sendAdminRequest( const res2 = await sendAdminRequest(
'config/federation/username', 'config/federation/username',
fediUsername fediUsername
@ -73,28 +70,47 @@ test('set required parameters and enable federation', async (done) => {
test('verify responses of /.well-known/webfinger when federation is enabled', async (done) => { test('verify responses of /.well-known/webfinger when federation is enabled', async (done) => {
const resNoResource = request.get('/.well-known/webfinger').expect(400); const resNoResource = request.get('/.well-known/webfinger').expect(400);
const resBadResource = request.get( const resBadResource = request
'/.well-known/webfinger?resource=' + fediUsername + '@' + serverName .get('/.well-known/webfinger?resource=' + fediUsername + '@' + serverName)
).expect(400); .expect(400);
const resBadResource2 = request.get( const resBadResource2 = request
'/.well-known/webfinger?resource=notacct:' + fediUsername + '@' + serverName .get(
).expect(400); '/.well-known/webfinger?resource=notacct:' +
const resBadServer = request.get( fediUsername +
'/.well-known/webfinger?resource=acct:' + fediUsername + '@not' + serverName '@' +
).expect(404); serverName
const resBadUser = request.get( )
'/.well-known/webfinger?resource=acct:not' + fediUsername + '@' + serverName .expect(400);
).expect(404); const resBadServer = request
const resNoAccept = request.get( .get(
'/.well-known/webfinger?resource=acct:' + fediUsername + '@' + serverName '/.well-known/webfinger?resource=acct:' +
).expect(200) fediUsername +
'@not' +
serverName
)
.expect(404);
const resBadUser = request
.get(
'/.well-known/webfinger?resource=acct:not' +
fediUsername +
'@' +
serverName
)
.expect(404);
const resNoAccept = request
.get(
'/.well-known/webfinger?resource=acct:' + fediUsername + '@' + serverName
)
.expect(200)
.expect('Content-Type', /json/) .expect('Content-Type', /json/)
.then((res) => { .then((res) => {
parseJson(res.text); parseJson(res.text);
}); });
const resWithAccept = request.get( const resWithAccept = request
'/.well-known/webfinger?resource=acct:' + fediUsername + '@' + serverName .get(
).expect(200) '/.well-known/webfinger?resource=acct:' + fediUsername + '@' + serverName
)
.expect(200)
.set('Accept', 'application/json') .set('Accept', 'application/json')
.expect('Content-Type', /json/) .expect('Content-Type', /json/)
.then((res) => { .then((res) => {
@ -104,14 +120,16 @@ test('verify responses of /.well-known/webfinger when federation is enabled', as
}); });
test('verify responses of /.well-known/host-meta when federation is enabled', async (done) => { test('verify responses of /.well-known/host-meta when federation is enabled', async (done) => {
const res = request.get('/.well-known/host-meta') const res = request
.get('/.well-known/host-meta')
.expect(200) .expect(200)
.expect('Content-Type', /xml/); .expect('Content-Type', /xml/);
done(); done();
}); });
test('verify responses of /.well-known/nodeinfo when federation is enabled', async (done) => { test('verify responses of /.well-known/nodeinfo when federation is enabled', async (done) => {
const res = request.get('/.well-known/nodeinfo') const res = request
.get('/.well-known/nodeinfo')
.expect(200) .expect(200)
.expect('Content-Type', /json/) .expect('Content-Type', /json/)
.then((res) => { .then((res) => {
@ -121,7 +139,8 @@ test('verify responses of /.well-known/nodeinfo when federation is enabled', asy
}); });
test('verify responses of /.well-known/x-nodeinfo2 when federation is enabled', async (done) => { test('verify responses of /.well-known/x-nodeinfo2 when federation is enabled', async (done) => {
const res = request.get('/.well-known/x-nodeinfo2') const res = request
.get('/.well-known/x-nodeinfo2')
.expect(200) .expect(200)
.expect('Content-Type', /json/) .expect('Content-Type', /json/)
.then((res) => { .then((res) => {
@ -143,7 +162,8 @@ test('verify responses of /nodeinfo/2.0 when federation is enabled', async (done
}); });
test('verify responses of /api/v1/instance when federation is enabled', async (done) => { test('verify responses of /api/v1/instance when federation is enabled', async (done) => {
const res = request.get('/api/v1/instance') const res = request
.get('/api/v1/instance')
.expect(200) .expect(200)
.expect('Content-Type', /json/) .expect('Content-Type', /json/)
.then((res) => { .then((res) => {
@ -153,15 +173,17 @@ test('verify responses of /api/v1/instance when federation is enabled', async (d
}); });
test('verify responses of /federation/user/ when federation is enabled', async (done) => { test('verify responses of /federation/user/ when federation is enabled', async (done) => {
const resNoAccept = request.get('/federation/user/') const resNoAccept = request.get('/federation/user/').expect(307);
.expect(307); const resWithAccept = request
const resWithAccept = request.get('/federation/user/') .get('/federation/user/')
.set('Accept', 'application/json') .set('Accept', 'application/json')
.expect(404); .expect(404);
const resWithAcceptWrongUsername = request.get('/federation/user/not' + fediUsername) const resWithAcceptWrongUsername = request
.get('/federation/user/not' + fediUsername)
.set('Accept', 'application/json') .set('Accept', 'application/json')
.expect(404); .expect(404);
const resWithAcceptUsername = request.get('/federation/user/' + fediUsername) const resWithAcceptUsername = request
.get('/federation/user/' + fediUsername)
.set('Accept', 'application/json') .set('Accept', 'application/json')
.expect(200) .expect(200)
.expect('Content-Type', /json/) .expect('Content-Type', /json/)
@ -172,9 +194,9 @@ test('verify responses of /federation/user/ when federation is enabled', async (
}); });
test('verify responses of /federation/ when federation is enabled', async (done) => { test('verify responses of /federation/ when federation is enabled', async (done) => {
const resNoAccept = request.get('/federation/') const resNoAccept = request.get('/federation/').expect(307);
.expect(307); const resWithAccept = request
const resWithAccept = request.get('/federation/') .get('/federation/')
.set('Accept', 'application/json') .set('Accept', 'application/json')
.expect(404); .expect(404);
done(); done();

View file

@ -3,91 +3,91 @@
set -e set -e
function install_ffmpeg() { function install_ffmpeg() {
# install a specific version of ffmpeg # install a specific version of ffmpeg
FFMPEG_VER="4.4.1" FFMPEG_VER="4.4.1"
FFMPEG_PATH="$(pwd)/ffmpeg-$FFMPEG_VER" FFMPEG_PATH="$(pwd)/ffmpeg-$FFMPEG_VER"
PATH=$FFMPEG_PATH:$PATH PATH=$FFMPEG_PATH:$PATH
if ! [[ -d "$FFMPEG_PATH" ]]; then if ! [[ -d "$FFMPEG_PATH" ]]; then
mkdir "$FFMPEG_PATH" mkdir "$FFMPEG_PATH"
fi fi
pushd "$FFMPEG_PATH" >/dev/null pushd "$FFMPEG_PATH" >/dev/null
if [[ -x "$FFMPEG_PATH/ffmpeg" ]]; then if [[ -x "$FFMPEG_PATH/ffmpeg" ]]; then
ffmpeg_version=$("$FFMPEG_PATH/ffmpeg" -version | awk -F 'ffmpeg version' '{print $2}' | awk 'NR==1{print $1}')
if [[ "$ffmpeg_version" == "$FFMPEG_VER-static" ]]; then ffmpeg_version=$("$FFMPEG_PATH/ffmpeg" -version | awk -F 'ffmpeg version' '{print $2}' | awk 'NR==1{print $1}')
popd >/dev/null
return 0
else
mv "$FFMPEG_PATH/ffmpeg" "$FFMPEG_PATH/ffmpeg.bk" || rm -f "$FFMPEG_PATH/ffmpeg"
fi
fi
rm -f ffmpeg.zip if [[ "$ffmpeg_version" == "$FFMPEG_VER-static" ]]; then
curl -sL --fail https://github.com/ffbinaries/ffbinaries-prebuilt/releases/download/v${FFMPEG_VER}/ffmpeg-${FFMPEG_VER}-linux-64.zip --output ffmpeg.zip >/dev/null popd >/dev/null
unzip -o ffmpeg.zip >/dev/null && rm -f ffmpeg.zip return 0
chmod +x ffmpeg else
PATH=$FFMPEG_PATH:$PATH mv "$FFMPEG_PATH/ffmpeg" "$FFMPEG_PATH/ffmpeg.bk" || rm -f "$FFMPEG_PATH/ffmpeg"
fi
fi
popd >/dev/null rm -f ffmpeg.zip
curl -sL --fail https://github.com/ffbinaries/ffbinaries-prebuilt/releases/download/v${FFMPEG_VER}/ffmpeg-${FFMPEG_VER}-linux-64.zip --output ffmpeg.zip >/dev/null
unzip -o ffmpeg.zip >/dev/null && rm -f ffmpeg.zip
chmod +x ffmpeg
PATH=$FFMPEG_PATH:$PATH
popd >/dev/null
} }
function start_owncast() { function start_owncast() {
# Build and run owncast from source # Build and run owncast from source
echo "Building owncast..." echo "Building owncast..."
pushd "$(git rev-parse --show-toplevel)" >/dev/null pushd "$(git rev-parse --show-toplevel)" >/dev/null
go build -o owncast main.go go build -o owncast main.go
echo "Running owncast..." echo "Running owncast..."
./owncast -database "$TEMP_DB" & ./owncast -disableResponseCaching -database "$TEMP_DB" &
SERVER_PID=$! SERVER_PID=$!
popd >/dev/null popd >/dev/null
sleep 5 sleep 5
} }
function start_stream() { function start_stream() {
# Start streaming the test file over RTMP to the local owncast instance. # Start streaming the test file over RTMP to the local owncast instance.
../../ocTestStream.sh & ../../ocTestStream.sh &
STREAM_PID=$! STREAM_PID=$!
echo "Waiting for stream to start..." echo "Waiting for stream to start..."
sleep 12 sleep 12
} }
function update_storage_config() { function update_storage_config() {
echo "Configuring external storage to use ${S3_BUCKET}..." echo "Configuring external storage to use ${S3_BUCKET}..."
# Hard-coded to admin:abc123 for auth # Hard-coded to admin:abc123 for auth
curl --fail 'http://localhost:8080/api/admin/config/s3' \ curl --fail 'http://localhost:8080/api/admin/config/s3' \
-H 'Authorization: Basic YWRtaW46YWJjMTIz' \ -H 'Authorization: Basic YWRtaW46YWJjMTIz' \
--data-raw "{\"value\":{\"accessKey\":\"${S3_ACCESS_KEY}\",\"acl\":\"\",\"bucket\":\"${S3_BUCKET}\",\"enabled\":true,\"endpoint\":\"${S3_ENDPOINT}\",\"region\":\"${S3_REGION}\",\"secret\":\"${S3_SECRET}\",\"servingEndpoint\":\"\"}}" --data-raw "{\"value\":{\"accessKey\":\"${S3_ACCESS_KEY}\",\"acl\":\"\",\"bucket\":\"${S3_BUCKET}\",\"enabled\":true,\"endpoint\":\"${S3_ENDPOINT}\",\"region\":\"${S3_REGION}\",\"secret\":\"${S3_SECRET}\",\"servingEndpoint\":\"\"}}"
} }
function kill_with_kids() { function kill_with_kids() {
# kill a process and all its children (by pid)! return no error. # kill a process and all its children (by pid)! return no error.
if [[ -n $1 ]]; then if [[ -n $1 ]]; then
mapfile -t CHILDREN_PID_LIST < <(ps --ppid "$1" -o pid= &>/dev/null || true) mapfile -t CHILDREN_PID_LIST < <(ps --ppid "$1" -o pid= &>/dev/null || true)
for child_pid in "${CHILDREN_PID_LIST[@]}"; do for child_pid in "${CHILDREN_PID_LIST[@]}"; do
kill "$child_pid" &>/dev/null || true kill "$child_pid" &>/dev/null || true
wait "$child_pid" &>/dev/null || true wait "$child_pid" &>/dev/null || true
done done
kill "$1" &>/dev/null || true kill "$1" &>/dev/null || true
wait "$1" &>/dev/null || true wait "$1" &>/dev/null || true
fi fi
} }
function finish() { function finish() {
echo "Cleaning up..." echo "Cleaning up..."
kill_with_kids "$STREAM_PID" kill_with_kids "$STREAM_PID"
kill "$SERVER_PID" &>/dev/null || true kill "$SERVER_PID" &>/dev/null || true
wait "$SERVER_PID" &>/dev/null || true wait "$SERVER_PID" &>/dev/null || true
rm -fr "$TEMP_DB" rm -fr "$TEMP_DB"
} }