This commit is contained in:
Deluan 2023-05-22 11:59:33 -04:00 • committed by Deluan
commit ea231fe265
8 changed files with 142 additions and 90 deletions

View file

@ -1,4 +1,3 @@
//go:generate npx swagger-cli bundle ../../api/spec.yml --outfile ../../api/openapi.yaml --type yaml
//go:generate go run github.com/deepmap/oapi-codegen/cmd/oapi-codegen -config ./openapi_api.cfg.yaml "../../api/openapi.yaml"
//go:generate go run github.com/deepmap/oapi-codegen/cmd/oapi-codegen -config ./openapi_types.cfg.yaml "../../api/openapi.yaml"

View file

@ -15,6 +15,7 @@ import (
"github.com/navidrome/navidrome/log"
"github.com/navidrome/navidrome/model"
"github.com/navidrome/navidrome/server"
. "github.com/navidrome/navidrome/utils/gg"
)
type contextKey string
@ -38,19 +39,19 @@ func toAPITrack(mf model.MediaFile) Track {
Albumartist: mf.AlbumArtist,
Artist: mf.Artist,
Bitrate: mf.BitRate,
Bpm: p(mf.Bpm),
Bpm: P(mf.Bpm),
Channels: mf.Channels,
Comments: p(mf.Comment),
Disc: p(mf.DiscNumber),
Comments: P(mf.Comment),
Disc: P(mf.DiscNumber),
Duration: mf.Duration,
Genre: p(mf.Genre),
Genre: P(mf.Genre),
Mimetype: mf.ContentType(),
RecordingMbid: p(mf.MbzTrackID),
RecordingMbid: P(mf.MbzTrackID),
Size: int(mf.Size),
Title: mf.Title,
Track: mf.TrackNumber,
TrackMbid: p(mf.MbzReleaseTrackID),
Year: p(mf.Year),
TrackMbid: P(mf.MbzReleaseTrackID),
Year: P(mf.Year),
},
Relationships: &TrackRelationships{
Albums: &[]AlbumTrackRelationship{toAlbumRelationship(mf)},
@ -97,22 +98,6 @@ func toAPITracks(mfs model.MediaFiles) []Track {
return tracks
}
func p[T comparable](t T) *T {
var zero T
if t == zero {
return nil
}
return &t
}
func v[T comparable](p *T) T {
var zero T
if p == nil {
return zero
}
return *p
}
// toQueryOptions convert a params struct to a model.QueryOptions struct, to be used by the
// GetAll and CountAll functions. It assumes all GetXxxxParams functions have the exact same structure.
func toQueryOptions(ctx context.Context, params GetTracksParams) model.QueryOptions {
@ -133,8 +118,8 @@ func toQueryOptions(ctx context.Context, params GetTracksParams) model.QueryOpti
parseFilter(params.FilterGreaterOrEqual, func(f, v string) squirrel.Sqlizer { return squirrel.GtOrEq{f: v} })
parseFilter(params.FilterLessThan, func(f, v string) squirrel.Sqlizer { return squirrel.Lt{f: v} })
parseFilter(params.FilterLessOrEqual, func(f, v string) squirrel.Sqlizer { return squirrel.LtOrEq{f: v} })
offset := v(params.PageOffset)
limit := v(params.PageLimit)
offset := V(params.PageOffset)
limit := V(params.PageLimit)
sort, err := toSortParams(params.Sort)
if err != nil {
log.Warn(ctx, "Ignoring invalid sort parameter", err)
@ -177,15 +162,15 @@ func toSortParams(sort *string) (string, error) {
return strings.Join(resultCols, ","), nil
}
func apiErrorHandler(w http.ResponseWriter, r *http.Request, err error) {
func apiErrorHandler(w http.ResponseWriter, _ *http.Request, err error) {
var res ErrorObject
switch {
case errors.Is(err, model.ErrNotAuthorized):
res = ErrorObject{Status: p(strconv.Itoa(http.StatusForbidden)), Title: p(http.StatusText(http.StatusForbidden))}
res = ErrorObject{Status: P(strconv.Itoa(http.StatusForbidden)), Title: P(http.StatusText(http.StatusForbidden))}
case errors.Is(err, model.ErrNotFound):
res = ErrorObject{Status: p(strconv.Itoa(http.StatusNotFound)), Title: p(http.StatusText(http.StatusNotFound))}
res = ErrorObject{Status: P(strconv.Itoa(http.StatusNotFound)), Title: P(http.StatusText(http.StatusNotFound))}
default:
res = ErrorObject{Status: p(strconv.Itoa(http.StatusInternalServerError)), Title: p(http.StatusText(http.StatusInternalServerError))}
res = ErrorObject{Status: P(strconv.Itoa(http.StatusInternalServerError)), Title: P(http.StatusText(http.StatusInternalServerError))}
}
w.Header().Set("Content-Type", "application/vnd.api+json")
w.WriteHeader(403)
@ -196,9 +181,9 @@ func apiErrorHandler(w http.ResponseWriter, r *http.Request, err error) {
func validationErrorHandler(w http.ResponseWriter, message string, statusCode int) {
_ = GetTracks400JSONResponse{BadRequestJSONResponse{Errors: []ErrorObject{
{
Status: p(strconv.Itoa(statusCode)),
Title: p(http.StatusText(statusCode)),
Detail: p(message),
Status: P(strconv.Itoa(statusCode)),
Title: P(http.StatusText(statusCode)),
Detail: P(message),
},
}}}.VisitGetTracksResponse(w)
}

View file

@ -6,6 +6,7 @@ import (
"net/http/httptest"
"net/url"
. "github.com/navidrome/navidrome/utils/gg"
. "github.com/onsi/ginkgo/v2"
. "github.com/onsi/gomega"
)
@ -34,44 +35,44 @@ var _ = Describe("BuildPaginationLinksAndMeta", func() {
It("returns correct pagination links and meta", func() {
links, meta := buildPaginationLinksAndMeta(totalItems, params, resourceName)
testLinkEquality(links.First, p("api/resource?page[offset]=0&page[limit]=10"))
testLinkEquality(links.Last, p("api/resource?page[offset]=140&page[limit]=10"))
testLinkEquality(links.Next, p("api/resource?page[offset]=10&page[limit]=10"))
testLinkEquality(links.First, P("api/resource?page[offset]=0&page[limit]=10"))
testLinkEquality(links.Last, P("api/resource?page[offset]=140&page[limit]=10"))
testLinkEquality(links.Next, P("api/resource?page[offset]=10&page[limit]=10"))
Expect(links.Prev).To(BeNil())
Expect(meta.CurrentPage).To(Equal(p(int32(1))))
Expect(meta.TotalItems).To(Equal(p(int32(150))))
Expect(meta.TotalPages).To(Equal(p(int32(15))))
Expect(meta.CurrentPage).To(Equal(P(int32(1))))
Expect(meta.TotalItems).To(Equal(P(int32(150))))
Expect(meta.TotalPages).To(Equal(P(int32(15))))
})
})
Context("with custom page limit and offset", func() {
BeforeEach(func() {
params = GetTracksParams{
PageLimit: p((PageLimit)(20)),
PageOffset: p((PageOffset)(40)),
PageLimit: P((PageLimit)(20)),
PageOffset: P((PageOffset)(40)),
}
})
It("returns correct pagination links and meta", func() {
links, meta := buildPaginationLinksAndMeta(totalItems, params, resourceName)
testLinkEquality(links.First, p("api/resource?page[offset]=0&page[limit]=20"))
testLinkEquality(links.Last, p("api/resource?page[offset]=140&page[limit]=20"))
testLinkEquality(links.Next, p("api/resource?page[offset]=60&page[limit]=20"))
testLinkEquality(links.Prev, p("api/resource?page[offset]=20&page[limit]=20"))
testLinkEquality(links.First, P("api/resource?page[offset]=0&page[limit]=20"))
testLinkEquality(links.Last, P("api/resource?page[offset]=140&page[limit]=20"))
testLinkEquality(links.Next, P("api/resource?page[offset]=60&page[limit]=20"))
testLinkEquality(links.Prev, P("api/resource?page[offset]=20&page[limit]=20"))
Expect(meta.CurrentPage).To(Equal(p(int32(3))))
Expect(meta.TotalItems).To(Equal(p(int32(150))))
Expect(meta.TotalPages).To(Equal(p(int32(8))))
Expect(meta.CurrentPage).To(Equal(P(int32(3))))
Expect(meta.TotalItems).To(Equal(P(int32(150))))
Expect(meta.TotalPages).To(Equal(P(int32(8))))
})
})
Context("with various filter params", func() {
BeforeEach(func() {
params = GetTracksParams{
PageLimit: p((PageLimit)(20)),
PageOffset: p((PageOffset)(40)),
PageLimit: P((PageLimit)(20)),
PageOffset: P((PageOffset)(40)),
FilterEquals: &[]string{"property1:value1", "property2:value2"},
FilterContains: &[]string{"property3:value3"},
FilterLessThan: &[]string{"property4:value4"},

View file

@ -190,41 +190,31 @@ var (
// handling the given request, as determined by the presence of X-Forwarded-* headers
// or the scheme and host of the request URL.
func serverAddress(r *http.Request) (scheme, host string) {
// Save the original request host for later comparison.
origHost := r.Host
// Determine the protocol of the request based on the presence of a TLS connection.
protocol := "http"
if r.TLS != nil {
protocol = "https"
}
// Get the X-Forwarded-Host header and extract the first host name if there are
// multiple hosts listed. If there is no X-Forwarded-Host header, use the original
// request host as the default.
xfh := r.Header.Get(xForwardedHost)
if xfh != "" {
i := strings.Index(xfh, ",")
if i == -1 {
i = len(xfh)
}
xfh = xfh[:i]
}
host = FirstOr(r.Host, xfh)
// Determine the protocol and scheme of the request based on the presence of
// X-Forwarded-* headers or the scheme of the request URL.
scheme = FirstOr(
protocol,
scheme = Coalesce(
r.Header.Get(xForwardedProto),
r.Header.Get(xForwardedScheme),
r.URL.Scheme,
protocol,
)
// Get the X-Forwarded-Host header and extract the first host name if there are
// multiple hosts listed. If there is no X-Forwarded-Host header, use the original
// request host as the default.
parts := strings.Split(r.Header.Get(xForwardedHost), ",")
host = Coalesce(parts[0], r.Host)
// If the request host has changed due to the X-Forwarded-Host header, log a trace
// message with the original and new host values, as well as the scheme and URL.
if host != origHost {
log.Trace(r.Context(), "Request host has changed", "origHost", origHost, "host", host, "scheme", scheme, "url", r.URL)
if host != r.Host {
log.Trace(r.Context(), "Request host has changed", "origHost", r.Host, "host", host, "scheme", scheme, "url", r.URL)
}
// Return the scheme and host of the server handling the request.

View file

@ -86,7 +86,7 @@ var _ = Describe("middlewares", func() {
})
})
Context("with X-Forwarded-Host header", func() {
Context("with X-Forwarded-Host header with one host", func() {
BeforeEach(func() {
req, _ = http.NewRequest("GET", "http://example.com", nil)
req.Header.Set("X-Forwarded-Host", "forwarded.example.com")
@ -99,6 +99,19 @@ var _ = Describe("middlewares", func() {
})
})
Context("with X-Forwarded-Host header with multiple hosts", func() {
BeforeEach(func() {
req, _ = http.NewRequest("GET", "http://example.com", nil)
req.Header.Set("X-Forwarded-Host", "forwarded.example.com,forwarded2.example.com")
})
It("should modify the request with the X-Forwarded-Host header value", func() {
middleware.ServeHTTP(recorder, req)
Expect(req.Host).To(Equal("forwarded.example.com"))
Expect(req.URL.Scheme).To(Equal("http"))
})
})
Context("with X-Forwarded-Proto header", func() {
BeforeEach(func() {
req, _ = http.NewRequest("GET", "http://example.com", nil)