Browse Source

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>
gek/ffmpeg-6
Gabe Kangas 2 years ago committed by GitHub
parent
commit
2217f0614a
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
  1. 4
      config/constants.go
  2. 36
      controllers/hls.go
  3. 1
      go.mod
  4. 2
      go.sum
  5. 27
      main.go
  6. 109
      router/router.go
  7. 1
      static/web/_next/static/-4yiuyziZt_OmyIpMdoZe/_buildManifest.js
  8. 1
      static/web/_next/static/-4yiuyziZt_OmyIpMdoZe/_ssgManifest.js
  9. 1
      static/web/_next/static/chunks/4281-f024035bb909af4e.js
  10. 1
      static/web/_next/static/chunks/5056-b14d7a3d2aee94c3.js
  11. 11
      static/web/_next/static/chunks/5283-23ec4c27947dae66.js
  12. 1
      static/web/_next/static/chunks/5584.08b6806ea3012a43.js
  13. 19
      static/web/_next/static/chunks/6017-bb9b0312a26c35dd.js
  14. 1
      static/web/_next/static/chunks/7268.461abbd8430d83c0.js
  15. 1
      static/web/_next/static/chunks/7521-9644c656878abfe6.js
  16. 1
      static/web/_next/static/chunks/9069.663a9f4ff16078e0.js
  17. 6
      static/web/_next/static/chunks/ee8b1517-4bb4890d4ff38215.js
  18. 1
      static/web/_next/static/chunks/pages/admin/upgrade-ba6e73bb555cba17.js
  19. 1
      static/web/_next/static/chunks/webpack-17c574c3c0f8700e.js
  20. 96
      test/automated/api/federation.test.js
  21. 116
      test/automated/tools.sh

4
config/constants.go

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

36
controllers/hls.go

