From 201ccc190dcfd2b44af2c92ff8c387a7ce2b0978 Mon Sep 17 00:00:00 2001 From: Gabe Kangas Date: Fri, 9 Jun 2023 13:25:04 -0700 Subject: [PATCH] feat: refactor web server + router. First step in #1749 --- activitypub/activitypub.go | 6 +- activitypub/router.go | 20 +- controllers/admin/serverConfig.go | 2 +- controllers/admin/status.go | 2 +- controllers/auth/indieauth/server.go | 2 +- controllers/chat.go | 2 +- controllers/config.go | 2 +- controllers/controllers.go | 2 +- controllers/hls.go | 2 +- controllers/index.go | 2 +- controllers/status.go | 2 +- main.go | 7 +- router/router.go | 444 ------------------ test/automated/api/publicstatic.test.js | 2 +- webserver/handlers/adminApi.go | 1 + webserver/handlers/directory.go | 1 + webserver/handlers/externalApi.go | 1 + webserver/handlers/handlers.go | 8 + webserver/handlers/hls.go | 1 + webserver/handlers/internalApi.go | 1 + webserver/handlers/static.go | 1 + webserver/handlers/testing.go | 7 + webserver/handlers/webAssets.go | 1 + .../middleware/activityPub.go | 0 {router => webserver}/middleware/auth.go | 0 {router => webserver}/middleware/caching.go | 0 {router => webserver}/middleware/cors.go | 0 {router => webserver}/middleware/headers.go | 0 .../middleware/pagination.go | 0 webserver/router.go | 420 +++++++++++++++++ webserver/webserver.go | 54 +++ webserver/webserver_test.go | 44 ++ 32 files changed, 568 insertions(+), 469 deletions(-) delete mode 100644 router/router.go create mode 100644 webserver/handlers/adminApi.go create mode 100644 webserver/handlers/directory.go create mode 100644 webserver/handlers/externalApi.go create mode 100644 webserver/handlers/handlers.go create mode 100644 webserver/handlers/hls.go create mode 100644 webserver/handlers/internalApi.go create mode 100644 webserver/handlers/static.go create mode 100644 webserver/handlers/testing.go create mode 100644 webserver/handlers/webAssets.go rename {router => webserver}/middleware/activityPub.go (100%) rename {router => webserver}/middleware/auth.go (100%) rename {router => webserver}/middleware/caching.go (100%) rename {router => webserver}/middleware/cors.go (100%) rename {router => webserver}/middleware/headers.go (100%) rename {router => webserver}/middleware/pagination.go (100%) create mode 100644 webserver/router.go create mode 100644 webserver/webserver.go create mode 100644 webserver/webserver_test.go diff --git a/activitypub/activitypub.go b/activitypub/activitypub.go index e8be9f961..e3271a0c8 100644 --- a/activitypub/activitypub.go +++ b/activitypub/activitypub.go @@ -1,6 +1,8 @@ package activitypub import ( + "net/http" + "github.com/owncast/owncast/activitypub/crypto" "github.com/owncast/owncast/activitypub/inbox" "github.com/owncast/owncast/activitypub/outbox" @@ -13,11 +15,11 @@ import ( ) // Start will initialize and start the federation support. -func Start(datastore *data.Datastore) { +func Start(datastore *data.Datastore, router *http.ServeMux) { persistence.Setup(datastore) workerpool.InitOutboundWorkerPool() inbox.InitInboxWorkerPool() - StartRouter() + StartRouter(router) // Generate the keys for signing federated activity if needed. if data.GetPrivateKey() == "" { diff --git a/activitypub/router.go b/activitypub/router.go index a7dc81d75..0d13b1115 100644 --- a/activitypub/router.go +++ b/activitypub/router.go @@ -4,32 +4,32 @@ import ( "net/http" "github.com/owncast/owncast/activitypub/controllers" - "github.com/owncast/owncast/router/middleware" + "github.com/owncast/owncast/webserver/middleware" ) // StartRouter will start the federation specific http router. -func StartRouter() { +func StartRouter(router *http.ServeMux) { // WebFinger - http.HandleFunc("/.well-known/webfinger", controllers.WebfingerHandler) + router.HandleFunc("/.well-known/webfinger", controllers.WebfingerHandler) // Host Metadata - http.HandleFunc("/.well-known/host-meta", controllers.HostMetaController) + router.HandleFunc("/.well-known/host-meta", controllers.HostMetaController) // Nodeinfo v1 - http.HandleFunc("/.well-known/nodeinfo", controllers.NodeInfoController) + router.HandleFunc("/.well-known/nodeinfo", controllers.NodeInfoController) // x-nodeinfo v2 - http.HandleFunc("/.well-known/x-nodeinfo2", controllers.XNodeInfo2Controller) + router.HandleFunc("/.well-known/x-nodeinfo2", controllers.XNodeInfo2Controller) // Nodeinfo v2 - http.HandleFunc("/nodeinfo/2.0", controllers.NodeInfoV2Controller) + router.HandleFunc("/nodeinfo/2.0", controllers.NodeInfoV2Controller) // Instance details - http.HandleFunc("/api/v1/instance", controllers.InstanceV1Controller) + router.HandleFunc("/api/v1/instance", controllers.InstanceV1Controller) // Single ActivityPub Actor - http.HandleFunc("/federation/user/", middleware.RequireActivityPubOrRedirect(controllers.ActorHandler)) + router.HandleFunc("/federation/user/", middleware.RequireActivityPubOrRedirect(controllers.ActorHandler)) // Single AP object - http.HandleFunc("/federation/", middleware.RequireActivityPubOrRedirect(controllers.ObjectHandler)) + router.HandleFunc("/federation/", middleware.RequireActivityPubOrRedirect(controllers.ObjectHandler)) } diff --git a/controllers/admin/serverConfig.go b/controllers/admin/serverConfig.go index 0a5269c63..071038100 100644 --- a/controllers/admin/serverConfig.go +++ b/controllers/admin/serverConfig.go @@ -8,8 +8,8 @@ import ( "github.com/owncast/owncast/core/data" "github.com/owncast/owncast/core/transcoder" "github.com/owncast/owncast/models" - "github.com/owncast/owncast/router/middleware" "github.com/owncast/owncast/utils" + "github.com/owncast/owncast/webserver/middleware" log "github.com/sirupsen/logrus" ) diff --git a/controllers/admin/status.go b/controllers/admin/status.go index 373a79aaa..6f30edf96 100644 --- a/controllers/admin/status.go +++ b/controllers/admin/status.go @@ -8,7 +8,7 @@ import ( "github.com/owncast/owncast/core/data" "github.com/owncast/owncast/metrics" "github.com/owncast/owncast/models" - "github.com/owncast/owncast/router/middleware" + "github.com/owncast/owncast/webserver/middleware" log "github.com/sirupsen/logrus" ) diff --git a/controllers/auth/indieauth/server.go b/controllers/auth/indieauth/server.go index 92cb4562f..c3179f9ff 100644 --- a/controllers/auth/indieauth/server.go +++ b/controllers/auth/indieauth/server.go @@ -6,7 +6,7 @@ import ( ia "github.com/owncast/owncast/auth/indieauth" "github.com/owncast/owncast/controllers" - "github.com/owncast/owncast/router/middleware" + "github.com/owncast/owncast/webserver/middleware" ) // HandleAuthEndpoint will handle the IndieAuth auth endpoint. diff --git a/controllers/chat.go b/controllers/chat.go index e8f2c7649..d2b8824ef 100644 --- a/controllers/chat.go +++ b/controllers/chat.go @@ -7,8 +7,8 @@ import ( "github.com/owncast/owncast/config" "github.com/owncast/owncast/core/chat" "github.com/owncast/owncast/core/user" - "github.com/owncast/owncast/router/middleware" "github.com/owncast/owncast/utils" + "github.com/owncast/owncast/webserver/middleware" log "github.com/sirupsen/logrus" ) diff --git a/controllers/config.go b/controllers/config.go index 41743ae2b..04a476687 100644 --- a/controllers/config.go +++ b/controllers/config.go @@ -10,8 +10,8 @@ import ( "github.com/owncast/owncast/config" "github.com/owncast/owncast/core/data" "github.com/owncast/owncast/models" - "github.com/owncast/owncast/router/middleware" "github.com/owncast/owncast/utils" + "github.com/owncast/owncast/webserver/middleware" log "github.com/sirupsen/logrus" ) diff --git a/controllers/controllers.go b/controllers/controllers.go index e5fb368fb..2c8faa3b4 100644 --- a/controllers/controllers.go +++ b/controllers/controllers.go @@ -5,7 +5,7 @@ import ( "net/http" "github.com/owncast/owncast/models" - "github.com/owncast/owncast/router/middleware" + "github.com/owncast/owncast/webserver/middleware" log "github.com/sirupsen/logrus" ) diff --git a/controllers/hls.go b/controllers/hls.go index 8b9651677..ba329db2c 100644 --- a/controllers/hls.go +++ b/controllers/hls.go @@ -11,8 +11,8 @@ import ( "github.com/owncast/owncast/core" "github.com/owncast/owncast/core/data" "github.com/owncast/owncast/models" - "github.com/owncast/owncast/router/middleware" "github.com/owncast/owncast/utils" + "github.com/owncast/owncast/webserver/middleware" ) // HandleHLSRequest will manage all requests to HLS content. diff --git a/controllers/index.go b/controllers/index.go index 18f44674d..1469d0054 100644 --- a/controllers/index.go +++ b/controllers/index.go @@ -8,9 +8,9 @@ import ( "strings" "github.com/owncast/owncast/core/data" - "github.com/owncast/owncast/router/middleware" "github.com/owncast/owncast/static" "github.com/owncast/owncast/utils" + "github.com/owncast/owncast/webserver/middleware" ) // IndexHandler handles the default index route. diff --git a/controllers/status.go b/controllers/status.go index 3914167f3..0db538e4b 100644 --- a/controllers/status.go +++ b/controllers/status.go @@ -7,8 +7,8 @@ import ( "github.com/owncast/owncast/core" "github.com/owncast/owncast/core/data" - "github.com/owncast/owncast/router/middleware" "github.com/owncast/owncast/utils" + "github.com/owncast/owncast/webserver/middleware" ) // GetStatus gets the status of the server. diff --git a/main.go b/main.go index a304d9d58..3d4489c39 100644 --- a/main.go +++ b/main.go @@ -6,13 +6,13 @@ import ( "strconv" "github.com/owncast/owncast/logging" + "github.com/owncast/owncast/webserver" log "github.com/sirupsen/logrus" "github.com/owncast/owncast/config" "github.com/owncast/owncast/core" "github.com/owncast/owncast/core/data" "github.com/owncast/owncast/metrics" - "github.com/owncast/owncast/router" "github.com/owncast/owncast/utils" ) @@ -105,8 +105,9 @@ func main() { go metrics.Start(core.GetStatus) - if err := router.Start(); err != nil { - log.Fatalln("failed to start/run the router", err) + webserver := webserver.New() + if err := webserver.Start(config.WebServerIP, config.WebServerPort); err != nil { + log.Fatalln("failed to start/run the web server", err) } } diff --git a/router/router.go b/router/router.go deleted file mode 100644 index c1866eb5f..000000000 --- a/router/router.go +++ /dev/null @@ -1,444 +0,0 @@ -package router - -import ( - "fmt" - "net/http" - "time" - - "github.com/CAFxX/httpcompression" - "github.com/prometheus/client_golang/prometheus/promhttp" - log "github.com/sirupsen/logrus" - "golang.org/x/net/http2" - "golang.org/x/net/http2/h2c" - - "github.com/owncast/owncast/activitypub" - "github.com/owncast/owncast/config" - "github.com/owncast/owncast/controllers" - "github.com/owncast/owncast/controllers/admin" - fediverseauth "github.com/owncast/owncast/controllers/auth/fediverse" - "github.com/owncast/owncast/controllers/auth/indieauth" - "github.com/owncast/owncast/controllers/moderation" - "github.com/owncast/owncast/core/chat" - "github.com/owncast/owncast/core/data" - "github.com/owncast/owncast/core/user" - "github.com/owncast/owncast/router/middleware" - "github.com/owncast/owncast/utils" - "github.com/owncast/owncast/yp" -) - -// Start starts the router for the http, ws, and rtmp. -func Start() error { - // The primary web app. - 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) - - // Custom Javascript - http.HandleFunc("/customjavascript", controllers.ServeCustomJavascript) - - // Return a single emoji image. - http.HandleFunc(config.EmojiDir, controllers.GetCustomEmojiImage) - - // return the logo - - // return a logo that's compatible with external social networks - http.HandleFunc("/logo/external", controllers.GetCompatibleLogo) - - // robots.txt - http.HandleFunc("/robots.txt", controllers.GetRobotsDotTxt) - - // status of the system - http.HandleFunc("/api/status", controllers.GetStatus) - - // custom emoji supported in the chat - http.HandleFunc("/api/emoji", controllers.GetCustomEmojiList) - - // chat rest api - http.HandleFunc("/api/chat", middleware.RequireUserAccessToken(controllers.GetChatMessages)) - - // web config api - 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) - - // return the list of video variants available - http.HandleFunc("/api/video/variants", controllers.GetVideoStreamOutputVariants) - - // tell the backend you're an active viewer - http.HandleFunc("/api/ping", controllers.Ping) - - // register a new chat user - http.HandleFunc("/api/chat/register", controllers.RegisterAnonymousChatUser) - - // return remote follow details - http.HandleFunc("/api/remotefollow", controllers.RemoteFollow) - - // return followers - http.HandleFunc("/api/followers", middleware.HandlePagination(controllers.GetFollowers)) - - // save client video playback metrics - http.HandleFunc("/api/metrics/playback", controllers.ReportPlaybackMetrics) - - // Register for notifications - http.HandleFunc("/api/notifications/register", middleware.RequireUserAccessToken(controllers.RegisterForLiveNotifications)) - - // Authenticated admin requests - - // Current inbound broadcaster - http.HandleFunc("/api/admin/status", middleware.RequireAdminAuth(admin.Status)) - - // Return HLS video - http.HandleFunc("/hls/", controllers.HandleHLSRequest) - - // Disconnect inbound stream - http.HandleFunc("/api/admin/disconnect", middleware.RequireAdminAuth(admin.DisconnectInboundConnection)) - - // Server config - http.HandleFunc("/api/admin/serverconfig", middleware.RequireAdminAuth(admin.GetServerConfig)) - - // Get viewer count over time - http.HandleFunc("/api/admin/viewersOverTime", middleware.RequireAdminAuth(admin.GetViewersOverTime)) - - // Get active viewers - http.HandleFunc("/api/admin/viewers", middleware.RequireAdminAuth(admin.GetActiveViewers)) - - // Get hardware stats - http.HandleFunc("/api/admin/hardwarestats", middleware.RequireAdminAuth(admin.GetHardwareStats)) - - // Get a a detailed list of currently connected chat clients - http.HandleFunc("/api/admin/chat/clients", middleware.RequireAdminAuth(admin.GetConnectedChatClients)) - - // Get all logs - http.HandleFunc("/api/admin/logs", middleware.RequireAdminAuth(admin.GetLogs)) - - // Get warning/error logs - http.HandleFunc("/api/admin/logs/warnings", middleware.RequireAdminAuth(admin.GetWarnings)) - - // Get all chat messages for the admin, unfiltered. - http.HandleFunc("/api/admin/chat/messages", middleware.RequireAdminAuth(admin.GetChatMessages)) - - // Update chat message visibility - http.HandleFunc("/api/admin/chat/messagevisibility", middleware.RequireAdminAuth(admin.UpdateMessageVisibility)) - - // Enable/disable a user - http.HandleFunc("/api/admin/chat/users/setenabled", middleware.RequireAdminAuth(admin.UpdateUserEnabled)) - - // Ban/unban an IP address - http.HandleFunc("/api/admin/chat/users/ipbans/create", middleware.RequireAdminAuth(admin.BanIPAddress)) - - // Remove an IP address ban - http.HandleFunc("/api/admin/chat/users/ipbans/remove", middleware.RequireAdminAuth(admin.UnBanIPAddress)) - - // Return all the banned IP addresses - http.HandleFunc("/api/admin/chat/users/ipbans", middleware.RequireAdminAuth(admin.GetIPAddressBans)) - - // Get a list of disabled users - http.HandleFunc("/api/admin/chat/users/disabled", middleware.RequireAdminAuth(admin.GetDisabledUsers)) - - // Set moderator status for a user - http.HandleFunc("/api/admin/chat/users/setmoderator", middleware.RequireAdminAuth(admin.UpdateUserModerator)) - - // Get a list of moderator users - http.HandleFunc("/api/admin/chat/users/moderators", middleware.RequireAdminAuth(admin.GetModerators)) - - // return followers - http.HandleFunc("/api/admin/followers", middleware.RequireAdminAuth(middleware.HandlePagination(controllers.GetFollowers))) - - // Get a list of pending follow requests - http.HandleFunc("/api/admin/followers/pending", middleware.RequireAdminAuth(admin.GetPendingFollowRequests)) - - // Get a list of rejected or blocked follows - http.HandleFunc("/api/admin/followers/blocked", middleware.RequireAdminAuth(admin.GetBlockedAndRejectedFollowers)) - - // Set the following state of a follower or follow request. - http.HandleFunc("/api/admin/followers/approve", middleware.RequireAdminAuth(admin.ApproveFollower)) - - // Upload custom emoji - http.HandleFunc("/api/admin/emoji/upload", middleware.RequireAdminAuth(admin.UploadCustomEmoji)) - - // Delete custom emoji - http.HandleFunc("/api/admin/emoji/delete", middleware.RequireAdminAuth(admin.DeleteCustomEmoji)) - - // Update config values - - // Change the current streaming key in memory - 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 - http.HandleFunc("/api/admin/config/pagecontent", middleware.RequireAdminAuth(admin.SetExtraPageContent)) - - // Stream title - http.HandleFunc("/api/admin/config/streamtitle", middleware.RequireAdminAuth(admin.SetStreamTitle)) - - // Server name - http.HandleFunc("/api/admin/config/name", middleware.RequireAdminAuth(admin.SetServerName)) - - // Server summary - http.HandleFunc("/api/admin/config/serversummary", middleware.RequireAdminAuth(admin.SetServerSummary)) - - // Offline message - http.HandleFunc("/api/admin/config/offlinemessage", middleware.RequireAdminAuth(admin.SetCustomOfflineMessage)) - - // Server welcome message - http.HandleFunc("/api/admin/config/welcomemessage", middleware.RequireAdminAuth(admin.SetServerWelcomeMessage)) - - // Disable chat - http.HandleFunc("/api/admin/config/chat/disable", middleware.RequireAdminAuth(admin.SetChatDisabled)) - - // Disable chat user join messages - http.HandleFunc("/api/admin/config/chat/joinmessagesenabled", middleware.RequireAdminAuth(admin.SetChatJoinMessagesEnabled)) - - // Enable/disable chat established user mode - http.HandleFunc("/api/admin/config/chat/establishedusermode", middleware.RequireAdminAuth(admin.SetEnableEstablishedChatUserMode)) - - // Set chat usernames that are not allowed - http.HandleFunc("/api/admin/config/chat/forbiddenusernames", middleware.RequireAdminAuth(admin.SetForbiddenUsernameList)) - - // Set the suggested chat usernames that will be assigned automatically - http.HandleFunc("/api/admin/config/chat/suggestedusernames", middleware.RequireAdminAuth(admin.SetSuggestedUsernameList)) - - // Set video codec - http.HandleFunc("/api/admin/config/video/codec", middleware.RequireAdminAuth(admin.SetVideoCodec)) - - // Set style/color/css values - http.HandleFunc("/api/admin/config/appearance", middleware.RequireAdminAuth(admin.SetCustomColorVariableValues)) - - // Return all webhooks - http.HandleFunc("/api/admin/webhooks", middleware.RequireAdminAuth(admin.GetWebhooks)) - - // Delete a single webhook - http.HandleFunc("/api/admin/webhooks/delete", middleware.RequireAdminAuth(admin.DeleteWebhook)) - - // Create a single webhook - http.HandleFunc("/api/admin/webhooks/create", middleware.RequireAdminAuth(admin.CreateWebhook)) - - // Get all access tokens - http.HandleFunc("/api/admin/accesstokens", middleware.RequireAdminAuth(admin.GetExternalAPIUsers)) - - // Delete a single access token - http.HandleFunc("/api/admin/accesstokens/delete", middleware.RequireAdminAuth(admin.DeleteExternalAPIUser)) - - // Create a single access token - http.HandleFunc("/api/admin/accesstokens/create", middleware.RequireAdminAuth(admin.CreateExternalAPIUser)) - - // Return the auto-update features that are supported for this instance. - http.HandleFunc("/api/admin/update/options", middleware.RequireAdminAuth(admin.AutoUpdateOptions)) - - // Begin the auto update - http.HandleFunc("/api/admin/update/start", middleware.RequireAdminAuth(admin.AutoUpdateStart)) - - // Force quit the service to restart it - http.HandleFunc("/api/admin/update/forcequit", middleware.RequireAdminAuth(admin.AutoUpdateForceQuit)) - - // Send a system message to chat - http.HandleFunc("/api/integrations/chat/system", middleware.RequireExternalAPIAccessToken(user.ScopeCanSendSystemMessages, admin.SendSystemMessage)) - - // Send a system message to a single client - http.HandleFunc(utils.RestEndpoint("/api/integrations/chat/system/client/{clientId}", middleware.RequireExternalAPIAccessToken(user.ScopeCanSendSystemMessages, admin.SendSystemMessageToConnectedClient))) - - // Send a user message to chat *NO LONGER SUPPORTED - http.HandleFunc("/api/integrations/chat/user", middleware.RequireExternalAPIAccessToken(user.ScopeCanSendChatMessages, admin.SendUserMessage)) - - // Send a message to chat as a specific 3rd party bot/integration based on its access token - http.HandleFunc("/api/integrations/chat/send", middleware.RequireExternalAPIAccessToken(user.ScopeCanSendChatMessages, admin.SendIntegrationChatMessage)) - - // Send a user action to chat - http.HandleFunc("/api/integrations/chat/action", middleware.RequireExternalAPIAccessToken(user.ScopeCanSendSystemMessages, admin.SendChatAction)) - - // Hide chat message - http.HandleFunc("/api/integrations/chat/messagevisibility", middleware.RequireExternalAPIAccessToken(user.ScopeHasAdminAccess, admin.ExternalUpdateMessageVisibility)) - - // Stream title - http.HandleFunc("/api/integrations/streamtitle", middleware.RequireExternalAPIAccessToken(user.ScopeHasAdminAccess, admin.ExternalSetStreamTitle)) - - // Get chat history - http.HandleFunc("/api/integrations/chat", middleware.RequireExternalAPIAccessToken(user.ScopeHasAdminAccess, controllers.ExternalGetChatMessages)) - - // Connected clients - http.HandleFunc("/api/integrations/clients", middleware.RequireExternalAPIAccessToken(user.ScopeHasAdminAccess, admin.ExternalGetConnectedChatClients)) - - // Logo path - http.HandleFunc("/api/admin/config/logo", middleware.RequireAdminAuth(admin.SetLogo)) - - // Server tags - http.HandleFunc("/api/admin/config/tags", middleware.RequireAdminAuth(admin.SetTags)) - - // ffmpeg - http.HandleFunc("/api/admin/config/ffmpegpath", middleware.RequireAdminAuth(admin.SetFfmpegPath)) - - // Server http port - http.HandleFunc("/api/admin/config/webserverport", middleware.RequireAdminAuth(admin.SetWebServerPort)) - - // Server http listen address - http.HandleFunc("/api/admin/config/webserverip", middleware.RequireAdminAuth(admin.SetWebServerIP)) - - // Server rtmp port - http.HandleFunc("/api/admin/config/rtmpserverport", middleware.RequireAdminAuth(admin.SetRTMPServerPort)) - - // Websocket host override - http.HandleFunc("/api/admin/config/sockethostoverride", middleware.RequireAdminAuth(admin.SetSocketHostOverride)) - - // Custom video serving endpoint - http.HandleFunc("/api/admin/config/videoservingendpoint", middleware.RequireAdminAuth(admin.SetVideoServingEndpoint)) - - // Is server marked as NSFW - http.HandleFunc("/api/admin/config/nsfw", middleware.RequireAdminAuth(admin.SetNSFW)) - - // directory enabled - http.HandleFunc("/api/admin/config/directoryenabled", middleware.RequireAdminAuth(admin.SetDirectoryEnabled)) - - // social handles - http.HandleFunc("/api/admin/config/socialhandles", middleware.RequireAdminAuth(admin.SetSocialHandles)) - - // set the number of video segments and duration per segment in a playlist - http.HandleFunc("/api/admin/config/video/streamlatencylevel", middleware.RequireAdminAuth(admin.SetStreamLatencyLevel)) - - // set an array of video output configurations - http.HandleFunc("/api/admin/config/video/streamoutputvariants", middleware.RequireAdminAuth(admin.SetStreamOutputVariants)) - - // set s3 configuration - http.HandleFunc("/api/admin/config/s3", middleware.RequireAdminAuth(admin.SetS3Configuration)) - - // set server url - http.HandleFunc("/api/admin/config/serverurl", middleware.RequireAdminAuth(admin.SetServerURL)) - - // reset the YP registration - http.HandleFunc("/api/admin/yp/reset", middleware.RequireAdminAuth(admin.ResetYPRegistration)) - - // set external action links - http.HandleFunc("/api/admin/config/externalactions", middleware.RequireAdminAuth(admin.SetExternalActions)) - - // set custom style css - http.HandleFunc("/api/admin/config/customstyles", middleware.RequireAdminAuth(admin.SetCustomStyles)) - - // set custom style javascript - http.HandleFunc("/api/admin/config/customjavascript", middleware.RequireAdminAuth(admin.SetCustomJavascript)) - - // Video playback metrics - http.HandleFunc("/api/admin/metrics/video", middleware.RequireAdminAuth(admin.GetVideoPlaybackMetrics)) - - // Is the viewer count hidden from viewers - http.HandleFunc("/api/admin/config/hideviewercount", middleware.RequireAdminAuth(admin.SetHideViewerCount)) - - // set disabling of search indexing - http.HandleFunc("/api/admin/config/disablesearchindexing", middleware.RequireAdminAuth(admin.SetDisableSearchIndexing)) - - // Inline chat moderation actions - - // Update chat message visibility - http.HandleFunc("/api/chat/messagevisibility", middleware.RequireUserModerationScopeAccesstoken(admin.UpdateMessageVisibility)) - - // Enable/disable a user - http.HandleFunc("/api/chat/users/setenabled", middleware.RequireUserModerationScopeAccesstoken(admin.UpdateUserEnabled)) - - // Get a user's details - http.HandleFunc("/api/moderation/chat/user/", middleware.RequireUserModerationScopeAccesstoken(moderation.GetUserDetails)) - - // Configure Federation features - - // enable/disable federation features - http.HandleFunc("/api/admin/config/federation/enable", middleware.RequireAdminAuth(admin.SetFederationEnabled)) - - // set if federation activities are private - http.HandleFunc("/api/admin/config/federation/private", middleware.RequireAdminAuth(admin.SetFederationActivityPrivate)) - - // set if fediverse engagement appears in chat - http.HandleFunc("/api/admin/config/federation/showengagement", middleware.RequireAdminAuth(admin.SetFederationShowEngagement)) - - // set local federated username - http.HandleFunc("/api/admin/config/federation/username", middleware.RequireAdminAuth(admin.SetFederationUsername)) - - // set federated go live message - http.HandleFunc("/api/admin/config/federation/livemessage", middleware.RequireAdminAuth(admin.SetFederationGoLiveMessage)) - - // Federation blocked domains - http.HandleFunc("/api/admin/config/federation/blockdomains", middleware.RequireAdminAuth(admin.SetFederationBlockDomains)) - - // send a public message to the Fediverse from the server's user - http.HandleFunc("/api/admin/federation/send", middleware.RequireAdminAuth(admin.SendFederatedMessage)) - - // Return federated activities - http.HandleFunc("/api/admin/federation/actions", middleware.RequireAdminAuth(middleware.HandlePagination(admin.GetFederatedActions))) - - // Prometheus metrics - http.Handle("/api/admin/prometheus", middleware.RequireAdminAuth(func(rw http.ResponseWriter, r *http.Request) { - promhttp.Handler().ServeHTTP(rw, r) - })) - - // Configure outbound notification channels. - http.HandleFunc("/api/admin/config/notifications/discord", middleware.RequireAdminAuth(admin.SetDiscordNotificationConfiguration)) - http.HandleFunc("/api/admin/config/notifications/browser", middleware.RequireAdminAuth(admin.SetBrowserNotificationConfiguration)) - - // Auth - - // Start auth flow - http.HandleFunc("/api/auth/indieauth", middleware.RequireUserAccessToken(indieauth.StartAuthFlow)) - http.HandleFunc("/api/auth/indieauth/callback", indieauth.HandleRedirect) - http.HandleFunc("/api/auth/provider/indieauth", indieauth.HandleAuthEndpoint) - - http.HandleFunc("/api/auth/fediverse", middleware.RequireUserAccessToken(fediverseauth.RegisterFediverseOTPRequest)) - http.HandleFunc("/api/auth/fediverse/verify", fediverseauth.VerifyFediverseOTPRequest) - - // ActivityPub has its own router - activitypub.Start(data.GetDatastore()) - - // websocket - http.HandleFunc("/ws", func(w http.ResponseWriter, r *http.Request) { - chat.HandleClientConnection(w, r) - }) - - // Optional public static files - http.Handle("/public/", http.StripPrefix("/public/", http.FileServer(http.Dir(config.PublicFilesPath)))) - - port := config.WebServerPort - ip := config.WebServerIP - - h2s := &http2.Server{} - - // Create a custom mux handler to intercept the /debug/vars endpoint. - // This is a hack because Prometheus enables this endpoint by default - // due to its use of expvar and we do not want this exposed. - defaultMux := h2c.NewHandler(http.DefaultServeMux, h2s) - m := http.NewServeMux() - - m.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) { - if r.URL.Path == "/debug/vars" { - w.WriteHeader(http.StatusNotFound) - return - } else if r.URL.Path == "/embed/chat/" || r.URL.Path == "/embed/chat" { - // Redirect /embed/chat - http.Redirect(w, r, "/embed/chat/readonly", http.StatusTemporaryRedirect) - } else { - defaultMux.ServeHTTP(w, r) - } - }) - - compress, _ := httpcompression.DefaultAdapter() // Use the default configuration - server := &http.Server{ - Addr: fmt.Sprintf("%s:%d", ip, port), - ReadHeaderTimeout: 4 * time.Second, - Handler: compress(m), - } - - if ip != "0.0.0.0" { - log.Infof("Web server is listening at %s:%d.", ip, port) - } else { - log.Infof("Web server is listening on port %d.", port) - } - log.Infoln("Configure this server by visiting /admin.") - - return server.ListenAndServe() -} diff --git a/test/automated/api/publicstatic.test.js b/test/automated/api/publicstatic.test.js index e1e80fcce..b4bec451c 100644 --- a/test/automated/api/publicstatic.test.js +++ b/test/automated/api/publicstatic.test.js @@ -32,7 +32,7 @@ test('public directory is writable', async (done) => { done(); }); -test('public static file is accessible', async (done) => { +test('public static file is accessible: ' + filename, async (done) => { request .get('/public/' + filename) .expect(200) diff --git a/webserver/handlers/adminApi.go b/webserver/handlers/adminApi.go new file mode 100644 index 000000000..5ac8282f4 --- /dev/null +++ b/webserver/handlers/adminApi.go @@ -0,0 +1 @@ +package handlers diff --git a/webserver/handlers/directory.go b/webserver/handlers/directory.go new file mode 100644 index 000000000..5ac8282f4 --- /dev/null +++ b/webserver/handlers/directory.go @@ -0,0 +1 @@ +package handlers diff --git a/webserver/handlers/externalApi.go b/webserver/handlers/externalApi.go new file mode 100644 index 000000000..5ac8282f4 --- /dev/null +++ b/webserver/handlers/externalApi.go @@ -0,0 +1 @@ +package handlers diff --git a/webserver/handlers/handlers.go b/webserver/handlers/handlers.go new file mode 100644 index 000000000..0d23597a3 --- /dev/null +++ b/webserver/handlers/handlers.go @@ -0,0 +1,8 @@ +package handlers + +type Handlers struct{} + +// New creates a new instances of web server handlers. +func New() *Handlers { + return &Handlers{} +} diff --git a/webserver/handlers/hls.go b/webserver/handlers/hls.go new file mode 100644 index 000000000..5ac8282f4 --- /dev/null +++ b/webserver/handlers/hls.go @@ -0,0 +1 @@ +package handlers diff --git a/webserver/handlers/internalApi.go b/webserver/handlers/internalApi.go new file mode 100644 index 000000000..5ac8282f4 --- /dev/null +++ b/webserver/handlers/internalApi.go @@ -0,0 +1 @@ +package handlers diff --git a/webserver/handlers/static.go b/webserver/handlers/static.go new file mode 100644 index 000000000..5ac8282f4 --- /dev/null +++ b/webserver/handlers/static.go @@ -0,0 +1 @@ +package handlers diff --git a/webserver/handlers/testing.go b/webserver/handlers/testing.go new file mode 100644 index 000000000..7c543841d --- /dev/null +++ b/webserver/handlers/testing.go @@ -0,0 +1,7 @@ +package handlers + +import "net/http" + +func (s *Handlers) HandleTesting(w http.ResponseWriter, r *http.Request) { + _, _ = w.Write([]byte("testing")) +} diff --git a/webserver/handlers/webAssets.go b/webserver/handlers/webAssets.go new file mode 100644 index 000000000..5ac8282f4 --- /dev/null +++ b/webserver/handlers/webAssets.go @@ -0,0 +1 @@ +package handlers diff --git a/router/middleware/activityPub.go b/webserver/middleware/activityPub.go similarity index 100% rename from router/middleware/activityPub.go rename to webserver/middleware/activityPub.go diff --git a/router/middleware/auth.go b/webserver/middleware/auth.go similarity index 100% rename from router/middleware/auth.go rename to webserver/middleware/auth.go diff --git a/router/middleware/caching.go b/webserver/middleware/caching.go similarity index 100% rename from router/middleware/caching.go rename to webserver/middleware/caching.go diff --git a/router/middleware/cors.go b/webserver/middleware/cors.go similarity index 100% rename from router/middleware/cors.go rename to webserver/middleware/cors.go diff --git a/router/middleware/headers.go b/webserver/middleware/headers.go similarity index 100% rename from router/middleware/headers.go rename to webserver/middleware/headers.go diff --git a/router/middleware/pagination.go b/webserver/middleware/pagination.go similarity index 100% rename from router/middleware/pagination.go rename to webserver/middleware/pagination.go diff --git a/webserver/router.go b/webserver/router.go new file mode 100644 index 000000000..8e95f135a --- /dev/null +++ b/webserver/router.go @@ -0,0 +1,420 @@ +package webserver + +import ( + "net/http" + + "github.com/owncast/owncast/activitypub" + "github.com/owncast/owncast/config" + "github.com/owncast/owncast/controllers" + "github.com/owncast/owncast/controllers/admin" + fediverseauth "github.com/owncast/owncast/controllers/auth/fediverse" + "github.com/owncast/owncast/controllers/auth/indieauth" + "github.com/owncast/owncast/controllers/moderation" + "github.com/owncast/owncast/core/chat" + "github.com/owncast/owncast/core/data" + "github.com/owncast/owncast/core/user" + "github.com/owncast/owncast/utils" + "github.com/owncast/owncast/webserver/middleware" + "github.com/owncast/owncast/yp" + "github.com/prometheus/client_golang/prometheus/promhttp" +) + +func (s *webServer) setupRoutes() { + s.router.HandleFunc("/test", s.handlers.HandleTesting) + + s.setupWebAssetRoutes() + s.setupInternalAPIRoutes() + s.setupAdminAPIRoutes() + s.setupExternalThirdPartyAPIRoutes() + s.setupModerationAPIRoutes() + + s.router.HandleFunc("/hls/", controllers.HandleHLSRequest) + + // websocket + s.router.HandleFunc("/ws", func(w http.ResponseWriter, r *http.Request) { + chat.HandleClientConnection(w, r) + }) + + s.router.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) { + // This is a hack because Prometheus enables this endpoint by default + // due to its use of expvar and we do not want this exposed. + if r.URL.Path == "/debug/vars" { + w.WriteHeader(http.StatusNotFound) + return + } else if r.URL.Path == "/embed/chat/" || r.URL.Path == "/embed/chat" { + // Redirect /embed/chat + http.Redirect(w, r, "/embed/chat/readonly", http.StatusTemporaryRedirect) + } else { + controllers.IndexHandler(w, r) + // s.ServeHTTP(w, r) + } + }) + + // ActivityPub has its own router + activitypub.Start(data.GetDatastore(), s.router) +} + +func (s *webServer) setupWebAssetRoutes() { + // The admin web app. + s.router.HandleFunc("/admin/", middleware.RequireAdminAuth(controllers.IndexHandler)) + + // Images + s.router.HandleFunc("/thumbnail.jpg", controllers.GetThumbnail) + s.router.HandleFunc("/preview.gif", controllers.GetPreview) + s.router.HandleFunc("/logo", controllers.GetLogo) + + // Custom Javascript + s.router.HandleFunc("/customjavascript", controllers.ServeCustomJavascript) + + // Return a single emoji image. + s.router.HandleFunc(config.EmojiDir, controllers.GetCustomEmojiImage) + + // return the logo + + // return a logo that's compatible with external social networks + s.router.HandleFunc("/logo/external", controllers.GetCompatibleLogo) + + // robots.txt + s.router.HandleFunc("/robots.txt", controllers.GetRobotsDotTxt) + + // Optional public static files + s.router.Handle("/public/", http.StripPrefix("/public/", http.FileServer(http.Dir(config.PublicFilesPath)))) +} + +func (s *webServer) setupInternalAPIRoutes() { + // Internal APIs + + // status of the system + s.router.HandleFunc("/api/status", controllers.GetStatus) + + // custom emoji supported in the chat + s.router.HandleFunc("/api/emoji", controllers.GetCustomEmojiList) + + // chat history api + s.router.HandleFunc("/api/chat", middleware.RequireUserAccessToken(controllers.GetChatMessages)) + + // web config api + s.router.HandleFunc("/api/config", controllers.GetWebConfig) + + // return the YP protocol data + s.router.HandleFunc("/api/yp", yp.GetYPResponse) + + // list of all social platforms + s.router.HandleFunc("/api/socialplatforms", controllers.GetAllSocialPlatforms) + + // return the list of video variants available + s.router.HandleFunc("/api/video/variants", controllers.GetVideoStreamOutputVariants) + + // tell the backend you're an active viewer + s.router.HandleFunc("/api/ping", controllers.Ping) + + // register a new chat user + s.router.HandleFunc("/api/chat/register", controllers.RegisterAnonymousChatUser) + + // return remote follow details + s.router.HandleFunc("/api/remotefollow", controllers.RemoteFollow) + + // return followers + s.router.HandleFunc("/api/followers", middleware.HandlePagination(controllers.GetFollowers)) + + // save client video playback metrics + s.router.HandleFunc("/api/metrics/playback", controllers.ReportPlaybackMetrics) + + // Register for notifications + s.router.HandleFunc("/api/notifications/register", middleware.RequireUserAccessToken(controllers.RegisterForLiveNotifications)) + + // Start auth flow + http.HandleFunc("/api/auth/indieauth", middleware.RequireUserAccessToken(indieauth.StartAuthFlow)) + http.HandleFunc("/api/auth/indieauth/callback", indieauth.HandleRedirect) + http.HandleFunc("/api/auth/provider/indieauth", indieauth.HandleAuthEndpoint) + + http.HandleFunc("/api/auth/fediverse", middleware.RequireUserAccessToken(fediverseauth.RegisterFediverseOTPRequest)) + http.HandleFunc("/api/auth/fediverse/verify", fediverseauth.VerifyFediverseOTPRequest) +} + +func (s *webServer) setupAdminAPIRoutes() { + // Current inbound broadcaster + s.router.HandleFunc("/api/admin/status", middleware.RequireAdminAuth(admin.Status)) + + // Disconnect inbound stream + s.router.HandleFunc("/api/admin/disconnect", middleware.RequireAdminAuth(admin.DisconnectInboundConnection)) + + // Server config + s.router.HandleFunc("/api/admin/serverconfig", middleware.RequireAdminAuth(admin.GetServerConfig)) + + // Get viewer count over time + s.router.HandleFunc("/api/admin/viewersOverTime", middleware.RequireAdminAuth(admin.GetViewersOverTime)) + + // Get active viewers + s.router.HandleFunc("/api/admin/viewers", middleware.RequireAdminAuth(admin.GetActiveViewers)) + + // Get hardware stats + s.router.HandleFunc("/api/admin/hardwarestats", middleware.RequireAdminAuth(admin.GetHardwareStats)) + + // Get a a detailed list of currently connected chat clients + s.router.HandleFunc("/api/admin/chat/clients", middleware.RequireAdminAuth(admin.GetConnectedChatClients)) + + // Get all logs + s.router.HandleFunc("/api/admin/logs", middleware.RequireAdminAuth(admin.GetLogs)) + + // Get warning/error logs + s.router.HandleFunc("/api/admin/logs/warnings", middleware.RequireAdminAuth(admin.GetWarnings)) + + // Get all chat messages for the admin, unfiltered. + s.router.HandleFunc("/api/admin/chat/messages", middleware.RequireAdminAuth(admin.GetChatMessages)) + + // Update chat message visibility + s.router.HandleFunc("/api/admin/chat/messagevisibility", middleware.RequireAdminAuth(admin.UpdateMessageVisibility)) + + // Enable/disable a user + s.router.HandleFunc("/api/admin/chat/users/setenabled", middleware.RequireAdminAuth(admin.UpdateUserEnabled)) + + // Ban/unban an IP address + s.router.HandleFunc("/api/admin/chat/users/ipbans/create", middleware.RequireAdminAuth(admin.BanIPAddress)) + + // Remove an IP address ban + s.router.HandleFunc("/api/admin/chat/users/ipbans/remove", middleware.RequireAdminAuth(admin.UnBanIPAddress)) + + // Return all the banned IP addresses + s.router.HandleFunc("/api/admin/chat/users/ipbans", middleware.RequireAdminAuth(admin.GetIPAddressBans)) + + // Get a list of disabled users + s.router.HandleFunc("/api/admin/chat/users/disabled", middleware.RequireAdminAuth(admin.GetDisabledUsers)) + + // Set moderator status for a user + s.router.HandleFunc("/api/admin/chat/users/setmoderator", middleware.RequireAdminAuth(admin.UpdateUserModerator)) + + // Get a list of moderator users + s.router.HandleFunc("/api/admin/chat/users/moderators", middleware.RequireAdminAuth(admin.GetModerators)) + + // return followers + s.router.HandleFunc("/api/admin/followers", middleware.RequireAdminAuth(middleware.HandlePagination(controllers.GetFollowers))) + + // Get a list of pending follow requests + s.router.HandleFunc("/api/admin/followers/pending", middleware.RequireAdminAuth(admin.GetPendingFollowRequests)) + + // Get a list of rejected or blocked follows + s.router.HandleFunc("/api/admin/followers/blocked", middleware.RequireAdminAuth(admin.GetBlockedAndRejectedFollowers)) + + // Set the following state of a follower or follow request. + s.router.HandleFunc("/api/admin/followers/approve", middleware.RequireAdminAuth(admin.ApproveFollower)) + + // Upload custom emoji + s.router.HandleFunc("/api/admin/emoji/upload", middleware.RequireAdminAuth(admin.UploadCustomEmoji)) + + // Delete custom emoji + s.router.HandleFunc("/api/admin/emoji/delete", middleware.RequireAdminAuth(admin.DeleteCustomEmoji)) + + // Update config values + + // Change the current streaming key in memory + s.router.HandleFunc("/api/admin/config/adminpass", middleware.RequireAdminAuth(admin.SetAdminPassword)) + + // Set an array of valid stream keys + s.router.HandleFunc("/api/admin/config/streamkeys", middleware.RequireAdminAuth(admin.SetStreamKeys)) + + // Change the extra page content in memory + s.router.HandleFunc("/api/admin/config/pagecontent", middleware.RequireAdminAuth(admin.SetExtraPageContent)) + + // Stream title + s.router.HandleFunc("/api/admin/config/streamtitle", middleware.RequireAdminAuth(admin.SetStreamTitle)) + + // Server name + s.router.HandleFunc("/api/admin/config/name", middleware.RequireAdminAuth(admin.SetServerName)) + + // Server summary + s.router.HandleFunc("/api/admin/config/serversummary", middleware.RequireAdminAuth(admin.SetServerSummary)) + + // Offline message + s.router.HandleFunc("/api/admin/config/offlinemessage", middleware.RequireAdminAuth(admin.SetCustomOfflineMessage)) + + // Server welcome message + s.router.HandleFunc("/api/admin/config/welcomemessage", middleware.RequireAdminAuth(admin.SetServerWelcomeMessage)) + + // Disable chat + s.router.HandleFunc("/api/admin/config/chat/disable", middleware.RequireAdminAuth(admin.SetChatDisabled)) + + // Disable chat user join messages + s.router.HandleFunc("/api/admin/config/chat/joinmessagesenabled", middleware.RequireAdminAuth(admin.SetChatJoinMessagesEnabled)) + + // Enable/disable chat established user mode + s.router.HandleFunc("/api/admin/config/chat/establishedusermode", middleware.RequireAdminAuth(admin.SetEnableEstablishedChatUserMode)) + + // Set chat usernames that are not allowed + s.router.HandleFunc("/api/admin/config/chat/forbiddenusernames", middleware.RequireAdminAuth(admin.SetForbiddenUsernameList)) + + // Set the suggested chat usernames that will be assigned automatically + s.router.HandleFunc("/api/admin/config/chat/suggestedusernames", middleware.RequireAdminAuth(admin.SetSuggestedUsernameList)) + + // Set video codec + s.router.HandleFunc("/api/admin/config/video/codec", middleware.RequireAdminAuth(admin.SetVideoCodec)) + + // Set style/color/css values + s.router.HandleFunc("/api/admin/config/appearance", middleware.RequireAdminAuth(admin.SetCustomColorVariableValues)) + + // Return all webhooks + s.router.HandleFunc("/api/admin/webhooks", middleware.RequireAdminAuth(admin.GetWebhooks)) + + // Delete a single webhook + s.router.HandleFunc("/api/admin/webhooks/delete", middleware.RequireAdminAuth(admin.DeleteWebhook)) + + // Create a single webhook + s.router.HandleFunc("/api/admin/webhooks/create", middleware.RequireAdminAuth(admin.CreateWebhook)) + + // Get all access tokens + s.router.HandleFunc("/api/admin/accesstokens", middleware.RequireAdminAuth(admin.GetExternalAPIUsers)) + + // Delete a single access token + s.router.HandleFunc("/api/admin/accesstokens/delete", middleware.RequireAdminAuth(admin.DeleteExternalAPIUser)) + + // Create a single access token + s.router.HandleFunc("/api/admin/accesstokens/create", middleware.RequireAdminAuth(admin.CreateExternalAPIUser)) + + // Return the auto-update features that are supported for this instance. + s.router.HandleFunc("/api/admin/update/options", middleware.RequireAdminAuth(admin.AutoUpdateOptions)) + + // Begin the auto update + s.router.HandleFunc("/api/admin/update/start", middleware.RequireAdminAuth(admin.AutoUpdateStart)) + + // Force quit the service to restart it + s.router.HandleFunc("/api/admin/update/forcequit", middleware.RequireAdminAuth(admin.AutoUpdateForceQuit)) + + // Logo path + s.router.HandleFunc("/api/admin/config/logo", middleware.RequireAdminAuth(admin.SetLogo)) + + // Server tags + s.router.HandleFunc("/api/admin/config/tags", middleware.RequireAdminAuth(admin.SetTags)) + + // ffmpeg + s.router.HandleFunc("/api/admin/config/ffmpegpath", middleware.RequireAdminAuth(admin.SetFfmpegPath)) + + // Server http port + s.router.HandleFunc("/api/admin/config/webserverport", middleware.RequireAdminAuth(admin.SetWebServerPort)) + + // Server http listen address + s.router.HandleFunc("/api/admin/config/webserverip", middleware.RequireAdminAuth(admin.SetWebServerIP)) + + // Server rtmp port + s.router.HandleFunc("/api/admin/config/rtmpserverport", middleware.RequireAdminAuth(admin.SetRTMPServerPort)) + + // Websocket host override + s.router.HandleFunc("/api/admin/config/sockethostoverride", middleware.RequireAdminAuth(admin.SetSocketHostOverride)) + + // Custom video serving endpoint + s.router.HandleFunc("/api/admin/config/videoservingendpoint", middleware.RequireAdminAuth(admin.SetVideoServingEndpoint)) + + // Is server marked as NSFW + s.router.HandleFunc("/api/admin/config/nsfw", middleware.RequireAdminAuth(admin.SetNSFW)) + + // directory enabled + s.router.HandleFunc("/api/admin/config/directoryenabled", middleware.RequireAdminAuth(admin.SetDirectoryEnabled)) + + // social handles + s.router.HandleFunc("/api/admin/config/socialhandles", middleware.RequireAdminAuth(admin.SetSocialHandles)) + + // set the number of video segments and duration per segment in a playlist + s.router.HandleFunc("/api/admin/config/video/streamlatencylevel", middleware.RequireAdminAuth(admin.SetStreamLatencyLevel)) + + // set an array of video output configurations + s.router.HandleFunc("/api/admin/config/video/streamoutputvariants", middleware.RequireAdminAuth(admin.SetStreamOutputVariants)) + + // set s3 configuration + s.router.HandleFunc("/api/admin/config/s3", middleware.RequireAdminAuth(admin.SetS3Configuration)) + + // set server url + s.router.HandleFunc("/api/admin/config/serverurl", middleware.RequireAdminAuth(admin.SetServerURL)) + + // reset the YP registration + s.router.HandleFunc("/api/admin/yp/reset", middleware.RequireAdminAuth(admin.ResetYPRegistration)) + + // set external action links + s.router.HandleFunc("/api/admin/config/externalactions", middleware.RequireAdminAuth(admin.SetExternalActions)) + + // set custom style css + s.router.HandleFunc("/api/admin/config/customstyles", middleware.RequireAdminAuth(admin.SetCustomStyles)) + + // set custom style javascript + s.router.HandleFunc("/api/admin/config/customjavascript", middleware.RequireAdminAuth(admin.SetCustomJavascript)) + + // Video playback metrics + s.router.HandleFunc("/api/admin/metrics/video", middleware.RequireAdminAuth(admin.GetVideoPlaybackMetrics)) + + // Is the viewer count hidden from viewers + s.router.HandleFunc("/api/admin/config/hideviewercount", middleware.RequireAdminAuth(admin.SetHideViewerCount)) + + // set disabling of search indexing + s.router.HandleFunc("/api/admin/config/disablesearchindexing", middleware.RequireAdminAuth(admin.SetDisableSearchIndexing)) + + // enable/disable federation features + s.router.HandleFunc("/api/admin/config/federation/enable", middleware.RequireAdminAuth(admin.SetFederationEnabled)) + + // set if federation activities are private + s.router.HandleFunc("/api/admin/config/federation/private", middleware.RequireAdminAuth(admin.SetFederationActivityPrivate)) + + // set if fediverse engagement appears in chat + s.router.HandleFunc("/api/admin/config/federation/showengagement", middleware.RequireAdminAuth(admin.SetFederationShowEngagement)) + + // set local federated username + s.router.HandleFunc("/api/admin/config/federation/username", middleware.RequireAdminAuth(admin.SetFederationUsername)) + + // set federated go live message + s.router.HandleFunc("/api/admin/config/federation/livemessage", middleware.RequireAdminAuth(admin.SetFederationGoLiveMessage)) + + // Federation blocked domains + s.router.HandleFunc("/api/admin/config/federation/blockdomains", middleware.RequireAdminAuth(admin.SetFederationBlockDomains)) + + // send a public message to the Fediverse from the server's user + s.router.HandleFunc("/api/admin/federation/send", middleware.RequireAdminAuth(admin.SendFederatedMessage)) + + // Return federated activities + s.router.HandleFunc("/api/admin/federation/actions", middleware.RequireAdminAuth(middleware.HandlePagination(admin.GetFederatedActions))) + + // Prometheus metrics + s.router.Handle("/api/admin/prometheus", middleware.RequireAdminAuth(func(rw http.ResponseWriter, r *http.Request) { + promhttp.Handler().ServeHTTP(rw, r) + })) + + // Configure outbound notification channels. + http.HandleFunc("/api/admin/config/notifications/discord", middleware.RequireAdminAuth(admin.SetDiscordNotificationConfiguration)) + http.HandleFunc("/api/admin/config/notifications/browser", middleware.RequireAdminAuth(admin.SetBrowserNotificationConfiguration)) +} + +func (s *webServer) setupExternalThirdPartyAPIRoutes() { + // Send a system message to chat + s.router.HandleFunc("/api/integrations/chat/system", middleware.RequireExternalAPIAccessToken(user.ScopeCanSendSystemMessages, admin.SendSystemMessage)) + + // Send a system message to a single client + s.router.HandleFunc(utils.RestEndpoint("/api/integrations/chat/system/client/{clientId}", middleware.RequireExternalAPIAccessToken(user.ScopeCanSendSystemMessages, admin.SendSystemMessageToConnectedClient))) + + // Send a user message to chat *NO LONGER SUPPORTED + s.router.HandleFunc("/api/integrations/chat/user", middleware.RequireExternalAPIAccessToken(user.ScopeCanSendChatMessages, admin.SendUserMessage)) + + // Send a message to chat as a specific 3rd party bot/integration based on its access token + s.router.HandleFunc("/api/integrations/chat/send", middleware.RequireExternalAPIAccessToken(user.ScopeCanSendChatMessages, admin.SendIntegrationChatMessage)) + + // Send a user action to chat + s.router.HandleFunc("/api/integrations/chat/action", middleware.RequireExternalAPIAccessToken(user.ScopeCanSendSystemMessages, admin.SendChatAction)) + + // Hide chat message + s.router.HandleFunc("/api/integrations/chat/messagevisibility", middleware.RequireExternalAPIAccessToken(user.ScopeHasAdminAccess, admin.ExternalUpdateMessageVisibility)) + + // Stream title + s.router.HandleFunc("/api/integrations/streamtitle", middleware.RequireExternalAPIAccessToken(user.ScopeHasAdminAccess, admin.ExternalSetStreamTitle)) + + // Get chat history + s.router.HandleFunc("/api/integrations/chat", middleware.RequireExternalAPIAccessToken(user.ScopeHasAdminAccess, controllers.ExternalGetChatMessages)) + + // Connected clients + s.router.HandleFunc("/api/integrations/clients", middleware.RequireExternalAPIAccessToken(user.ScopeHasAdminAccess, admin.ExternalGetConnectedChatClients)) +} + +func (s *webServer) setupModerationAPIRoutes() { + // Update chat message visibility + s.router.HandleFunc("/api/chat/messagevisibility", middleware.RequireUserModerationScopeAccesstoken(admin.UpdateMessageVisibility)) + + // Enable/disable a user + s.router.HandleFunc("/api/chat/users/setenabled", middleware.RequireUserModerationScopeAccesstoken(admin.UpdateUserEnabled)) + + // Get a user's details + s.router.HandleFunc("/api/moderation/chat/user/", middleware.RequireUserModerationScopeAccesstoken(moderation.GetUserDetails)) +} diff --git a/webserver/webserver.go b/webserver/webserver.go new file mode 100644 index 000000000..d9f433344 --- /dev/null +++ b/webserver/webserver.go @@ -0,0 +1,54 @@ +package webserver + +import ( + "fmt" + "net/http" + "time" + + "github.com/CAFxX/httpcompression" + "github.com/owncast/owncast/webserver/handlers" + log "github.com/sirupsen/logrus" + "golang.org/x/net/http2" + "golang.org/x/net/http2/h2c" +) + +type webServer struct { + router *http.ServeMux + handlers *handlers.Handlers + server *http.Server +} + +func New() *webServer { + s := &webServer{ + router: http.NewServeMux(), + } + + s.setupRoutes() + + return s +} + +func (s *webServer) Start(listenIP string, listenPort int) error { + compress, _ := httpcompression.DefaultAdapter() // Use the default configuration + h2s := &http2.Server{} + http2Router := h2c.NewHandler(s.router, h2s) + + s.server = &http.Server{ + Addr: fmt.Sprintf("%s:%d", listenIP, listenPort), + ReadHeaderTimeout: 4 * time.Second, + Handler: compress(http2Router), + } + + if listenIP != "0.0.0.0" { + log.Infof("Web server is listening at %s:%d.", listenIP, listenPort) + } else { + log.Infof("Web server is listening on port %d.", listenPort) + } + + return s.server.ListenAndServe() +} + +// ServeHTTP is the entry point for all web requests. +func (s *webServer) ServeHTTP(w http.ResponseWriter, r *http.Request) { + s.router.ServeHTTP(w, r) +} diff --git a/webserver/webserver_test.go b/webserver/webserver_test.go new file mode 100644 index 000000000..fb273a7b1 --- /dev/null +++ b/webserver/webserver_test.go @@ -0,0 +1,44 @@ +package webserver + +import ( + "net/http" + "net/http/httptest" + "os" + "testing" + + "github.com/owncast/owncast/core/data" +) + +var srv *webServer + +func TestMain(m *testing.M) { + dbFile, err := os.CreateTemp(os.TempDir(), "owncast-test-db.db") + if err != nil { + panic(err) + } + + data.SetupPersistence(dbFile.Name()) + srv = New() + + m.Run() +} + +// TestPrometheusPath tests that the /debug/vars endpoint that +// Prometheus automatically enables is not exposed. +func TestPrometheusDebugPath(t *testing.T) { + r := httptest.NewRequest(http.MethodGet, "/debug/vars", nil) + w := httptest.NewRecorder() + srv.ServeHTTP(w, r) + if w.Result().StatusCode != http.StatusNotFound { + t.Errorf("Expected 404, got %d", w.Result().StatusCode) + } +} + +func TestTestingEndpoint(t *testing.T) { + r := httptest.NewRequest(http.MethodGet, "/test", nil) + w := httptest.NewRecorder() + srv.ServeHTTP(w, r) + if w.Result().StatusCode != http.StatusOK { + t.Errorf("Expected 200, got %d", w.Result().StatusCode) + } +}