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, ` - + Login diff --git a/ui/ui.go b/ui/ui.go index 87d9e7f0e..89ea75a44 100644 --- a/ui/ui.go +++ b/ui/ui.go @@ -1,9 +1,46 @@ package ui -import "embed" +import ( + "embed" + "io/fs" + "runtime" +) //go:embed v2.5/build -var UIBox embed.FS +var uiBox embed.FS +var UIBox fs.FS //go:embed login -var LoginUIBox embed.FS +var loginUIBox embed.FS +var LoginUIBox fs.FS + +func init() { + var err error + UIBox, err = fs.Sub(uiBox, "v2.5/build") + if err != nil { + panic(err) + } + + LoginUIBox, err = fs.Sub(loginUIBox, "login") + if err != nil { + panic(err) + } +} + +type faviconProvider struct{} + +var FaviconProvider = faviconProvider{} + +func (p *faviconProvider) GetFavicon() []byte { + if runtime.GOOS == "windows" { + ret, _ := fs.ReadFile(UIBox, "favicon.ico") + return ret + } + + return p.GetFaviconPng() +} + +func (p *faviconProvider) GetFaviconPng() []byte { + ret, _ := fs.ReadFile(UIBox, "favicon.png") + return ret +} diff --git a/ui/v2.5/index.html b/ui/v2.5/index.html index 11bbb270d..4134a27f0 100755 --- a/ui/v2.5/index.html +++ b/ui/v2.5/index.html @@ -12,9 +12,6 @@ Stash - diff --git a/ui/v2.5/src/App.tsx b/ui/v2.5/src/App.tsx index 4a374065c..a522f3624 100644 --- a/ui/v2.5/src/App.tsx +++ b/ui/v2.5/src/App.tsx @@ -94,6 +94,20 @@ export const App: React.FC = () => { // use en-GB as default messages if any messages aren't found in the chosen language const [messages, setMessages] = useState<{}>(); + const [customMessages, setCustomMessages] = useState<{}>(); + + useEffect(() => { + (async () => { + try { + const res = await fetch(getPlatformURL() + "customlocales"); + if (res.ok) { + setCustomMessages(await res.json()); + } + } catch (err) { + console.log(err); + } + })(); + }, []); useEffect(() => { const setLocale = async () => { @@ -106,15 +120,6 @@ export const App: React.FC = () => { const defaultMessages = (await locales[defaultMessageLanguage]()).default; const mergedMessages = cloneDeep(Object.assign({}, defaultMessages)); const chosenMessages = (await locales[messageLanguage]()).default; - let customMessages = {}; - try { - const res = await fetch(getPlatformURL() + "customlocales"); - if (res.ok) { - customMessages = await res.json(); - } - } catch (err) { - console.log(err); - } mergeWith( mergedMessages, @@ -142,7 +147,7 @@ export const App: React.FC = () => { }; setLocale(); - }, [language]); + }, [customMessages, language]); const location = useLocation(); const history = useHistory(); diff --git a/ui/v2.5/src/components/Galleries/GalleryRecommendationRow.tsx b/ui/v2.5/src/components/Galleries/GalleryRecommendationRow.tsx index dbd10e090..ee94d6da2 100644 --- a/ui/v2.5/src/components/Galleries/GalleryRecommendationRow.tsx +++ b/ui/v2.5/src/components/Galleries/GalleryRecommendationRow.tsx @@ -1,4 +1,5 @@ import React from "react"; +import { Link } from "react-router-dom"; import { useFindGalleries } from "src/core/StashService"; import Slider from "@ant-design/react-slick"; import { GalleryCard } from "./GalleryCard"; @@ -26,9 +27,9 @@ export const GalleryRecommendationRow: React.FC = (props) => { className="gallery-recommendations" header={props.header} link={ - + - + } > import("./Manual")); @@ -48,14 +49,14 @@ export const ManualLink: React.FC = ({ tab, children }) => { const { openManual } = React.useContext(ManualStateContext); return ( - { openManual(`${tab}.md`); e.preventDefault(); }} > {children} - + ); }; diff --git a/ui/v2.5/src/components/Images/ImageRecommendationRow.tsx b/ui/v2.5/src/components/Images/ImageRecommendationRow.tsx index 36f13b8d4..f0fc84493 100644 --- a/ui/v2.5/src/components/Images/ImageRecommendationRow.tsx +++ b/ui/v2.5/src/components/Images/ImageRecommendationRow.tsx @@ -1,4 +1,5 @@ import React from "react"; +import { Link } from "react-router-dom"; import { useFindImages } from "src/core/StashService"; import Slider from "@ant-design/react-slick"; import { ListFilterModel } from "src/models/list-filter/filter"; @@ -26,9 +27,9 @@ export const ImageRecommendationRow: React.FC = (props: IProps) => { className="images-recommendations" header={props.header} link={ - + - + } > { return ( + ); + } + } const renderTabs = () => ( @@ -402,13 +416,7 @@ const PerformerPage: React.FC = ({ performer }) => { {encodingImage ? ( ) : ( - + renderImage() )}
diff --git a/ui/v2.5/src/components/Performers/PerformerRecommendationRow.tsx b/ui/v2.5/src/components/Performers/PerformerRecommendationRow.tsx index 40611967a..3c094f7ad 100644 --- a/ui/v2.5/src/components/Performers/PerformerRecommendationRow.tsx +++ b/ui/v2.5/src/components/Performers/PerformerRecommendationRow.tsx @@ -1,4 +1,5 @@ import React from "react"; +import { Link } from "react-router-dom"; import { useFindPerformers } from "src/core/StashService"; import Slider from "@ant-design/react-slick"; import { PerformerCard } from "./PerformerCard"; @@ -26,9 +27,9 @@ export const PerformerRecommendationRow: React.FC = (props) => { className="performer-recommendations" header={props.header} link={ - + - + } > = ( [props.scene] ); - // studio image is missing if it uses the default - const missingStudioImage = - props.scene.studio?.image_path?.endsWith("?default=true"); - const showStudioAsText = - missingStudioImage || (configuration?.interface.showStudioAsText ?? false); - function maybeRenderSceneSpecsOverlay() { let sizeObj = null; if (file?.size) { @@ -146,21 +140,31 @@ export const SceneCard: React.FC = ( ); } + function renderStudioThumbnail() { + const studioImage = props.scene.studio?.image_path; + const studioName = props.scene.studio?.name; + + if (configuration?.interface.showStudioAsText || !studioImage) { + return studioName; + } + + const studioImageURL = new URL(studioImage); + if (studioImageURL.searchParams.get("default") === "true") { + return studioName; + } + + return ( + {studioName} + ); + } + function maybeRenderSceneStudioOverlay() { if (!props.scene.studio) return; return (
- {showStudioAsText ? ( - props.scene.studio.name - ) : ( - {props.scene.studio.name} - )} + {renderStudioThumbnail()}
); diff --git a/ui/v2.5/src/components/Scenes/SceneRecommendationRow.tsx b/ui/v2.5/src/components/Scenes/SceneRecommendationRow.tsx index 86ae558f0..d33762761 100644 --- a/ui/v2.5/src/components/Scenes/SceneRecommendationRow.tsx +++ b/ui/v2.5/src/components/Scenes/SceneRecommendationRow.tsx @@ -1,4 +1,5 @@ import React, { useMemo } from "react"; +import { Link } from "react-router-dom"; import { useFindScenes } from "src/core/StashService"; import Slider from "@ant-design/react-slick"; import { SceneCard } from "./SceneCard"; @@ -31,9 +32,9 @@ export const SceneRecommendationRow: React.FC = (props) => { className="scene-recommendations" header={props.header} link={ - + - + } > = ({ studio }) => { function renderImage() { let studioImage = studio.image_path; if (isEditing) { - if (image === null) { - studioImage = `${studioImage}&default=true`; + if (image === null && studioImage) { + const studioImageURL = new URL(studioImage); + studioImageURL.searchParams.set("default", "true"); + studioImage = studioImageURL.toString(); } else if (image) { studioImage = image; } diff --git a/ui/v2.5/src/components/Studios/StudioRecommendationRow.tsx b/ui/v2.5/src/components/Studios/StudioRecommendationRow.tsx index ec639aeb6..3df4f65c6 100644 --- a/ui/v2.5/src/components/Studios/StudioRecommendationRow.tsx +++ b/ui/v2.5/src/components/Studios/StudioRecommendationRow.tsx @@ -1,4 +1,5 @@ import React from "react"; +import { Link } from "react-router-dom"; import { useFindStudios } from "src/core/StashService"; import Slider from "@ant-design/react-slick"; import { StudioCard } from "./StudioCard"; @@ -26,9 +27,9 @@ export const StudioRecommendationRow: React.FC = (props) => { className="studio-recommendations" header={props.header} link={ - + - + } > = ({ tag }) => { function renderImage() { let tagImage = tag.image_path; if (isEditing) { - if (image === null) { - tagImage = `${tagImage}&default=true`; + if (image === null && tagImage) { + const tagImageURL = new URL(tagImage); + tagImageURL.searchParams.set("default", "true"); + tagImage = tagImageURL.toString(); } else if (image) { tagImage = image; } diff --git a/ui/v2.5/src/components/Tags/TagRecommendationRow.tsx b/ui/v2.5/src/components/Tags/TagRecommendationRow.tsx index 4e50b4989..9d10d7333 100644 --- a/ui/v2.5/src/components/Tags/TagRecommendationRow.tsx +++ b/ui/v2.5/src/components/Tags/TagRecommendationRow.tsx @@ -1,4 +1,5 @@ import React from "react"; +import { Link } from "react-router-dom"; import { useFindTags } from "src/core/StashService"; import Slider from "@ant-design/react-slick"; import { TagCard } from "./TagCard"; @@ -26,9 +27,9 @@ export const TagRecommendationRow: React.FC = (props) => { className="tag-recommendations" header={props.header} link={ - + - + } > { - const baseURL = window.STASH_BASE_URL; - if (baseURL === "/%BASE_URL%/") return "/"; - return baseURL; -}; +export const baseURL = + document.querySelector("base")?.getAttribute("href") ?? "/"; export const getPlatformURL = (ws?: boolean) => { - const platformUrl = new URL(window.location.origin + getBaseURL()); + const platformUrl = new URL(window.location.origin + baseURL); if (import.meta.env.DEV) { platformUrl.port = import.meta.env.VITE_APP_PLATFORM_PORT ?? "9999"; @@ -139,10 +136,7 @@ export const createClient = () => { // handle unauthorized error by redirecting to the login page if (networkError && (networkError as ServerError).statusCode === 401) { // redirect to login page - const newURL = new URL( - `${getBaseURL()}login`, - window.location.toString() - ); + const newURL = new URL(`${baseURL}login`, window.location.toString()); newURL.searchParams.append("returnURL", window.location.href); window.location.href = newURL.toString(); } diff --git a/ui/v2.5/src/globals.d.ts b/ui/v2.5/src/globals.d.ts index 3bd1b67fe..360388e5f 100644 --- a/ui/v2.5/src/globals.d.ts +++ b/ui/v2.5/src/globals.d.ts @@ -1,5 +1,3 @@ -// eslint-disable-next-line no-var -declare var STASH_BASE_URL: string; declare module "intersection-observer"; declare module "*.md" { diff --git a/ui/v2.5/src/index.tsx b/ui/v2.5/src/index.tsx index ba718f75e..2e66ca2c3 100755 --- a/ui/v2.5/src/index.tsx +++ b/ui/v2.5/src/index.tsx @@ -3,14 +3,14 @@ import ReactDOM from "react-dom"; import { BrowserRouter } from "react-router-dom"; import { App } from "./App"; import { getClient } from "./core/StashService"; -import { getPlatformURL, getBaseURL } from "./core/createClient"; +import { baseURL, getPlatformURL } from "./core/createClient"; import "./index.scss"; import * as serviceWorker from "./serviceWorker"; ReactDOM.render( <> - + diff --git a/vendor/github.com/go-chi/cors/LICENSE b/vendor/github.com/go-chi/cors/LICENSE new file mode 100644 index 000000000..aee6182f9 --- /dev/null +++ b/vendor/github.com/go-chi/cors/LICENSE @@ -0,0 +1,21 @@ +Copyright (c) 2014 Olivier Poitrey +Copyright (c) 2016-Present https://github.com/go-chi authors + +MIT License + +Permission is hereby granted, free of charge, to any person obtaining a copy of +this software and associated documentation files (the "Software"), to deal in +the Software without restriction, including without limitation the rights to +use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of +the Software, and to permit persons to whom the Software is furnished to do so, +subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS +FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR +COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER +IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN +CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. diff --git a/vendor/github.com/go-chi/cors/README.md b/vendor/github.com/go-chi/cors/README.md new file mode 100644 index 000000000..b41686b6a --- /dev/null +++ b/vendor/github.com/go-chi/cors/README.md @@ -0,0 +1,39 @@ +# CORS net/http middleware + +[go-chi/cors](https://github.com/go-chi/cors) is a fork of [github.com/rs/cors](https://github.com/rs/cors) that +provides a `net/http` compatible middleware for performing preflight CORS checks on the server side. These headers +are required for using the browser native [Fetch API](https://developer.mozilla.org/en-US/docs/Web/API/Fetch_API). + +This middleware is designed to be used as a top-level middleware on the [chi](https://github.com/go-chi/chi) router. +Applying with within a `r.Group()` or using `With()` will not work without routes matching `OPTIONS` added. + +## Usage + +```go +func main() { + r := chi.NewRouter() + + // Basic CORS + // for more ideas, see: https://developer.github.com/v3/#cross-origin-resource-sharing + r.Use(cors.Handler(cors.Options{ + // AllowedOrigins: []string{"https://foo.com"}, // Use this to allow specific origin hosts + AllowedOrigins: []string{"https://*", "http://*"}, + // AllowOriginFunc: func(r *http.Request, origin string) bool { return true }, + AllowedMethods: []string{"GET", "POST", "PUT", "DELETE", "OPTIONS"}, + AllowedHeaders: []string{"Accept", "Authorization", "Content-Type", "X-CSRF-Token"}, + ExposedHeaders: []string{"Link"}, + AllowCredentials: false, + MaxAge: 300, // Maximum value not ignored by any of major browsers + })) + + r.Get("/", func(w http.ResponseWriter, r *http.Request) { + w.Write([]byte("welcome")) + }) + + http.ListenAndServe(":3000", r) +} +``` + +## Credits + +All credit for the original work of this middleware goes out to [github.com/rs](github.com/rs). diff --git a/vendor/github.com/rs/cors/cors.go b/vendor/github.com/go-chi/cors/cors.go similarity index 71% rename from vendor/github.com/rs/cors/cors.go rename to vendor/github.com/go-chi/cors/cors.go index d301ca724..8df81636e 100644 --- a/vendor/github.com/rs/cors/cors.go +++ b/vendor/github.com/go-chi/cors/cors.go @@ -1,23 +1,21 @@ -/* -Package cors is net/http handler to handle CORS related requests -as defined by http://www.w3.org/TR/cors/ - -You can configure it by passing an option struct to cors.New: - - c := cors.New(cors.Options{ - AllowedOrigins: []string{"foo.com"}, - AllowedMethods: []string{"GET", "POST", "DELETE"}, - AllowCredentials: true, - }) - -Then insert the handler in the chain: - - handler = c.Handler(handler) - -See Options documentation for more options. - -The resulting handler is a standard net/http handler. -*/ +// cors package is net/http handler to handle CORS related requests +// as defined by http://www.w3.org/TR/cors/ +// +// You can configure it by passing an option struct to cors.New: +// +// c := cors.New(cors.Options{ +// AllowedOrigins: []string{"foo.com"}, +// AllowedMethods: []string{"GET", "POST", "DELETE"}, +// AllowCredentials: true, +// }) +// +// Then insert the handler in the chain: +// +// handler = c.Handler(handler) +// +// See Options documentation for more options. +// +// The resulting handler is a standard net/http handler. package cors import ( @@ -37,61 +35,77 @@ type Options struct { // Only one wildcard can be used per origin. // Default value is ["*"] AllowedOrigins []string - // AllowOriginFunc is a custom function to validate the origin. It take the origin + + // AllowOriginFunc is a custom function to validate the origin. It takes the origin // as argument and returns true if allowed or false otherwise. If this option is // set, the content of AllowedOrigins is ignored. - AllowOriginFunc func(origin string) bool - // AllowOriginFunc is a custom function to validate the origin. It takes the HTTP Request object and the origin as - // argument and returns true if allowed or false otherwise. If this option is set, the content of `AllowedOrigins` - // and `AllowOriginFunc` is ignored. - AllowOriginRequestFunc func(r *http.Request, origin string) bool + AllowOriginFunc func(r *http.Request, origin string) bool + // AllowedMethods is a list of methods the client is allowed to use with // cross-domain requests. Default value is simple methods (HEAD, GET and POST). AllowedMethods []string + // AllowedHeaders is list of non simple headers the client is allowed to use with // cross-domain requests. // If the special "*" value is present in the list, all headers will be allowed. // Default value is [] but "Origin" is always appended to the list. AllowedHeaders []string + // ExposedHeaders indicates which headers are safe to expose to the API of a CORS // API specification ExposedHeaders []string - // MaxAge indicates how long (in seconds) the results of a preflight request - // can be cached - MaxAge int + // AllowCredentials indicates whether the request can include user credentials like // cookies, HTTP authentication or client side SSL certificates. AllowCredentials bool + + // MaxAge indicates how long (in seconds) the results of a preflight request + // can be cached + MaxAge int + // OptionsPassthrough instructs preflight to let other potential next handlers to // process the OPTIONS method. Turn this on if your application handles OPTIONS. OptionsPassthrough bool + // Debugging flag adds additional output to debug server side CORS issues Debug bool } +// Logger generic interface for logger +type Logger interface { + Printf(string, ...interface{}) +} + // Cors http handler type Cors struct { // Debug logger - Log *log.Logger + Log Logger + // Normalized list of plain allowed origins allowedOrigins []string + // List of allowed origins containing wildcards allowedWOrigins []wildcard + // Optional origin validator function - allowOriginFunc func(origin string) bool - // Optional origin validator (with request) function - allowOriginRequestFunc func(r *http.Request, origin string) bool + allowOriginFunc func(r *http.Request, origin string) bool + // Normalized list of allowed headers allowedHeaders []string + // Normalized list of allowed methods allowedMethods []string + // Normalized list of exposed headers exposedHeaders []string maxAge int + // Set to true when allowed origins contains a "*" allowedOriginsAll bool + // Set to true when allowed headers contains a "*" allowedHeadersAll bool + allowCredentials bool optionPassthrough bool } @@ -99,14 +113,13 @@ type Cors struct { // New creates a new Cors handler with the provided options. func New(options Options) *Cors { c := &Cors{ - exposedHeaders: convert(options.ExposedHeaders, http.CanonicalHeaderKey), - allowOriginFunc: options.AllowOriginFunc, - allowOriginRequestFunc: options.AllowOriginRequestFunc, - allowCredentials: options.AllowCredentials, - maxAge: options.MaxAge, - optionPassthrough: options.OptionsPassthrough, + exposedHeaders: convert(options.ExposedHeaders, http.CanonicalHeaderKey), + allowOriginFunc: options.AllowOriginFunc, + allowCredentials: options.AllowCredentials, + maxAge: options.MaxAge, + optionPassthrough: options.OptionsPassthrough, } - if options.Debug { + if options.Debug && c.Log == nil { c.Log = log.New(os.Stdout, "[cors] ", log.LstdFlags) } @@ -116,7 +129,7 @@ func New(options Options) *Cors { // Allowed Origins if len(options.AllowedOrigins) == 0 { - if options.AllowOriginFunc == nil && options.AllowOriginRequestFunc == nil { + if options.AllowOriginFunc == nil { // Default is all origins c.allowedOriginsAll = true } @@ -145,7 +158,7 @@ func New(options Options) *Cors { // Allowed Headers if len(options.AllowedHeaders) == 0 { // Use sensible defaults - c.allowedHeaders = []string{"Origin", "Accept", "Content-Type", "X-Requested-With"} + c.allowedHeaders = []string{"Origin", "Accept", "Content-Type"} } else { // Origin is always appended as some browsers will always request for this header at preflight c.allowedHeaders = convert(append(options.AllowedHeaders, "Origin"), http.CanonicalHeaderKey) @@ -161,7 +174,7 @@ func New(options Options) *Cors { // Allowed Methods if len(options.AllowedMethods) == 0 { // Default is spec's "simple" methods - c.allowedMethods = []string{"GET", "POST", "HEAD"} + c.allowedMethods = []string{http.MethodGet, http.MethodPost, http.MethodHead} } else { c.allowedMethods = convert(options.AllowedMethods, strings.ToUpper) } @@ -169,17 +182,25 @@ func New(options Options) *Cors { return c } -// Default creates a new Cors handler with default options. -func Default() *Cors { - return New(Options{}) +// Handler creates a new Cors handler with passed options. +func Handler(options Options) func(next http.Handler) http.Handler { + c := New(options) + return c.Handler } // AllowAll create a new Cors handler with permissive configuration allowing all // origins with all standard methods with any header and credentials. func AllowAll() *Cors { return New(Options{ - AllowedOrigins: []string{"*"}, - AllowedMethods: []string{"HEAD", "GET", "POST", "PUT", "PATCH", "DELETE"}, + AllowedOrigins: []string{"*"}, + AllowedMethods: []string{ + http.MethodHead, + http.MethodGet, + http.MethodPost, + http.MethodPut, + http.MethodPatch, + http.MethodDelete, + }, AllowedHeaders: []string{"*"}, AllowCredentials: false, }) @@ -187,7 +208,7 @@ func AllowAll() *Cors { // Handler apply the CORS specification on the request, and add relevant CORS headers // as necessary. -func (c *Cors) Handler(h http.Handler) http.Handler { +func (c *Cors) Handler(next http.Handler) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { if r.Method == http.MethodOptions && r.Header.Get("Access-Control-Request-Method") != "" { c.logf("Handler: Preflight request") @@ -197,57 +218,25 @@ func (c *Cors) Handler(h http.Handler) http.Handler { // is authentication middleware ; OPTIONS requests won't carry authentication // headers (see #1) if c.optionPassthrough { - h.ServeHTTP(w, r) + next.ServeHTTP(w, r) } else { w.WriteHeader(http.StatusOK) } } else { c.logf("Handler: Actual request") c.handleActualRequest(w, r) - h.ServeHTTP(w, r) + next.ServeHTTP(w, r) } }) } -// HandlerFunc provides Martini compatible handler -func (c *Cors) HandlerFunc(w http.ResponseWriter, r *http.Request) { - if r.Method == http.MethodOptions && r.Header.Get("Access-Control-Request-Method") != "" { - c.logf("HandlerFunc: Preflight request") - c.handlePreflight(w, r) - } else { - c.logf("HandlerFunc: Actual request") - c.handleActualRequest(w, r) - } -} - -// Negroni compatible interface -func (c *Cors) ServeHTTP(w http.ResponseWriter, r *http.Request, next http.HandlerFunc) { - if r.Method == http.MethodOptions && r.Header.Get("Access-Control-Request-Method") != "" { - c.logf("ServeHTTP: Preflight request") - c.handlePreflight(w, r) - // Preflight requests are standalone and should stop the chain as some other - // middleware may not handle OPTIONS requests correctly. One typical example - // is authentication middleware ; OPTIONS requests won't carry authentication - // headers (see #1) - if c.optionPassthrough { - next(w, r) - } else { - w.WriteHeader(http.StatusOK) - } - } else { - c.logf("ServeHTTP: Actual request") - c.handleActualRequest(w, r) - next(w, r) - } -} - // handlePreflight handles pre-flight CORS requests func (c *Cors) handlePreflight(w http.ResponseWriter, r *http.Request) { headers := w.Header() origin := r.Header.Get("Origin") if r.Method != http.MethodOptions { - c.logf(" Preflight aborted: %s!=OPTIONS", r.Method) + c.logf("Preflight aborted: %s!=OPTIONS", r.Method) return } // Always set Vary headers @@ -258,22 +247,22 @@ func (c *Cors) handlePreflight(w http.ResponseWriter, r *http.Request) { headers.Add("Vary", "Access-Control-Request-Headers") if origin == "" { - c.logf(" Preflight aborted: empty origin") + c.logf("Preflight aborted: empty origin") return } if !c.isOriginAllowed(r, origin) { - c.logf(" Preflight aborted: origin '%s' not allowed", origin) + c.logf("Preflight aborted: origin '%s' not allowed", origin) return } reqMethod := r.Header.Get("Access-Control-Request-Method") if !c.isMethodAllowed(reqMethod) { - c.logf(" Preflight aborted: method '%s' not allowed", reqMethod) + c.logf("Preflight aborted: method '%s' not allowed", reqMethod) return } reqHeaders := parseHeaderList(r.Header.Get("Access-Control-Request-Headers")) if !c.areHeadersAllowed(reqHeaders) { - c.logf(" Preflight aborted: headers '%v' not allowed", reqHeaders) + c.logf("Preflight aborted: headers '%v' not allowed", reqHeaders) return } if c.allowedOriginsAll { @@ -296,7 +285,7 @@ func (c *Cors) handlePreflight(w http.ResponseWriter, r *http.Request) { if c.maxAge > 0 { headers.Set("Access-Control-Max-Age", strconv.Itoa(c.maxAge)) } - c.logf(" Preflight response headers: %v", headers) + c.logf("Preflight response headers: %v", headers) } // handleActualRequest handles simple cross-origin requests, actual request or redirects @@ -304,18 +293,14 @@ func (c *Cors) handleActualRequest(w http.ResponseWriter, r *http.Request) { headers := w.Header() origin := r.Header.Get("Origin") - if r.Method == http.MethodOptions { - c.logf(" Actual request no headers added: method == %s", r.Method) - return - } // Always set Vary, see https://github.com/rs/cors/issues/10 headers.Add("Vary", "Origin") if origin == "" { - c.logf(" Actual request no headers added: missing origin") + c.logf("Actual request no headers added: missing origin") return } if !c.isOriginAllowed(r, origin) { - c.logf(" Actual request no headers added: origin '%s' not allowed", origin) + c.logf("Actual request no headers added: origin '%s' not allowed", origin) return } @@ -324,7 +309,7 @@ func (c *Cors) handleActualRequest(w http.ResponseWriter, r *http.Request) { // spec doesn't instruct to check the allowed methods for simple cross-origin requests. // We think it's a nice feature to be able to have control on those methods though. if !c.isMethodAllowed(r.Method) { - c.logf(" Actual request no headers added: method '%s' not allowed", r.Method) + c.logf("Actual request no headers added: method '%s' not allowed", r.Method) return } @@ -339,10 +324,10 @@ func (c *Cors) handleActualRequest(w http.ResponseWriter, r *http.Request) { if c.allowCredentials { headers.Set("Access-Control-Allow-Credentials", "true") } - c.logf(" Actual response added headers: %v", headers) + c.logf("Actual response added headers: %v", headers) } -// convenience method. checks if debugging is turned on before printing +// convenience method. checks if a logger is set. func (c *Cors) logf(format string, a ...interface{}) { if c.Log != nil { c.Log.Printf(format, a...) @@ -352,11 +337,8 @@ func (c *Cors) logf(format string, a ...interface{}) { // isOriginAllowed checks if a given origin is allowed to perform cross-domain requests // on the endpoint func (c *Cors) isOriginAllowed(r *http.Request, origin string) bool { - if c.allowOriginRequestFunc != nil { - return c.allowOriginRequestFunc(r, origin) - } if c.allowOriginFunc != nil { - return c.allowOriginFunc(origin) + return c.allowOriginFunc(r, origin) } if c.allowedOriginsAll { return true @@ -376,7 +358,7 @@ func (c *Cors) isOriginAllowed(r *http.Request, origin string) bool { } // isMethodAllowed checks if a given method can be used as part of a cross-domain request -// on the endpoing +// on the endpoint func (c *Cors) isMethodAllowed(method string) bool { if len(c.allowedMethods) == 0 { // If no method allowed, always return false, even for preflight request @@ -407,6 +389,7 @@ func (c *Cors) areHeadersAllowed(requestedHeaders []string) bool { for _, h := range c.allowedHeaders { if h == header { found = true + break } } if !found { diff --git a/vendor/github.com/rs/cors/utils.go b/vendor/github.com/go-chi/cors/utils.go similarity index 89% rename from vendor/github.com/rs/cors/utils.go rename to vendor/github.com/go-chi/cors/utils.go index 53ad9e9db..3fe5a5aee 100644 --- a/vendor/github.com/rs/cors/utils.go +++ b/vendor/github.com/go-chi/cors/utils.go @@ -39,20 +39,19 @@ func parseHeaderList(headerList string) []string { headers := make([]string, 0, t) for i := 0; i < l; i++ { b := headerList[i] - switch { - case b >= 'a' && b <= 'z': + if b >= 'a' && b <= 'z' { if upper { h = append(h, b-toLower) } else { h = append(h, b) } - case b >= 'A' && b <= 'Z': + } else if b >= 'A' && b <= 'Z' { if !upper { h = append(h, b+toLower) } else { h = append(h, b) } - case b == '-' || b == '_' || (b >= '0' && b <= '9'): + } else if b == '-' || b == '_' || b == '.' || (b >= '0' && b <= '9') { h = append(h, b) } @@ -64,7 +63,7 @@ func parseHeaderList(headerList string) []string { upper = true } } else { - upper = b == '-' || b == '_' + upper = b == '-' } } return headers diff --git a/vendor/github.com/rs/cors/.travis.yml b/vendor/github.com/rs/cors/.travis.yml deleted file mode 100644 index 17e5e50d5..000000000 --- a/vendor/github.com/rs/cors/.travis.yml +++ /dev/null @@ -1,8 +0,0 @@ -language: go -go: -- 1.9 -- "1.10" -- tip -matrix: - allow_failures: - - go: tip diff --git a/vendor/github.com/rs/cors/LICENSE b/vendor/github.com/rs/cors/LICENSE deleted file mode 100644 index d8e2df5a4..000000000 --- a/vendor/github.com/rs/cors/LICENSE +++ /dev/null @@ -1,19 +0,0 @@ -Copyright (c) 2014 Olivier Poitrey - -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is furnished -to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in all -copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN -THE SOFTWARE. diff --git a/vendor/github.com/rs/cors/README.md b/vendor/github.com/rs/cors/README.md deleted file mode 100644 index ecc83b295..000000000 --- a/vendor/github.com/rs/cors/README.md +++ /dev/null @@ -1,115 +0,0 @@ -# Go CORS handler [![godoc](http://img.shields.io/badge/godoc-reference-blue.svg?style=flat)](https://godoc.org/github.com/rs/cors) [![license](http://img.shields.io/badge/license-MIT-red.svg?style=flat)](https://raw.githubusercontent.com/rs/cors/master/LICENSE) [![build](https://img.shields.io/travis/rs/cors.svg?style=flat)](https://travis-ci.org/rs/cors) [![Coverage](http://gocover.io/_badge/github.com/rs/cors)](http://gocover.io/github.com/rs/cors) - -CORS is a `net/http` handler implementing [Cross Origin Resource Sharing W3 specification](http://www.w3.org/TR/cors/) in Golang. - -## Getting Started - -After installing Go and setting up your [GOPATH](http://golang.org/doc/code.html#GOPATH), create your first `.go` file. We'll call it `server.go`. - -```go -package main - -import ( - "net/http" - - "github.com/rs/cors" -) - -func main() { - mux := http.NewServeMux() - mux.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) { - w.Header().Set("Content-Type", "application/json") - w.Write([]byte("{\"hello\": \"world\"}")) - }) - - // cors.Default() setup the middleware with default options being - // all origins accepted with simple methods (GET, POST). See - // documentation below for more options. - handler := cors.Default().Handler(mux) - http.ListenAndServe(":8080", handler) -} -``` - -Install `cors`: - - go get github.com/rs/cors - -Then run your server: - - go run server.go - -The server now runs on `localhost:8080`: - - $ curl -D - -H 'Origin: http://foo.com' http://localhost:8080/ - HTTP/1.1 200 OK - Access-Control-Allow-Origin: foo.com - Content-Type: application/json - Date: Sat, 25 Oct 2014 03:43:57 GMT - Content-Length: 18 - - {"hello": "world"} - -### Allow * With Credentials Security Protection - -This library has been modified to avoid a well known security issue when configured with `AllowedOrigins` to `*` and `AllowCredentials` to `true`. Such setup used to make the library reflects the request `Origin` header value, working around a security protection embedded into the standard that makes clients to refuse such configuration. This behavior has been removed with [#55](https://github.com/rs/cors/issues/55) and [#57](https://github.com/rs/cors/issues/57). - -If you depend on this behavior and understand the implications, you can restore it using the `AllowOriginFunc` with `func(origin string) {return true}`. - -Please refer to [#55](https://github.com/rs/cors/issues/55) for more information about the security implications. - -### More Examples - -* `net/http`: [examples/nethttp/server.go](https://github.com/rs/cors/blob/master/examples/nethttp/server.go) -* [Goji](https://goji.io): [examples/goji/server.go](https://github.com/rs/cors/blob/master/examples/goji/server.go) -* [Martini](http://martini.codegangsta.io): [examples/martini/server.go](https://github.com/rs/cors/blob/master/examples/martini/server.go) -* [Negroni](https://github.com/codegangsta/negroni): [examples/negroni/server.go](https://github.com/rs/cors/blob/master/examples/negroni/server.go) -* [Alice](https://github.com/justinas/alice): [examples/alice/server.go](https://github.com/rs/cors/blob/master/examples/alice/server.go) -* [HttpRouter](https://github.com/julienschmidt/httprouter): [examples/httprouter/server.go](https://github.com/rs/cors/blob/master/examples/httprouter/server.go) -* [Gorilla](http://www.gorillatoolkit.org/pkg/mux): [examples/gorilla/server.go](https://github.com/rs/cors/blob/master/examples/gorilla/server.go) -* [Buffalo](https://gobuffalo.io): [examples/buffalo/server.go](https://github.com/rs/cors/blob/master/examples/buffalo/server.go) -* [Gin](https://gin-gonic.github.io/gin): [examples/gin/server.go](https://github.com/rs/cors/blob/master/examples/gin/server.go) -* [Chi](https://github.com/go-chi/chi): [examples/chi/server.go](https://github.com/rs/cors/blob/master/examples/chi/server.go) - -## Parameters - -Parameters are passed to the middleware thru the `cors.New` method as follow: - -```go -c := cors.New(cors.Options{ - AllowedOrigins: []string{"http://foo.com", "http://foo.com:8080"}, - AllowCredentials: true, - // Enable Debugging for testing, consider disabling in production - Debug: true, -}) - -// Insert the middleware -handler = c.Handler(handler) -``` - -* **AllowedOrigins** `[]string`: A list of origins a cross-domain request can be executed from. If the special `*` value is present in the list, all origins will be allowed. An origin may contain a wildcard (`*`) to replace 0 or more characters (i.e.: `http://*.domain.com`). Usage of wildcards implies a small performance penality. Only one wildcard can be used per origin. The default value is `*`. -* **AllowOriginFunc** `func (origin string) bool`: A custom function to validate the origin. It takes the origin as an argument and returns true if allowed, or false otherwise. If this option is set, the content of `AllowedOrigins` is ignored. -* **AllowOriginRequestFunc** `func (r *http.Request origin string) bool`: A custom function to validate the origin. It takes the HTTP Request object and the origin as argument and returns true if allowed or false otherwise. If this option is set, the content of `AllowedOrigins` and `AllowOriginFunc` is ignored -* **AllowedMethods** `[]string`: A list of methods the client is allowed to use with cross-domain requests. Default value is simple methods (`GET` and `POST`). -* **AllowedHeaders** `[]string`: A list of non simple headers the client is allowed to use with cross-domain requests. -* **ExposedHeaders** `[]string`: Indicates which headers are safe to expose to the API of a CORS API specification -* **AllowCredentials** `bool`: Indicates whether the request can include user credentials like cookies, HTTP authentication or client side SSL certificates. The default is `false`. -* **MaxAge** `int`: Indicates how long (in seconds) the results of a preflight request can be cached. The default is `0` which stands for no max age. -* **OptionsPassthrough** `bool`: Instructs preflight to let other potential next handlers to process the `OPTIONS` method. Turn this on if your application handles `OPTIONS`. -* **Debug** `bool`: Debugging flag adds additional output to debug server side CORS issues. - -See [API documentation](http://godoc.org/github.com/rs/cors) for more info. - -## Benchmarks - - BenchmarkWithout 20000000 64.6 ns/op 8 B/op 1 allocs/op - BenchmarkDefault 3000000 469 ns/op 114 B/op 2 allocs/op - BenchmarkAllowedOrigin 3000000 608 ns/op 114 B/op 2 allocs/op - BenchmarkPreflight 20000000 73.2 ns/op 0 B/op 0 allocs/op - BenchmarkPreflightHeader 20000000 73.6 ns/op 0 B/op 0 allocs/op - BenchmarkParseHeaderList 2000000 847 ns/op 184 B/op 6 allocs/op - BenchmarkParse…Single 5000000 290 ns/op 32 B/op 3 allocs/op - BenchmarkParse…Normalized 2000000 776 ns/op 160 B/op 6 allocs/op - -## Licenses - -All source code is licensed under the [MIT License](https://raw.github.com/rs/cors/master/LICENSE). diff --git a/vendor/modules.txt b/vendor/modules.txt index c7b53d4f0..b53f16a6b 100644 --- a/vendor/modules.txt +++ b/vendor/modules.txt @@ -154,6 +154,9 @@ github.com/go-chi/chi/middleware ## explicit; go 1.16 github.com/go-chi/chi/v5 github.com/go-chi/chi/v5/middleware +# github.com/go-chi/cors v1.2.1 +## explicit; go 1.14 +github.com/go-chi/cors # github.com/go-chi/httplog v0.2.1 ## explicit; go 1.14 github.com/go-chi/httplog @@ -308,9 +311,6 @@ github.com/robertkrimen/otto/file github.com/robertkrimen/otto/parser github.com/robertkrimen/otto/registry github.com/robertkrimen/otto/token -# github.com/rs/cors v1.6.0 -## explicit -github.com/rs/cors # github.com/rs/zerolog v1.26.1 ## explicit; go 1.15 github.com/rs/zerolog