mirror of
https://github.com/mudler/LocalAI.git
synced 2025-02-11 13:15:20 +00:00
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 #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 e2d9e9916d/swagger.go (L79)
Signed-off-by: Max Goltzsche <max.goltzsche@gmail.com>
99 lines
3.2 KiB
Go
99 lines
3.2 KiB
Go
package middleware
|
|
|
|
import (
|
|
"crypto/subtle"
|
|
"errors"
|
|
|
|
"github.com/dave-gray101/v2keyauth"
|
|
"github.com/gofiber/fiber/v2"
|
|
"github.com/gofiber/fiber/v2/middleware/keyauth"
|
|
"github.com/mudler/LocalAI/core/config"
|
|
"github.com/mudler/LocalAI/core/http/utils"
|
|
)
|
|
|
|
// This file contains the configuration generators and handler functions that are used along with the fiber/keyauth middleware
|
|
// Currently this requires an upstream patch - and feature patches are no longer accepted to v2
|
|
// Therefore `dave-gray101/v2keyauth` contains the v2 backport of the middleware until v3 stabilizes and we migrate.
|
|
|
|
func GetKeyAuthConfig(applicationConfig *config.ApplicationConfig) (*v2keyauth.Config, error) {
|
|
customLookup, err := v2keyauth.MultipleKeySourceLookup([]string{"header:Authorization", "header:x-api-key", "header:xi-api-key", "cookie:token"}, keyauth.ConfigDefault.AuthScheme)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
return &v2keyauth.Config{
|
|
CustomKeyLookup: customLookup,
|
|
Next: getApiKeyRequiredFilterFunction(applicationConfig),
|
|
Validator: getApiKeyValidationFunction(applicationConfig),
|
|
ErrorHandler: getApiKeyErrorHandler(applicationConfig),
|
|
AuthScheme: "Bearer",
|
|
}, nil
|
|
}
|
|
|
|
func getApiKeyErrorHandler(applicationConfig *config.ApplicationConfig) fiber.ErrorHandler {
|
|
return func(ctx *fiber.Ctx, err error) error {
|
|
if errors.Is(err, v2keyauth.ErrMissingOrMalformedAPIKey) {
|
|
if len(applicationConfig.ApiKeys) == 0 {
|
|
return ctx.Next() // if no keys are set up, any error we get here is not an error.
|
|
}
|
|
ctx.Set("WWW-Authenticate", "Bearer")
|
|
if applicationConfig.OpaqueErrors {
|
|
return ctx.SendStatus(401)
|
|
}
|
|
return ctx.Status(401).Render("views/login", fiber.Map{
|
|
"BaseURL": utils.BaseURL(ctx),
|
|
})
|
|
}
|
|
if applicationConfig.OpaqueErrors {
|
|
return ctx.SendStatus(500)
|
|
}
|
|
return err
|
|
}
|
|
}
|
|
|
|
func getApiKeyValidationFunction(applicationConfig *config.ApplicationConfig) func(*fiber.Ctx, string) (bool, error) {
|
|
|
|
if applicationConfig.UseSubtleKeyComparison {
|
|
return func(ctx *fiber.Ctx, apiKey string) (bool, error) {
|
|
if len(applicationConfig.ApiKeys) == 0 {
|
|
return true, nil // If no keys are setup, accept everything
|
|
}
|
|
for _, validKey := range applicationConfig.ApiKeys {
|
|
if subtle.ConstantTimeCompare([]byte(apiKey), []byte(validKey)) == 1 {
|
|
return true, nil
|
|
}
|
|
}
|
|
return false, v2keyauth.ErrMissingOrMalformedAPIKey
|
|
}
|
|
}
|
|
|
|
return func(ctx *fiber.Ctx, apiKey string) (bool, error) {
|
|
if len(applicationConfig.ApiKeys) == 0 {
|
|
return true, nil // If no keys are setup, accept everything
|
|
}
|
|
for _, validKey := range applicationConfig.ApiKeys {
|
|
if apiKey == validKey {
|
|
return true, nil
|
|
}
|
|
}
|
|
return false, v2keyauth.ErrMissingOrMalformedAPIKey
|
|
}
|
|
}
|
|
|
|
func getApiKeyRequiredFilterFunction(applicationConfig *config.ApplicationConfig) func(*fiber.Ctx) bool {
|
|
if applicationConfig.DisableApiKeyRequirementForHttpGet {
|
|
return func(c *fiber.Ctx) bool {
|
|
if c.Method() != "GET" {
|
|
return false
|
|
}
|
|
for _, rx := range applicationConfig.HttpGetExemptedEndpoints {
|
|
if rx.MatchString(c.Path()) {
|
|
return true
|
|
}
|
|
}
|
|
return false
|
|
}
|
|
}
|
|
return func(c *fiber.Ctx) bool { return false }
|
|
}
|