From b4b7cf02b63a61db6fad9aed333aedea4a28fc36 Mon Sep 17 00:00:00 2001
From: DingDongSoLong4 <99329275+DingDongSoLong4@users.noreply.github.com>
Date: Wed, 19 Apr 2023 05:01:32 +0200
Subject: [PATCH] Improve caching, HTTP headers and URL handling (#3594)
* Fix relative URLs
* Improve login base URL and redirects
* Prevent duplicate customlocales requests
* Improve UI base URL handling
* Improve UI embedding
* Improve CSP header
* Add Cache-Control headers to all responses
* Improve CORS responses
* Improve authentication handler
* Add back media timestamp suffixes
* Fix default image handling
* Add default param to other image URLs
---
cmd/stash/main.go | 2 +-
go.mod | 2 +-
go.sum | 4 +-
internal/api/authentication.go | 79 ++----
internal/api/resolver_model_movie.go | 37 +--
internal/api/resolver_model_performer.go | 11 +-
internal/api/resolver_model_scene.go | 6 +-
internal/api/resolver_model_scene_marker.go | 9 +-
internal/api/resolver_model_studio.go | 10 +-
internal/api/resolver_model_tag.go | 11 +-
internal/api/resolver_query_scene.go | 2 +-
internal/api/routes_image.go | 20 +-
internal/api/routes_movie.go | 8 +-
internal/api/routes_performer.go | 6 +-
internal/api/routes_scene.go | 79 +++---
internal/api/routes_studio.go | 4 +-
internal/api/routes_tag.go | 4 +-
internal/api/server.go | 244 +++++++++---------
internal/api/session.go | 58 +++--
internal/api/urlbuilders/gallery.go | 19 --
internal/api/urlbuilders/image.go | 4 +-
internal/api/urlbuilders/movie.go | 10 +-
internal/api/urlbuilders/performer.go | 8 +-
internal/api/urlbuilders/scene.go | 31 +--
internal/api/urlbuilders/scene_markers.go | 33 +++
internal/api/urlbuilders/studio.go | 8 +-
internal/api/urlbuilders/tag.go | 8 +-
internal/manager/downloads.go | 1 +
internal/manager/favicon.go | 28 --
internal/manager/manager.go | 6 +-
internal/manager/running_streams.go | 25 +-
pkg/ffmpeg/stream_segmented.go | 9 +-
pkg/ffmpeg/stream_transcode.go | 1 +
pkg/file/file.go | 26 +-
pkg/models/mocks/MovieReaderWriter.go | 21 ++
pkg/models/mocks/PerformerReaderWriter.go | 21 ++
pkg/models/mocks/TagReaderWriter.go | 21 ++
pkg/models/movie.go | 1 +
pkg/models/performer.go | 1 +
pkg/models/tag.go | 1 +
pkg/sqlite/movies.go | 4 +
pkg/sqlite/performer.go | 4 +
pkg/sqlite/tag.go | 4 +
pkg/utils/http.go | 41 +++
pkg/utils/image.go | 26 +-
ui/login/login.html | 2 +-
ui/ui.go | 43 ++-
ui/v2.5/index.html | 3 -
ui/v2.5/src/App.tsx | 25 +-
.../Galleries/GalleryRecommendationRow.tsx | 5 +-
ui/v2.5/src/components/Help/context.tsx | 7 +-
.../Images/ImageRecommendationRow.tsx | 5 +-
ui/v2.5/src/components/MainNavbar.tsx | 3 +-
.../components/Movies/MovieDetails/Movie.tsx | 6 +-
.../Movies/MovieRecommendationRow.tsx | 5 +-
.../Performers/PerformerDetails/Performer.tsx | 36 ++-
.../Performers/PerformerRecommendationRow.tsx | 5 +-
ui/v2.5/src/components/Scenes/SceneCard.tsx | 34 +--
.../Scenes/SceneRecommendationRow.tsx | 5 +-
.../Studios/StudioDetails/Studio.tsx | 6 +-
.../Studios/StudioRecommendationRow.tsx | 5 +-
.../src/components/Tags/TagDetails/Tag.tsx | 6 +-
.../components/Tags/TagRecommendationRow.tsx | 5 +-
ui/v2.5/src/core/createClient.ts | 14 +-
ui/v2.5/src/globals.d.ts | 2 -
ui/v2.5/src/index.tsx | 4 +-
vendor/github.com/go-chi/cors/LICENSE | 21 ++
vendor/github.com/go-chi/cors/README.md | 39 +++
vendor/github.com/{rs => go-chi}/cors/cors.go | 189 ++++++--------
.../github.com/{rs => go-chi}/cors/utils.go | 9 +-
vendor/github.com/rs/cors/.travis.yml | 8 -
vendor/github.com/rs/cors/LICENSE | 19 --
vendor/github.com/rs/cors/README.md | 115 ---------
vendor/modules.txt | 6 +-
74 files changed, 808 insertions(+), 782 deletions(-)
delete mode 100644 internal/api/urlbuilders/gallery.go
create mode 100644 internal/api/urlbuilders/scene_markers.go
delete mode 100644 internal/manager/favicon.go
create mode 100644 pkg/utils/http.go
create mode 100644 vendor/github.com/go-chi/cors/LICENSE
create mode 100644 vendor/github.com/go-chi/cors/README.md
rename vendor/github.com/{rs => go-chi}/cors/cors.go (71%)
rename vendor/github.com/{rs => go-chi}/cors/utils.go (89%)
delete mode 100644 vendor/github.com/rs/cors/.travis.yml
delete mode 100644 vendor/github.com/rs/cors/LICENSE
delete mode 100644 vendor/github.com/rs/cors/README.md
diff --git a/cmd/stash/main.go b/cmd/stash/main.go
index 7b1adeb26..4aadf4fb1 100644
--- a/cmd/stash/main.go
+++ b/cmd/stash/main.go
@@ -34,7 +34,7 @@ func main() {
}()
go handleSignals()
- desktop.Start(manager.GetInstance(), &manager.FaviconProvider{UIBox: ui.UIBox})
+ desktop.Start(manager.GetInstance(), &ui.FaviconProvider)
blockForever()
}
diff --git a/go.mod b/go.mod
index 44a54616a..d39d21b98 100644
--- a/go.mod
+++ b/go.mod
@@ -24,7 +24,6 @@ require (
github.com/pkg/browser v0.0.0-20210911075715-681adbf594b8
github.com/remeh/sizedwaitgroup v1.0.0
github.com/robertkrimen/otto v0.0.0-20200922221731-ef014fd054ac
- github.com/rs/cors v1.6.0
github.com/shurcooL/graphql v0.0.0-20181231061246-d48a9a75455f
github.com/sirupsen/logrus v1.8.1
github.com/spf13/afero v1.8.2 // indirect
@@ -48,6 +47,7 @@ require (
require (
github.com/asticode/go-astisub v0.20.0
github.com/doug-martin/goqu/v9 v9.18.0
+ github.com/go-chi/cors v1.2.1
github.com/go-chi/httplog v0.2.1
github.com/go-toast/toast v0.0.0-20190211030409-01e6764cf0a4
github.com/hashicorp/golang-lru v0.5.4
diff --git a/go.sum b/go.sum
index 75b2d679e..83456f972 100644
--- a/go.sum
+++ b/go.sum
@@ -242,6 +242,8 @@ github.com/go-chi/chi v4.0.2+incompatible h1:maB6vn6FqCxrpz4FqWdh4+lwpyZIQS7YEAU
github.com/go-chi/chi v4.0.2+incompatible/go.mod h1:eB3wogJHnLi3x/kFX2A+IbTBlXxmMeXJVKy9tTv1XzQ=
github.com/go-chi/chi/v5 v5.0.0 h1:DBPx88FjZJH3FsICfDAfIfnb7XxKIYVGG6lOPlhENAg=
github.com/go-chi/chi/v5 v5.0.0/go.mod h1:BBug9lr0cqtdAhsu6R4AAdvufI0/XBzAQSsUqJpoZOs=
+github.com/go-chi/cors v1.2.1 h1:xEC8UT3Rlp2QuWNEr4Fs/c2EAGVKBwy/1vHx3bppil4=
+github.com/go-chi/cors v1.2.1/go.mod h1:sSbTewc+6wYHBBCW7ytsFSn836hqM7JxpglAy2Vzc58=
github.com/go-chi/httplog v0.2.1 h1:KgCtIUkYNlfIsUPzE3utxd1KDKOvCrnAKaqdo0rmrh0=
github.com/go-chi/httplog v0.2.1/go.mod h1:JyHOFO9twSfGoTin/RoP25Lx2a9Btq10ug+sgxe0+bo=
github.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1/go.mod h1:vR7hzQXu2zJy9AVAgeJqvqgH9Q5CA+iKCZ2gyEVpxRU=
@@ -668,8 +670,6 @@ github.com/rogpeppe/fastuuid v1.2.0/go.mod h1:jVj6XXZzXRy/MSR5jhDC/2q6DgLz+nrA6L
github.com/rogpeppe/go-internal v1.1.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4=
github.com/rogpeppe/go-internal v1.2.2/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4=
github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4=
-github.com/rs/cors v1.6.0 h1:G9tHG9lebljV9mfp9SNPDL36nCDxmo3zTlAf1YgvzmI=
-github.com/rs/cors v1.6.0/go.mod h1:gFx+x8UowdsKA9AchylcLynDq+nNFfI8FkUZdN/jGCU=
github.com/rs/xid v1.2.1/go.mod h1:+uKXf+4Djp6Md1KODXJxgGQPKngRmWyn10oCKFzNHOQ=
github.com/rs/xid v1.3.0/go.mod h1:trrq9SKmegXys3aeAKXMUTdJsYXVwGY3RLcfgqegfbg=
github.com/rs/zerolog v1.13.0/go.mod h1:YbFCdg8HfsridGWAh22vktObvhZbQsZXe4/zB0OKkWU=
diff --git a/internal/api/authentication.go b/internal/api/authentication.go
index d02f98b13..94b5328f5 100644
--- a/internal/api/authentication.go
+++ b/internal/api/authentication.go
@@ -5,6 +5,7 @@ import (
"net"
"net/http"
"net/url"
+ "path"
"strings"
"github.com/stashapp/stash/internal/manager"
@@ -13,11 +14,6 @@ import (
"github.com/stashapp/stash/pkg/session"
)
-const (
- loginEndPoint = "/login"
- logoutEndPoint = "/logout"
-)
-
const (
tripwireActivatedErrMsg = "Stash is exposed to the public internet without authentication, and is not serving any more content to protect your privacy. " +
"More information and fixes are available at https://docs.stashapp.cc/networking/authentication-required-when-accessing-stash-from-the-internet"
@@ -30,7 +26,7 @@ const (
func allowUnauthenticated(r *http.Request) bool {
// #2715 - allow access to UI files
- return strings.HasPrefix(r.URL.Path, loginEndPoint) || r.URL.Path == logoutEndPoint || r.URL.Path == "/css" || strings.HasPrefix(r.URL.Path, "/assets")
+ return strings.HasPrefix(r.URL.Path, loginEndpoint) || r.URL.Path == logoutEndpoint || r.URL.Path == "/css" || strings.HasPrefix(r.URL.Path, "/assets")
}
func authenticateHandler() func(http.Handler) http.Handler {
@@ -38,38 +34,41 @@ func authenticateHandler() func(http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
c := config.GetInstance()
- if !checkSecurityTripwireActivated(c, w) {
+ // error if external access tripwire activated
+ if accessErr := session.CheckExternalAccessTripwire(c); accessErr != nil {
+ http.Error(w, tripwireActivatedErrMsg, http.StatusForbidden)
return
}
userID, err := manager.GetInstance().SessionStore.Authenticate(w, r)
if err != nil {
if errors.Is(err, session.ErrUnauthorized) {
- w.WriteHeader(http.StatusInternalServerError)
- _, err = w.Write([]byte(err.Error()))
- if err != nil {
- logger.Error(err)
- }
+ http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
// unauthorized error
- w.Header().Add("WWW-Authenticate", `FormBased`)
+ w.Header().Add("WWW-Authenticate", "FormBased")
w.WriteHeader(http.StatusUnauthorized)
return
}
if err := session.CheckAllowPublicWithoutAuth(c, r); err != nil {
- var externalAccess session.ExternalAccessError
- switch {
- case errors.As(err, &externalAccess):
- securityActivateTripwireAccessedFromInternetWithoutAuth(c, externalAccess, w)
- return
- default:
+ var accessErr session.ExternalAccessError
+ if errors.As(err, &accessErr) {
+ session.LogExternalAccessError(accessErr)
+
+ err := c.ActivatePublicAccessTripwire(net.IP(accessErr).String())
+ if err != nil {
+ logger.Errorf("Error activating public access tripwire: %v", err)
+ }
+
+ http.Error(w, externalAccessErrMsg, http.StatusForbidden)
+ } else {
logger.Errorf("Error checking external access security: %v", err)
w.WriteHeader(http.StatusInternalServerError)
- return
}
+ return
}
ctx := r.Context()
@@ -77,15 +76,15 @@ func authenticateHandler() func(http.Handler) http.Handler {
if c.HasCredentials() {
// authentication is required
if userID == "" && !allowUnauthenticated(r) {
- // authentication was not received, redirect
- // if graphql was requested, we just return a forbidden error
- if r.URL.Path == "/graphql" {
- w.Header().Add("WWW-Authenticate", `FormBased`)
+ // if graphql or a non-webpage was requested, we just return a forbidden error
+ ext := path.Ext(r.URL.Path)
+ if r.URL.Path == gqlEndpoint || (ext != "" && ext != ".html") {
+ w.Header().Add("WWW-Authenticate", "FormBased")
w.WriteHeader(http.StatusUnauthorized)
return
}
- prefix := getProxyPrefix(r.Header)
+ prefix := getProxyPrefix(r)
// otherwise redirect to the login page
returnURL := url.URL{
@@ -95,7 +94,7 @@ func authenticateHandler() func(http.Handler) http.Handler {
q := make(url.Values)
q.Set(returnURLParam, returnURL.String())
u := url.URL{
- Path: prefix + "/login",
+ Path: prefix + loginEndpoint,
RawQuery: q.Encode(),
}
http.Redirect(w, r, u.String(), http.StatusFound)
@@ -111,31 +110,3 @@ func authenticateHandler() func(http.Handler) http.Handler {
})
}
}
-
-func checkSecurityTripwireActivated(c *config.Instance, w http.ResponseWriter) bool {
- if accessErr := session.CheckExternalAccessTripwire(c); accessErr != nil {
- w.WriteHeader(http.StatusForbidden)
- _, err := w.Write([]byte(tripwireActivatedErrMsg))
- if err != nil {
- logger.Error(err)
- }
- return false
- }
-
- return true
-}
-
-func securityActivateTripwireAccessedFromInternetWithoutAuth(c *config.Instance, accessErr session.ExternalAccessError, w http.ResponseWriter) {
- session.LogExternalAccessError(accessErr)
-
- err := c.ActivatePublicAccessTripwire(net.IP(accessErr).String())
- if err != nil {
- logger.Error(err)
- }
-
- w.WriteHeader(http.StatusForbidden)
- _, err = w.Write([]byte(externalAccessErrMsg))
- if err != nil {
- logger.Error(err)
- }
-}
diff --git a/internal/api/resolver_model_movie.go b/internal/api/resolver_model_movie.go
index 9967ef323..fea2276ea 100644
--- a/internal/api/resolver_model_movie.go
+++ b/internal/api/resolver_model_movie.go
@@ -86,33 +86,38 @@ func (r *movieResolver) Synopsis(ctx context.Context, obj *models.Movie) (*strin
}
func (r *movieResolver) FrontImagePath(ctx context.Context, obj *models.Movie) (*string, error) {
- baseURL, _ := ctx.Value(BaseURLCtxKey).(string)
- frontimagePath := urlbuilders.NewMovieURLBuilder(baseURL, obj).GetMovieFrontImageURL()
- return &frontimagePath, nil
-}
-
-func (r *movieResolver) BackImagePath(ctx context.Context, obj *models.Movie) (*string, error) {
- // don't return any thing if there is no back image
- hasImage := false
+ var hasImage bool
if err := r.withReadTxn(ctx, func(ctx context.Context) error {
var err error
- hasImage, err = r.repository.Movie.HasBackImage(ctx, obj.ID)
- if err != nil {
- return err
- }
-
- return nil
+ hasImage, err = r.repository.Movie.HasFrontImage(ctx, obj.ID)
+ return err
}); err != nil {
return nil, err
}
+ baseURL, _ := ctx.Value(BaseURLCtxKey).(string)
+ imagePath := urlbuilders.NewMovieURLBuilder(baseURL, obj).GetMovieFrontImageURL(hasImage)
+ return &imagePath, nil
+}
+
+func (r *movieResolver) BackImagePath(ctx context.Context, obj *models.Movie) (*string, error) {
+ var hasImage bool
+ if err := r.withReadTxn(ctx, func(ctx context.Context) error {
+ var err error
+ hasImage, err = r.repository.Movie.HasBackImage(ctx, obj.ID)
+ return err
+ }); err != nil {
+ return nil, err
+ }
+
+ // don't return anything if there is no back image
if !hasImage {
return nil, nil
}
baseURL, _ := ctx.Value(BaseURLCtxKey).(string)
- backimagePath := urlbuilders.NewMovieURLBuilder(baseURL, obj).GetMovieBackImageURL()
- return &backimagePath, nil
+ imagePath := urlbuilders.NewMovieURLBuilder(baseURL, obj).GetMovieBackImageURL()
+ return &imagePath, nil
}
func (r *movieResolver) SceneCount(ctx context.Context, obj *models.Movie) (ret *int, err error) {
diff --git a/internal/api/resolver_model_performer.go b/internal/api/resolver_model_performer.go
index 6b39c9f94..0fb8f6518 100644
--- a/internal/api/resolver_model_performer.go
+++ b/internal/api/resolver_model_performer.go
@@ -63,8 +63,17 @@ func (r *performerResolver) Birthdate(ctx context.Context, obj *models.Performer
}
func (r *performerResolver) ImagePath(ctx context.Context, obj *models.Performer) (*string, error) {
+ var hasImage bool
+ if err := r.withReadTxn(ctx, func(ctx context.Context) error {
+ var err error
+ hasImage, err = r.repository.Performer.HasImage(ctx, obj.ID)
+ return err
+ }); err != nil {
+ return nil, err
+ }
+
baseURL, _ := ctx.Value(BaseURLCtxKey).(string)
- imagePath := urlbuilders.NewPerformerURLBuilder(baseURL, obj).GetPerformerImageURL()
+ imagePath := urlbuilders.NewPerformerURLBuilder(baseURL, obj).GetPerformerImageURL(hasImage)
return &imagePath, nil
}
diff --git a/internal/api/resolver_model_scene.go b/internal/api/resolver_model_scene.go
index a5c70fadc..99f42e64f 100644
--- a/internal/api/resolver_model_scene.go
+++ b/internal/api/resolver_model_scene.go
@@ -178,8 +178,8 @@ func formatFingerprint(fp interface{}) string {
func (r *sceneResolver) Paths(ctx context.Context, obj *models.Scene) (*ScenePathsType, error) {
baseURL, _ := ctx.Value(BaseURLCtxKey).(string)
config := manager.GetInstance().Config
- builder := urlbuilders.NewSceneURLBuilder(baseURL, obj.ID)
- screenshotPath := builder.GetScreenshotURL(obj.UpdatedAt)
+ builder := urlbuilders.NewSceneURLBuilder(baseURL, obj)
+ screenshotPath := builder.GetScreenshotURL()
previewPath := builder.GetStreamPreviewURL()
streamPath := builder.GetStreamURL(config.GetAPIKey()).String()
webpPath := builder.GetStreamPreviewImageURL()
@@ -370,7 +370,7 @@ func (r *sceneResolver) SceneStreams(ctx context.Context, obj *models.Scene) ([]
config := manager.GetInstance().Config
baseURL, _ := ctx.Value(BaseURLCtxKey).(string)
- builder := urlbuilders.NewSceneURLBuilder(baseURL, obj.ID)
+ builder := urlbuilders.NewSceneURLBuilder(baseURL, obj)
apiKey := config.GetAPIKey()
return manager.GetSceneStreamPaths(obj, builder.GetStreamURL(apiKey), config.GetMaxStreamingTranscodeSize())
diff --git a/internal/api/resolver_model_scene_marker.go b/internal/api/resolver_model_scene_marker.go
index 0057db4e8..3e6ab4030 100644
--- a/internal/api/resolver_model_scene_marker.go
+++ b/internal/api/resolver_model_scene_marker.go
@@ -48,20 +48,17 @@ func (r *sceneMarkerResolver) Tags(ctx context.Context, obj *models.SceneMarker)
func (r *sceneMarkerResolver) Stream(ctx context.Context, obj *models.SceneMarker) (string, error) {
baseURL, _ := ctx.Value(BaseURLCtxKey).(string)
- sceneID := int(obj.SceneID.Int64)
- return urlbuilders.NewSceneURLBuilder(baseURL, sceneID).GetSceneMarkerStreamURL(obj.ID), nil
+ return urlbuilders.NewSceneMarkerURLBuilder(baseURL, obj).GetStreamURL(), nil
}
func (r *sceneMarkerResolver) Preview(ctx context.Context, obj *models.SceneMarker) (string, error) {
baseURL, _ := ctx.Value(BaseURLCtxKey).(string)
- sceneID := int(obj.SceneID.Int64)
- return urlbuilders.NewSceneURLBuilder(baseURL, sceneID).GetSceneMarkerStreamPreviewURL(obj.ID), nil
+ return urlbuilders.NewSceneMarkerURLBuilder(baseURL, obj).GetPreviewURL(), nil
}
func (r *sceneMarkerResolver) Screenshot(ctx context.Context, obj *models.SceneMarker) (string, error) {
baseURL, _ := ctx.Value(BaseURLCtxKey).(string)
- sceneID := int(obj.SceneID.Int64)
- return urlbuilders.NewSceneURLBuilder(baseURL, sceneID).GetSceneMarkerStreamScreenshotURL(obj.ID), nil
+ return urlbuilders.NewSceneMarkerURLBuilder(baseURL, obj).GetScreenshotURL(), nil
}
func (r *sceneMarkerResolver) CreatedAt(ctx context.Context, obj *models.SceneMarker) (*time.Time, error) {
diff --git a/internal/api/resolver_model_studio.go b/internal/api/resolver_model_studio.go
index 4d689df77..10bc577f3 100644
--- a/internal/api/resolver_model_studio.go
+++ b/internal/api/resolver_model_studio.go
@@ -27,9 +27,6 @@ func (r *studioResolver) URL(ctx context.Context, obj *models.Studio) (*string,
}
func (r *studioResolver) ImagePath(ctx context.Context, obj *models.Studio) (*string, error) {
- baseURL, _ := ctx.Value(BaseURLCtxKey).(string)
- imagePath := urlbuilders.NewStudioURLBuilder(baseURL, obj).GetStudioImageURL()
-
var hasImage bool
if err := r.withReadTxn(ctx, func(ctx context.Context) error {
var err error
@@ -39,11 +36,8 @@ func (r *studioResolver) ImagePath(ctx context.Context, obj *models.Studio) (*st
return nil, err
}
- // indicate that image is missing by setting default query param to true
- if !hasImage {
- imagePath += "?default=true"
- }
-
+ baseURL, _ := ctx.Value(BaseURLCtxKey).(string)
+ imagePath := urlbuilders.NewStudioURLBuilder(baseURL, obj).GetStudioImageURL(hasImage)
return &imagePath, nil
}
diff --git a/internal/api/resolver_model_tag.go b/internal/api/resolver_model_tag.go
index 70fee39e0..f2c677b87 100644
--- a/internal/api/resolver_model_tag.go
+++ b/internal/api/resolver_model_tag.go
@@ -111,8 +111,17 @@ func (r *tagResolver) PerformerCount(ctx context.Context, obj *models.Tag) (ret
}
func (r *tagResolver) ImagePath(ctx context.Context, obj *models.Tag) (*string, error) {
+ var hasImage bool
+ if err := r.withReadTxn(ctx, func(ctx context.Context) error {
+ var err error
+ hasImage, err = r.repository.Performer.HasImage(ctx, obj.ID)
+ return err
+ }); err != nil {
+ return nil, err
+ }
+
baseURL, _ := ctx.Value(BaseURLCtxKey).(string)
- imagePath := urlbuilders.NewTagURLBuilder(baseURL, obj).GetTagImageURL()
+ imagePath := urlbuilders.NewTagURLBuilder(baseURL, obj).GetTagImageURL(hasImage)
return &imagePath, nil
}
diff --git a/internal/api/resolver_query_scene.go b/internal/api/resolver_query_scene.go
index 120998d71..e7f16604b 100644
--- a/internal/api/resolver_query_scene.go
+++ b/internal/api/resolver_query_scene.go
@@ -34,7 +34,7 @@ func (r *queryResolver) SceneStreams(ctx context.Context, id *string) ([]*manage
config := manager.GetInstance().Config
baseURL, _ := ctx.Value(BaseURLCtxKey).(string)
- builder := urlbuilders.NewSceneURLBuilder(baseURL, scene.ID)
+ builder := urlbuilders.NewSceneURLBuilder(baseURL, scene)
apiKey := config.GetAPIKey()
return manager.GetSceneStreamPaths(scene, builder.GetStreamURL(apiKey), config.GetMaxStreamingTranscodeSize())
diff --git a/internal/api/routes_image.go b/internal/api/routes_image.go
index 7ac8c99ae..2685a7a76 100644
--- a/internal/api/routes_image.go
+++ b/internal/api/routes_image.go
@@ -8,7 +8,6 @@ import (
"net/http"
"os/exec"
"strconv"
- "syscall"
"github.com/go-chi/chi"
"github.com/stashapp/stash/internal/manager"
@@ -19,6 +18,7 @@ import (
"github.com/stashapp/stash/pkg/logger"
"github.com/stashapp/stash/pkg/models"
"github.com/stashapp/stash/pkg/txn"
+ "github.com/stashapp/stash/pkg/utils"
)
type ImageFinder interface {
@@ -51,12 +51,10 @@ func (rs imageRoutes) Thumbnail(w http.ResponseWriter, r *http.Request) {
img := r.Context().Value(imageKey).(*models.Image)
filepath := manager.GetInstance().Paths.Generated.GetThumbnailPath(img.Checksum, models.DefaultGthumbWidth)
- w.Header().Add("Cache-Control", "max-age=604800000")
-
// if the thumbnail doesn't exist, encode on the fly
exists, _ := fsutil.FileExists(filepath)
if exists {
- http.ServeFile(w, r, filepath)
+ utils.ServeStaticFile(w, r, filepath)
} else {
const useDefault = true
@@ -88,13 +86,13 @@ func (rs imageRoutes) Thumbnail(w http.ResponseWriter, r *http.Request) {
// write the generated thumbnail to disk if enabled
if manager.GetInstance().Config.IsWriteImageThumbnails() {
logger.Debugf("writing thumbnail to disk: %s", img.Path)
- if err := fsutil.WriteFile(filepath, data); err != nil {
- logger.Errorf("error writing thumbnail for image %s: %v", img.Path, err)
+ if err := fsutil.WriteFile(filepath, data); err == nil {
+ utils.ServeStaticFile(w, r, filepath)
+ return
}
+ logger.Errorf("error writing thumbnail for image %s: %v", img.Path, err)
}
- if n, err := w.Write(data); err != nil && !errors.Is(err, syscall.EPIPE) {
- logger.Errorf("error serving thumbnail (wrote %v bytes out of %v): %v", n, len(data), err)
- }
+ utils.ServeStaticContent(w, r, data)
}
}
@@ -131,8 +129,8 @@ func (rs imageRoutes) serveImage(w http.ResponseWriter, r *http.Request, i *mode
// fall back to static image
f, _ := static.Image.Open(defaultImageImage)
defer f.Close()
- stat, _ := f.Stat()
- http.ServeContent(w, r, "image.svg", stat.ModTime(), f.(io.ReadSeeker))
+ image, _ := io.ReadAll(f)
+ utils.ServeImage(w, r, image)
}
// endregion
diff --git a/internal/api/routes_movie.go b/internal/api/routes_movie.go
index 7b77586a6..a64aae76c 100644
--- a/internal/api/routes_movie.go
+++ b/internal/api/routes_movie.go
@@ -58,9 +58,7 @@ func (rs movieRoutes) FrontImage(w http.ResponseWriter, r *http.Request) {
image, _ = utils.ProcessBase64Image(models.DefaultMovieImage)
}
- if err := utils.ServeImage(image, w, r); err != nil {
- logger.Warnf("error serving movie front image: %v", err)
- }
+ utils.ServeImage(w, r, image)
}
func (rs movieRoutes) BackImage(w http.ResponseWriter, r *http.Request) {
@@ -85,9 +83,7 @@ func (rs movieRoutes) BackImage(w http.ResponseWriter, r *http.Request) {
image, _ = utils.ProcessBase64Image(models.DefaultMovieImage)
}
- if err := utils.ServeImage(image, w, r); err != nil {
- logger.Warnf("error serving movie back image: %v", err)
- }
+ utils.ServeImage(w, r, image)
}
func (rs movieRoutes) MovieCtx(next http.Handler) http.Handler {
diff --git a/internal/api/routes_performer.go b/internal/api/routes_performer.go
index 1717e99f9..e7631de5b 100644
--- a/internal/api/routes_performer.go
+++ b/internal/api/routes_performer.go
@@ -54,13 +54,11 @@ func (rs performerRoutes) Image(w http.ResponseWriter, r *http.Request) {
}
}
- if len(image) == 0 || defaultParam == "true" {
+ if len(image) == 0 {
image, _ = getRandomPerformerImageUsingName(performer.Name, performer.Gender, config.GetInstance().GetCustomPerformerImageLocation())
}
- if err := utils.ServeImage(image, w, r); err != nil {
- logger.Warnf("error serving performer image: %v", err)
- }
+ utils.ServeImage(w, r, image)
}
func (rs performerRoutes) PerformerCtx(next http.Handler) http.Handler {
diff --git a/internal/api/routes_scene.go b/internal/api/routes_scene.go
index c01c43104..9a5e81496 100644
--- a/internal/api/routes_scene.go
+++ b/internal/api/routes_scene.go
@@ -88,24 +88,12 @@ func (rs sceneRoutes) Routes() chi.Router {
// region Handlers
func (rs sceneRoutes) StreamDirect(w http.ResponseWriter, r *http.Request) {
-
scene := r.Context().Value(sceneKey).(*models.Scene)
- // #3526 - return 404 if the scene does not have any files
- if scene.Path == "" {
- w.WriteHeader(http.StatusNotFound)
- return
+ ss := manager.SceneServer{
+ TxnManager: rs.txnManager,
+ SceneCoverGetter: rs.sceneFinder,
}
-
- sceneHash := scene.GetHash(config.GetInstance().GetVideoFileNamingAlgorithm())
-
- filepath := manager.GetInstance().Paths.Scene.GetStreamPath(scene.Path, sceneHash)
- streamRequestCtx := ffmpeg.NewStreamRequestContext(w, r)
-
- // #2579 - hijacking and closing the connection here causes video playback to fail in Safari
- // We trust that the request context will be closed, so we don't need to call Cancel on the
- // returned context here.
- _ = manager.GetInstance().ReadLockManager.ReadLock(streamRequestCtx, filepath)
- http.ServeFile(w, r, filepath)
+ ss.StreamSceneDirect(scene, w, r)
}
func (rs sceneRoutes) StreamMp4(w http.ResponseWriter, r *http.Request) {
@@ -266,22 +254,16 @@ func (rs sceneRoutes) Preview(w http.ResponseWriter, r *http.Request) {
scene := r.Context().Value(sceneKey).(*models.Scene)
sceneHash := scene.GetHash(config.GetInstance().GetVideoFileNamingAlgorithm())
filepath := manager.GetInstance().Paths.Scene.GetVideoPreviewPath(sceneHash)
- serveFileNoCache(w, r, filepath)
-}
-// serveFileNoCache serves the provided file, ensuring that the response
-// contains headers to prevent caching.
-func serveFileNoCache(w http.ResponseWriter, r *http.Request, filepath string) {
- w.Header().Add("Cache-Control", "no-cache")
-
- http.ServeFile(w, r, filepath)
+ utils.ServeStaticFile(w, r, filepath)
}
func (rs sceneRoutes) Webp(w http.ResponseWriter, r *http.Request) {
scene := r.Context().Value(sceneKey).(*models.Scene)
sceneHash := scene.GetHash(config.GetInstance().GetVideoFileNamingAlgorithm())
filepath := manager.GetInstance().Paths.Scene.GetWebpPreviewPath(sceneHash)
- http.ServeFile(w, r, filepath)
+
+ utils.ServeStaticFile(w, r, filepath)
}
func (rs sceneRoutes) getChapterVttTitle(ctx context.Context, marker *models.SceneMarker) (*string, error) {
@@ -355,7 +337,7 @@ func (rs sceneRoutes) VttChapter(w http.ResponseWriter, r *http.Request) {
vtt := strings.Join(vttLines, "\n")
w.Header().Set("Content-Type", "text/vtt")
- _, _ = w.Write([]byte(vtt))
+ utils.ServeStaticContent(w, r, []byte(vtt))
}
func (rs sceneRoutes) VttThumbs(w http.ResponseWriter, r *http.Request) {
@@ -366,9 +348,10 @@ func (rs sceneRoutes) VttThumbs(w http.ResponseWriter, r *http.Request) {
} else {
sceneHash = chi.URLParam(r, "sceneHash")
}
- w.Header().Set("Content-Type", "text/vtt")
filepath := manager.GetInstance().Paths.Scene.GetSpriteVttFilePath(sceneHash)
- http.ServeFile(w, r, filepath)
+
+ w.Header().Set("Content-Type", "text/vtt")
+ utils.ServeStaticFile(w, r, filepath)
}
func (rs sceneRoutes) VttSprite(w http.ResponseWriter, r *http.Request) {
@@ -379,23 +362,24 @@ func (rs sceneRoutes) VttSprite(w http.ResponseWriter, r *http.Request) {
} else {
sceneHash = chi.URLParam(r, "sceneHash")
}
- w.Header().Set("Content-Type", "image/jpeg")
filepath := manager.GetInstance().Paths.Scene.GetSpriteImageFilePath(sceneHash)
- http.ServeFile(w, r, filepath)
+
+ utils.ServeStaticFile(w, r, filepath)
}
func (rs sceneRoutes) Funscript(w http.ResponseWriter, r *http.Request) {
s := r.Context().Value(sceneKey).(*models.Scene)
- funscript := video.GetFunscriptPath(s.Path)
- serveFileNoCache(w, r, funscript)
+ filepath := video.GetFunscriptPath(s.Path)
+
+ utils.ServeStaticFile(w, r, filepath)
}
func (rs sceneRoutes) InteractiveHeatmap(w http.ResponseWriter, r *http.Request) {
scene := r.Context().Value(sceneKey).(*models.Scene)
sceneHash := scene.GetHash(config.GetInstance().GetVideoFileNamingAlgorithm())
- w.Header().Set("Content-Type", "image/png")
filepath := manager.GetInstance().Paths.Scene.GetInteractiveHeatmapPath(sceneHash)
- http.ServeFile(w, r, filepath)
+
+ utils.ServeStaticFile(w, r, filepath)
}
func (rs sceneRoutes) Caption(w http.ResponseWriter, r *http.Request, lang string, ext string) {
@@ -434,16 +418,17 @@ func (rs sceneRoutes) Caption(w http.ResponseWriter, r *http.Request, lang strin
return
}
- var b bytes.Buffer
- err = sub.WriteToWebVTT(&b)
+ var buf bytes.Buffer
+
+ err = sub.WriteToWebVTT(&buf)
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
w.Header().Set("Content-Type", "text/vtt")
- w.Header().Add("Cache-Control", "no-cache")
- _, _ = b.WriteTo(w)
+ utils.ServeStaticContent(w, r, buf.Bytes())
+ return
}
}
@@ -483,7 +468,7 @@ func (rs sceneRoutes) SceneMarkerStream(w http.ResponseWriter, r *http.Request)
}
filepath := manager.GetInstance().Paths.SceneMarkers.GetVideoPreviewPath(sceneHash, int(sceneMarker.Seconds))
- http.ServeFile(w, r, filepath)
+ utils.ServeStaticFile(w, r, filepath)
}
func (rs sceneRoutes) SceneMarkerPreview(w http.ResponseWriter, r *http.Request) {
@@ -516,12 +501,10 @@ func (rs sceneRoutes) SceneMarkerPreview(w http.ResponseWriter, r *http.Request)
exists, _ := fsutil.FileExists(filepath)
if !exists {
w.Header().Set("Content-Type", "image/png")
- w.Header().Set("Cache-Control", "no-store")
- _, _ = w.Write(utils.PendingGenerateResource)
- return
+ utils.ServeStaticContent(w, r, utils.PendingGenerateResource)
+ } else {
+ utils.ServeStaticFile(w, r, filepath)
}
-
- http.ServeFile(w, r, filepath)
}
func (rs sceneRoutes) SceneMarkerScreenshot(w http.ResponseWriter, r *http.Request) {
@@ -554,12 +537,10 @@ func (rs sceneRoutes) SceneMarkerScreenshot(w http.ResponseWriter, r *http.Reque
exists, _ := fsutil.FileExists(filepath)
if !exists {
w.Header().Set("Content-Type", "image/png")
- w.Header().Set("Cache-Control", "no-store")
- _, _ = w.Write(utils.PendingGenerateResource)
- return
+ utils.ServeStaticContent(w, r, utils.PendingGenerateResource)
+ } else {
+ utils.ServeStaticFile(w, r, filepath)
}
-
- http.ServeFile(w, r, filepath)
}
// endregion
diff --git a/internal/api/routes_studio.go b/internal/api/routes_studio.go
index 85c66e199..ca4e580f6 100644
--- a/internal/api/routes_studio.go
+++ b/internal/api/routes_studio.go
@@ -67,9 +67,7 @@ func (rs studioRoutes) Image(w http.ResponseWriter, r *http.Request) {
return
}
- if err := utils.ServeImage(image, w, r); err != nil {
- logger.Warnf("error serving studio image: %v", err)
- }
+ utils.ServeImage(w, r, image)
}
func (rs studioRoutes) StudioCtx(next http.Handler) http.Handler {
diff --git a/internal/api/routes_tag.go b/internal/api/routes_tag.go
index 4c0ff43b8..d8837da80 100644
--- a/internal/api/routes_tag.go
+++ b/internal/api/routes_tag.go
@@ -67,9 +67,7 @@ func (rs tagRoutes) Image(w http.ResponseWriter, r *http.Request) {
return
}
- if err := utils.ServeImage(image, w, r); err != nil {
- logger.Warnf("error serving tag image: %v", err)
- }
+ utils.ServeImage(w, r, image)
}
func (rs tagRoutes) TagCtx(next http.Handler) http.Handler {
diff --git a/internal/api/server.go b/internal/api/server.go
index 26d81d5db..cfc57b3dd 100644
--- a/internal/api/server.go
+++ b/internal/api/server.go
@@ -27,17 +27,25 @@ import (
"github.com/gorilla/websocket"
"github.com/vearutop/statigz"
+ "github.com/go-chi/cors"
"github.com/go-chi/httplog"
- "github.com/rs/cors"
"github.com/stashapp/stash/internal/api/loaders"
"github.com/stashapp/stash/internal/manager"
"github.com/stashapp/stash/internal/manager/config"
"github.com/stashapp/stash/pkg/fsutil"
"github.com/stashapp/stash/pkg/logger"
"github.com/stashapp/stash/pkg/plugin"
+ "github.com/stashapp/stash/pkg/utils"
"github.com/stashapp/stash/ui"
)
+const (
+ loginEndpoint = "/login"
+ logoutEndpoint = "/logout"
+ gqlEndpoint = "/graphql"
+ playgroundEndpoint = "/playground"
+)
+
var version string
var buildstamp string
var githash string
@@ -51,6 +59,7 @@ func Start() error {
r := chi.NewRouter()
r.Use(middleware.Heartbeat("/healthz"))
+ r.Use(cors.AllowAll().Handler)
r.Use(authenticateHandler())
visitedPluginHandler := manager.GetInstance().SessionStore.VisitedPluginHandler()
r.Use(visitedPluginHandler)
@@ -67,7 +76,6 @@ func Start() error {
r.Use(SecurityHeadersMiddleware)
r.Use(middleware.DefaultCompress)
r.Use(middleware.StripSlashes)
- r.Use(cors.AllowAll().Handler)
r.Use(BaseURLMiddleware)
recoverFunc := func(ctx context.Context, err interface{}) error {
@@ -123,6 +131,7 @@ func Start() error {
gqlSrv.SetErrorPresenter(gqlErrorHandler)
gqlHandlerFunc := func(w http.ResponseWriter, r *http.Request) {
+ w.Header().Set("Cache-Control", "no-store")
gqlSrv.ServeHTTP(w, r)
}
@@ -132,14 +141,12 @@ func Start() error {
gqlHandler := visitedPluginHandler(dataloaders.Middleware(http.HandlerFunc(gqlHandlerFunc)))
manager.GetInstance().PluginCache.RegisterGQLHandler(gqlHandler)
- r.HandleFunc("/graphql", gqlHandlerFunc)
- r.HandleFunc("/playground", gqlPlayground.Handler("GraphQL playground", "/graphql"))
-
- // session handlers
- r.Post(loginEndPoint, handleLogin(loginUIBox))
- r.Get(logoutEndPoint, handleLogout(loginUIBox))
-
- r.Get(loginEndPoint, getLoginHandler(loginUIBox))
+ r.HandleFunc(gqlEndpoint, gqlHandlerFunc)
+ r.HandleFunc(playgroundEndpoint, func(w http.ResponseWriter, r *http.Request) {
+ setPageSecurityHeaders(w, r)
+ endpoint := getProxyPrefix(r) + gqlEndpoint
+ gqlPlayground.Handler("GraphQL playground", endpoint)(w, r)
+ })
r.Mount("/performer", performerRoutes{
txnManager: txnManager,
@@ -174,36 +181,17 @@ func Start() error {
r.HandleFunc("/css", cssHandler(c, pluginCache))
r.HandleFunc("/javascript", javascriptHandler(c, pluginCache))
- r.HandleFunc("/customlocales", func(w http.ResponseWriter, r *http.Request) {
- w.Header().Set("Content-Type", "application/json")
- if c.GetCustomLocalesEnabled() {
- // search for custom-locales.json in current directory, then $HOME/.stash
- fn := c.GetCustomLocalesPath()
- exists, _ := fsutil.FileExists(fn)
- if exists {
- http.ServeFile(w, r, fn)
- return
- }
- }
- _, _ = w.Write([]byte("{}"))
- })
+ r.HandleFunc("/customlocales", customLocalesHandler(c))
- r.HandleFunc("/login*", func(w http.ResponseWriter, r *http.Request) {
- ext := path.Ext(r.URL.Path)
- if ext == ".html" || ext == "" {
- prefix := getProxyPrefix(r.Header)
+ staticLoginUI := statigz.FileServer(loginUIBox.(fs.ReadDirFS))
- data := getLoginPage(loginUIBox)
- baseURLIndex := strings.Replace(string(data), "%BASE_URL%", prefix+"/", 2)
- _, _ = w.Write([]byte(baseURLIndex))
- } else {
- r.URL.Path = strings.Replace(r.URL.Path, loginEndPoint, "", 1)
- loginRoot, err := fs.Sub(loginUIBox, loginRootDir)
- if err != nil {
- panic(err)
- }
- http.FileServer(http.FS(loginRoot)).ServeHTTP(w, r)
- }
+ r.Get(loginEndpoint, handleLogin(loginUIBox))
+ r.Post(loginEndpoint, handleLoginPost(loginUIBox))
+ r.Get(logoutEndpoint, handleLogout())
+ r.HandleFunc(loginEndpoint+"/*", func(w http.ResponseWriter, r *http.Request) {
+ r.URL.Path = strings.TrimPrefix(r.URL.Path, loginEndpoint)
+ w.Header().Set("Cache-Control", "no-cache")
+ staticLoginUI.ServeHTTP(w, r)
})
// Serve static folders
@@ -215,12 +203,10 @@ func Start() error {
}
customUILocation := c.GetCustomUILocation()
- static := statigz.FileServer(uiBox)
+ staticUI := statigz.FileServer(uiBox.(fs.ReadDirFS))
// Serve the web app
r.HandleFunc("/*", func(w http.ResponseWriter, r *http.Request) {
- const uiRootDir = "v2.5/build"
-
ext := path.Ext(r.URL.Path)
if customUILocation != "" {
@@ -234,29 +220,29 @@ func Start() error {
if ext == ".html" || ext == "" {
themeColor := c.GetThemeColor()
- data, err := uiBox.ReadFile(uiRootDir + "/index.html")
+ data, err := fs.ReadFile(uiBox, "index.html")
if err != nil {
panic(err)
}
+ indexHtml := string(data)
- prefix := getProxyPrefix(r.Header)
- baseURLIndex := strings.ReplaceAll(string(data), "%COLOR%", themeColor)
- baseURLIndex = strings.ReplaceAll(baseURLIndex, "/%BASE_URL%", prefix)
- baseURLIndex = strings.Replace(baseURLIndex, "base href=\"/\"", fmt.Sprintf("base href=\"%s\"", prefix+"/"), 1)
- _, _ = w.Write([]byte(baseURLIndex))
+ prefix := getProxyPrefix(r)
+ indexHtml = strings.ReplaceAll(indexHtml, "%COLOR%", themeColor)
+ indexHtml = strings.Replace(indexHtml, `