@ -6,6 +6,7 @@ import ( @@ -6,6 +6,7 @@ import (
"path/filepath"
"strconv"
"strings"
"time"
"github.com/owncast/owncast/config"
"github.com/owncast/owncast/core"
@ -13,8 +14,19 @@ import ( @@ -13,8 +14,19 @@ import (
"github.com/owncast/owncast/models"
"github.com/owncast/owncast/router/middleware"
"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.
func HandleHLSRequest(w http.ResponseWriter, r *http.Request) {
// Sanity check to limit requests to HLS file types.
@ -23,6 +35,26 @@ func HandleHLSRequest(w http.ResponseWriter, r *http.Request) { @@ -23,6 +35,26 @@ func HandleHLSRequest(w http.ResponseWriter, r *http.Request) {
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
relativePath := strings.Replace(requestedPath, "/hls/", "", 1)
fullPath := filepath.Join(config.HLSStoragePath, relativePath)
@ -48,6 +80,10 @@ func HandleHLSRequest(w http.ResponseWriter, r *http.Request) { @@ -48,6 +80,10 @@ func HandleHLSRequest(w http.ResponseWriter, r *http.Request) {
} else {
cacheTime := utils.GetCacheDurationSecondsForPath(relativePath)
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)

1
go.mod

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

2
go.sum

@ -138,6 +138,8 @@ github.com/tklauser/numcpus v0.6.1/go.mod h1:1XfjsgE2zo8GVw7POkMbHENHzVg3GzmoZ9f @@ -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/valyala/gozstd v1.20.1 h1:xPnnnvjmaDDitMFfDxmQ4vpx0+3CdTg2o3lALvXTU/g=
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.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
github.com/yuin/goldmark v1.6.0 h1:boZcn2GTjpsynOsC0iJHnBWa4Bi0qzfJjthwauItG68=

27
main.go

@ -17,17 +17,18 @@ import ( @@ -17,17 +17,18 @@ import (
)
var (
dbFile = flag.String("database", "", "Path to the database file.")
logDirectory = flag.String("logdir", "", "Directory where logs will be written to")
backupDirectory = flag.String("backupdir", "", "Directory where backups will be written to")
enableDebugOptions = flag.Bool("enableDebugFeatures", false, "Enable additional debugging options.")
enableVerboseLogging = flag.Bool("enableVerboseLogging", false, "Enable additional logging.")
restoreDatabaseFile = flag.String("restoreDatabase", "", "Restore an Owncast database backup")
newAdminPassword = flag.String("adminpassword", "", "Set your admin password")
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")
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")
dbFile = flag.String("database", "", "Path to the database file.")
logDirectory = flag.String("logdir", "", "Directory where logs will be written to")
backupDirectory = flag.String("backupdir", "", "Directory where backups will be written to")
enableDebugOptions = flag.Bool("enableDebugFeatures", false, "Enable additional debugging options.")
enableVerboseLogging = flag.Bool("enableVerboseLogging", false, "Enable additional logging.")
restoreDatabaseFile = flag.String("restoreDatabase", "", "Restore an Owncast database backup")
newAdminPassword = flag.String("adminpassword", "", "Set your admin password")
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")
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")
disableResponseCaching = flag.Bool("disableResponseCaching", false, "Do not optimize performance by caching of web responses")
)
// nolint:cyclop
@ -42,6 +43,10 @@ func main() { @@ -42,6 +43,10 @@ func main() {
config.BackupDirectory = *backupDirectory
}
if *disableResponseCaching {
config.DisableResponseCaching = *disableResponseCaching
}
// Create the data directory if needed
if !utils.DoesFileExists("data") {
if err := os.Mkdir("./data", 0o700); err != nil {

109
router/router.go

@ -24,52 +24,131 @@ import ( @@ -24,52 +24,131 @@ import (
"github.com/owncast/owncast/router/middleware"
"github.com/owncast/owncast/utils"
"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.
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.
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.
http.HandleFunc("/admin/", middleware.RequireAdminAuth(controllers.IndexHandler))
// Images
http.HandleFunc("/thumbnail.jpg", controllers.GetThumbnail)
http.HandleFunc("/preview.gif", controllers.GetPreview)
http.HandleFunc("/logo", controllers.GetLogo)
http.HandleFunc("/thumbnail.jpg", func(rw http.ResponseWriter, r *http.Request) {
superShortCacheClient.Middleware(http.HandlerFunc(controllers.GetThumbnail)).ServeHTTP(rw, r)
})
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
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.
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 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
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
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
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
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
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
http.HandleFunc("/api/yp", yp.GetYPResponse)
// 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
http.HandleFunc("/api/video/variants", controllers.GetVideoStreamOutputVariants)
@ -84,7 +163,9 @@ func Start() error { @@ -84,7 +163,9 @@ func Start() error {
http.HandleFunc("/api/remotefollow", controllers.RemoteFollow)
// 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
http.HandleFunc("/api/metrics/playback", controllers.ReportPlaybackMetrics)

1
static/web/_next/static/-4yiuyziZt_OmyIpMdoZe/_buildManifest.js

File diff suppressed because one or more lines are too long

1
static/web/_next/static/-4yiuyziZt_OmyIpMdoZe/_ssgManifest.js

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

1
static/web/_next/static/chunks/4281-f024035bb909af4e.js vendored

File diff suppressed because one or more lines are too long

1
static/web/_next/static/chunks/5056-b14d7a3d2aee94c3.js vendored

File diff suppressed because one or more lines are too long

11
static/web/_next/static/chunks/5283-23ec4c27947dae66.js vendored

File diff suppressed because one or more lines are too long

1
static/web/_next/static/chunks/5584.08b6806ea3012a43.js vendored

@ -0,0 +1 @@ @@ -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)"})))}}}]);

19
static/web/_next/static/chunks/6017-bb9b0312a26c35dd.js vendored

File diff suppressed because one or more lines are too long

1
static/web/_next/static/chunks/7268.461abbd8430d83c0.js vendored

@ -0,0 +1 @@ @@ -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)"}))))}}}]);

1
static/web/_next/static/chunks/7521-9644c656878abfe6.js vendored

File diff suppressed because one or more lines are too long

1
static/web/_next/static/chunks/9069.663a9f4ff16078e0.js vendored

@ -0,0 +1 @@ @@ -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)"})))))}}}]);

6
static/web/_next/static/chunks/ee8b1517-4bb4890d4ff38215.js vendored

File diff suppressed because one or more lines are too long

1
static/web/_next/static/chunks/pages/admin/upgrade-ba6e73bb555cba17.js vendored

@ -0,0 +1 @@ @@ -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()}]);

1
static/web/_next/static/chunks/webpack-17c574c3c0f8700e.js vendored

File diff suppressed because one or more lines are too long

96
test/automated/api/federation.test.js

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

116
test/automated/tools.sh

@ -3,91 +3,91 @@ @@ -3,91 +3,91 @@
set -e
function install_ffmpeg() {
# install a specific version of ffmpeg
# install a specific version of ffmpeg
FFMPEG_VER="4.4.1"
FFMPEG_PATH="$(pwd)/ffmpeg-$FFMPEG_VER"
PATH=$FFMPEG_PATH:$PATH
FFMPEG_VER="4.4.1"
FFMPEG_PATH="$(pwd)/ffmpeg-$FFMPEG_VER"
PATH=$FFMPEG_PATH:$PATH
if ! [[ -d "$FFMPEG_PATH" ]]; then
mkdir "$FFMPEG_PATH"
fi
if ! [[ -d "$FFMPEG_PATH" ]]; then
mkdir "$FFMPEG_PATH"
fi
pushd "$FFMPEG_PATH" >/dev/null
pushd "$FFMPEG_PATH" >/dev/null
if [[ -x "$FFMPEG_PATH/ffmpeg" ]]; then
ffmpeg_version=$("$FFMPEG_PATH/ffmpeg" -version | awk -F 'ffmpeg version' '{print $2}' | awk 'NR==1{print $1}')
if [[ -x "$FFMPEG_PATH/ffmpeg" ]]; then
if [[ "$ffmpeg_version" == "$FFMPEG_VER-static" ]]; then
popd >/dev/null
return 0
else
mv "$FFMPEG_PATH/ffmpeg" "$FFMPEG_PATH/ffmpeg.bk" || rm -f "$FFMPEG_PATH/ffmpeg"
fi
fi
ffmpeg_version=$("$FFMPEG_PATH/ffmpeg" -version | awk -F 'ffmpeg version' '{print $2}' | awk 'NR==1{print $1}')
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
if [[ "$ffmpeg_version" == "$FFMPEG_VER-static" ]]; then
popd >/dev/null
return 0
else
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() {
# Build and run owncast from source
echo "Building owncast..."
pushd "$(git rev-parse --show-toplevel)" >/dev/null
go build -o owncast main.go
# Build and run owncast from source
echo "Building owncast..."
pushd "$(git rev-parse --show-toplevel)" >/dev/null
go build -o owncast main.go
echo "Running owncast..."
./owncast -database "$TEMP_DB" &
SERVER_PID=$!
popd >/dev/null
echo "Running owncast..."
./owncast -disableResponseCaching -database "$TEMP_DB" &
SERVER_PID=$!
popd >/dev/null
sleep 5
sleep 5
}
function start_stream() {
# Start streaming the test file over RTMP to the local owncast instance.
../../ocTestStream.sh &
STREAM_PID=$!
# Start streaming the test file over RTMP to the local owncast instance.
../../ocTestStream.sh &
STREAM_PID=$!
echo "Waiting for stream to start..."
sleep 12
echo "Waiting for stream to start..."
sleep 12
}
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
curl --fail 'http://localhost:8080/api/admin/config/s3' \
-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\":\"\"}}"
# Hard-coded to admin:abc123 for auth
curl --fail 'http://localhost:8080/api/admin/config/s3' \
-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\":\"\"}}"
}
function kill_with_kids() {
# kill a process and all its children (by pid)! return no error.
if [[ -n $1 ]]; then
mapfile -t CHILDREN_PID_LIST < <(ps --ppid "$1" -o pid= &>/dev/null || true)
for child_pid in "${CHILDREN_PID_LIST[@]}"; do
kill "$child_pid" &>/dev/null || true
wait "$child_pid" &>/dev/null || true
done
kill "$1" &>/dev/null || true
wait "$1" &>/dev/null || true
fi
# kill a process and all its children (by pid)! return no error.
if [[ -n $1 ]]; then
mapfile -t CHILDREN_PID_LIST < <(ps --ppid "$1" -o pid= &>/dev/null || true)
for child_pid in "${CHILDREN_PID_LIST[@]}"; do
kill "$child_pid" &>/dev/null || true
wait "$child_pid" &>/dev/null || true
done
kill "$1" &>/dev/null || true
wait "$1" &>/dev/null || true
fi
}
function finish() {
echo "Cleaning up..."
kill_with_kids "$STREAM_PID"
kill "$SERVER_PID" &>/dev/null || true
wait "$SERVER_PID" &>/dev/null || true
echo "Cleaning up..."
kill_with_kids "$STREAM_PID"
kill "$SERVER_PID" &>/dev/null || true
wait "$SERVER_PID" &>/dev/null || true
rm -fr "$TEMP_DB"
}

Loading…
Cancel
Save