forked from mudler/LocalAI
-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat(ui): path prefix support via HTTP header
Makes the web app honour the `X-Forwarded-Prefix` HTTP request header that may be sent by a reverse-proxy in order to inform the app that its public routes contain a path prefix. For instance this allows to serve the webapp via a reverse-proxy/ingress controller under a path prefix/sub path such as e.g. `/localai/` while still being able to use the regular LocalAI routes/paths without prefix when directly connecting to the LocalAI server. Changes: * Add new `StripPathPrefix` middleware to strip the path prefix (provided with the `X-Forwarded-Prefix` HTTP request header) from the request path prior to matching the HTTP route. * Add a `BaseURL` utility function to build the base URL, honouring the `X-Forwarded-Prefix` HTTP request header. * Generate the derived base URL into the HTML (`head.html` template) as `<base/>` tag. * Make all webapp-internal URLs (within HTML+JS) relative in order to make the browser resolve them against the `<base/>` URL specified within each HTML page's header. * Make font URLs within the CSS files relative to the CSS file. * Generate redirect location URLs using the new `BaseURL` function. * Use the new `BaseURL` function to generate absolute URLs within gallery JSON responses. Closes mudler#3095 TL;DR: The header-based approach allows to move the path prefix configuration concern completely to the reverse-proxy/ingress as opposed to having to align the path prefix configuration between LocalAI, the reverse-proxy and potentially other internal LocalAI clients. The gofiber swagger handler already supports path prefixes this way, see https://github.com/gofiber/swagger/blob/e2d9e9916d8809e8b23c4365f8acfbbd8a71c4cd/swagger.go#L79 Signed-off-by: Max Goltzsche <[email protected]>
- Loading branch information
1 parent
5c29e0c
commit 580101a
Showing
37 changed files
with
470 additions
and
105 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,36 @@ | ||
package middleware | ||
|
||
import ( | ||
"strings" | ||
|
||
"github.com/gofiber/fiber/v2" | ||
) | ||
|
||
// StripPathPrefix returns a middleware that strips a path prefix from the request path. | ||
// The path prefix is obtained from the X-Forwarded-Prefix HTTP request header. | ||
func StripPathPrefix() fiber.Handler { | ||
return func(c *fiber.Ctx) error { | ||
for _, prefix := range c.GetReqHeaders()["X-Forwarded-Prefix"] { | ||
if prefix != "" { | ||
path := c.Path() | ||
pos := len(prefix) | ||
|
||
if prefix[pos-1] == '/' { | ||
pos-- | ||
} else { | ||
prefix += "/" | ||
} | ||
|
||
if strings.HasPrefix(path, prefix) { | ||
c.Path(path[pos:]) | ||
break | ||
} else if prefix[:pos] == path { | ||
c.Redirect(prefix) | ||
return nil | ||
} | ||
} | ||
} | ||
|
||
return c.Next() | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,121 @@ | ||
package middleware | ||
|
||
import ( | ||
"net/http/httptest" | ||
"testing" | ||
|
||
"github.com/gofiber/fiber/v2" | ||
"github.com/stretchr/testify/require" | ||
) | ||
|
||
func TestStripPathPrefix(t *testing.T) { | ||
var actualPath string | ||
|
||
app := fiber.New() | ||
|
||
app.Use(StripPathPrefix()) | ||
|
||
app.Get("/hello/world", func(c *fiber.Ctx) error { | ||
actualPath = c.Path() | ||
return nil | ||
}) | ||
|
||
app.Get("/", func(c *fiber.Ctx) error { | ||
actualPath = c.Path() | ||
return nil | ||
}) | ||
|
||
for _, tc := range []struct { | ||
name string | ||
path string | ||
prefixHeader []string | ||
expectStatus int | ||
expectPath string | ||
}{ | ||
{ | ||
name: "without prefix and header", | ||
path: "/hello/world", | ||
expectStatus: 200, | ||
expectPath: "/hello/world", | ||
}, | ||
{ | ||
name: "without prefix and headers on root path", | ||
path: "/", | ||
expectStatus: 200, | ||
expectPath: "/", | ||
}, | ||
{ | ||
name: "without prefix but header", | ||
path: "/hello/world", | ||
prefixHeader: []string{"/otherprefix/"}, | ||
expectStatus: 200, | ||
expectPath: "/hello/world", | ||
}, | ||
{ | ||
name: "with prefix but non-matching header", | ||
path: "/prefix/hello/world", | ||
prefixHeader: []string{"/otherprefix/"}, | ||
expectStatus: 404, | ||
}, | ||
{ | ||
name: "with prefix and matching header", | ||
path: "/myprefix/hello/world", | ||
prefixHeader: []string{"/myprefix/"}, | ||
expectStatus: 200, | ||
expectPath: "/hello/world", | ||
}, | ||
{ | ||
name: "with prefix and 1st header matching", | ||
path: "/myprefix/hello/world", | ||
prefixHeader: []string{"/myprefix/", "/otherprefix/"}, | ||
expectStatus: 200, | ||
expectPath: "/hello/world", | ||
}, | ||
{ | ||
name: "with prefix and 2nd header matching", | ||
path: "/myprefix/hello/world", | ||
prefixHeader: []string{"/otherprefix/", "/myprefix/"}, | ||
expectStatus: 200, | ||
expectPath: "/hello/world", | ||
}, | ||
{ | ||
name: "with prefix and header not ending with slash", | ||
path: "/myprefix/hello/world", | ||
prefixHeader: []string{"/myprefix"}, | ||
expectStatus: 200, | ||
expectPath: "/hello/world", | ||
}, | ||
{ | ||
name: "with prefix and non-matching header not ending with slash", | ||
path: "/myprefix-suffix/hello/world", | ||
prefixHeader: []string{"/myprefix"}, | ||
expectStatus: 404, | ||
}, | ||
{ | ||
name: "redirect when prefix does not end with a slash", | ||
path: "/myprefix", | ||
prefixHeader: []string{"/myprefix"}, | ||
expectStatus: 302, | ||
expectPath: "/myprefix/", | ||
}, | ||
} { | ||
t.Run(tc.name, func(t *testing.T) { | ||
actualPath = "" | ||
req := httptest.NewRequest("GET", tc.path, nil) | ||
if tc.prefixHeader != nil { | ||
req.Header["X-Forwarded-Prefix"] = tc.prefixHeader | ||
} | ||
|
||
resp, err := app.Test(req, -1) | ||
|
||
require.NoError(t, err) | ||
require.Equal(t, tc.expectStatus, resp.StatusCode, "response status code") | ||
|
||
if tc.expectStatus == 200 { | ||
require.Equal(t, tc.expectPath, actualPath, "rewritten path") | ||
} else if tc.expectStatus == 302 { | ||
require.Equal(t, tc.expectPath, resp.Header.Get("Location"), "redirect location") | ||
} | ||
}) | ||
} | ||
} |
Oops, something went wrong.