diff --git a/api/message.go b/api/message.go index ca818109..ee3c1ef0 100644 --- a/api/message.go +++ b/api/message.go @@ -105,7 +105,7 @@ func buildWithPaging(ctx *gin.Context, paging *pagingParams, messages []*model.M useMessages = messages[:len(messages)-1] since = useMessages[len(useMessages)-1].ID url := location.Get(ctx) - url.Path = ctx.Request.URL.Path + url.Path = withForwardedPrefix(ctx, ctx.Request.URL.Path) query := url.Query() query.Add("limit", strconv.Itoa(paging.Limit)) query.Add("since", strconv.FormatUint(uint64(since), 10)) @@ -118,6 +118,20 @@ func buildWithPaging(ctx *gin.Context, paging *pagingParams, messages []*model.M } } +// withForwardedPrefix prepends the reverse-proxy path prefix to the given +// request path. Reverse proxies that strip a subpath before forwarding the +// request (e.g. Caddy's `uri strip_prefix /gotify`) can announce the stripped +// prefix via the X-Forwarded-Prefix header, so that URLs emitted by gotify — +// like the paging `next` link — keep pointing at the externally reachable path +// instead of dropping the prefix. See #664. +func withForwardedPrefix(ctx *gin.Context, path string) string { + prefix := strings.Trim(ctx.GetHeader("X-Forwarded-Prefix"), "/") + if prefix == "" { + return path + } + return "/" + prefix + "/" + strings.TrimPrefix(path, "/") +} + func withPaging(ctx *gin.Context, f func(pagingParams *pagingParams)) { params := &pagingParams{Limit: 100} if err := ctx.MustBindWith(params, binding.Query); err == nil { diff --git a/api/message_test.go b/api/message_test.go index 2a1d4a2b..968318aa 100644 --- a/api/message_test.go +++ b/api/message_test.go @@ -106,6 +106,33 @@ func (s *MessageSuite) Test_GetMessages_WithLimit_ReturnsNext() { test.BodyEquals(s.T(), expected, s.recorder) } +func (s *MessageSuite) Test_GetMessages_WithLimit_BehindProxyPrefix_ReturnsNextWithPrefix() { + user := s.db.User(5) + app1 := user.App(1) + app2 := user.App(2) + var messages []*model.Message + for i := 100; i >= 1; i -= 2 { + one := app2.NewMessage(uint(i)) + two := app1.NewMessage(uint(i - 1)) + messages = append(messages, &one, &two) + } + + // The reverse proxy strips the /gotify prefix, so gotify receives /message, + // but announces the stripped prefix via X-Forwarded-Prefix. The emitted + // next url must keep the prefix. See #664. + s.withURL("http", "example.com", "/message", "limit=5") + s.ctx.Request.Header.Set("X-Forwarded-Prefix", "/gotify") + test.WithUser(s.ctx, 5) + s.a.GetMessages(s.ctx) + + expected := &model.PagedMessages{ + Paging: model.Paging{Limit: 5, Size: 5, Since: 96, Next: "http://example.com/gotify/message?limit=5&since=96"}, + Messages: toExternalMessages(messages[:5]), + } + + test.BodyEquals(s.T(), expected, s.recorder) +} + func (s *MessageSuite) Test_GetMessages_WithLimit_WithSince_ReturnsNext() { user := s.db.User(5) app1 := user.App(1)