diff --git a/core/cli/run.go b/core/cli/run.go index 17fb79be..a2bf4732 100644 --- a/core/cli/run.go +++ b/core/cli/run.go @@ -47,6 +47,7 @@ type RunCMD struct { UploadLimit int `env:"LOCALAI_UPLOAD_LIMIT,UPLOAD_LIMIT" default:"15" help:"Default upload-limit in MB" group:"api"` APIKeys []string `env:"LOCALAI_API_KEY,API_KEY" help:"List of API Keys to enable API authentication. When this is set, all the requests must be authenticated with one of these API keys" group:"api"` DisableWebUI bool `env:"LOCALAI_DISABLE_WEBUI,DISABLE_WEBUI" default:"false" help:"Disable webui" group:"api"` + OpaqueErrors bool `env:"LOCALAI_OPAQUE_ERRORS" default:"false" help:"If true, all error responses are replaced with blank 500 errors. This is intended only for hardening against information leaks and is normally not recommended." group:"api"` Peer2Peer bool `env:"LOCALAI_P2P,P2P" name:"p2p" default:"false" help:"Enable P2P mode" group:"p2p"` Peer2PeerToken string `env:"LOCALAI_P2P_TOKEN,P2P_TOKEN" name:"p2ptoken" help:"Token for P2P mode (optional)" group:"p2p"` ParallelRequests bool `env:"LOCALAI_PARALLEL_REQUESTS,PARALLEL_REQUESTS" help:"Enable backends to handle multiple requests in parallel if they support it (e.g.: llama.cpp or vllm)" group:"backends"` @@ -85,6 +86,7 @@ func (r *RunCMD) Run(ctx *cliContext.Context) error { config.WithUploadLimitMB(r.UploadLimit), config.WithApiKeys(r.APIKeys), config.WithModelsURL(append(r.Models, r.ModelArgs...)...), + config.WithOpaqueErrors(r.OpaqueErrors), } if r.Peer2Peer || r.Peer2PeerToken != "" { diff --git a/core/config/application_config.go b/core/config/application_config.go index 9f563842..50fa627d 100644 --- a/core/config/application_config.go +++ b/core/config/application_config.go @@ -31,6 +31,7 @@ type ApplicationConfig struct { PreloadModelsFromPath string CORSAllowOrigins string ApiKeys []string + OpaqueErrors bool ModelLibraryURL string @@ -287,6 +288,12 @@ func WithApiKeys(apiKeys []string) AppOption { } } +func WithOpaqueErrors(opaque bool) AppOption { + return func(o *ApplicationConfig) { + o.OpaqueErrors = opaque + } +} + // ToConfigLoaderOptions returns a slice of ConfigLoader Option. // Some options defined at the application level are going to be passed as defaults for // all the configuration for the models. diff --git a/core/http/app.go b/core/http/app.go index 1ffd6b45..92a79abf 100644 --- a/core/http/app.go +++ b/core/http/app.go @@ -66,15 +66,19 @@ var embedDirStatic embed.FS // @name Authorization func App(cl *config.BackendConfigLoader, ml *model.ModelLoader, appConfig *config.ApplicationConfig) (*fiber.App, error) { - // Return errors as JSON responses - app := fiber.New(fiber.Config{ + + fiberCfg := fiber.Config{ Views: renderEngine(), BodyLimit: appConfig.UploadLimitMB * 1024 * 1024, // this is the default limit of 4MB // We disable the Fiber startup message as it does not conform to structured logging. // We register a startup log line with connection information in the OnListen hook to keep things user friendly though DisableStartupMessage: true, // Override default error handler - ErrorHandler: func(ctx *fiber.Ctx, err error) error { + } + + if !appConfig.OpaqueErrors { + // Normally, return errors as JSON responses + fiberCfg.ErrorHandler = func(ctx *fiber.Ctx, err error) error { // Status code defaults to 500 code := fiber.StatusInternalServerError @@ -90,8 +94,15 @@ func App(cl *config.BackendConfigLoader, ml *model.ModelLoader, appConfig *confi Error: &schema.APIError{Message: err.Error(), Code: code}, }, ) - }, - }) + } + } else { + // If OpaqueErrors are required, replace everything with a blank 500. + fiberCfg.ErrorHandler = func(ctx *fiber.Ctx, _ error) error { + return ctx.Status(500).SendString("") + } + } + + app := fiber.New(fiberCfg) app.Hooks().OnListen(func(listenData fiber.ListenData) error { scheme := "http" @@ -178,7 +189,7 @@ func App(cl *config.BackendConfigLoader, ml *model.ModelLoader, appConfig *confi utils.LoadConfig(appConfig.ConfigsDir, openai.AssistantsConfigFile, &openai.Assistants) utils.LoadConfig(appConfig.ConfigsDir, openai.AssistantsFileConfigFile, &openai.AssistantFiles) - galleryService := services.NewGalleryService(appConfig.ModelPath) + galleryService := services.NewGalleryService(appConfig) galleryService.Start(appConfig.Context, cl) routes.RegisterElevenLabsRoutes(app, cl, ml, appConfig, auth) diff --git a/core/http/app_test.go b/core/http/app_test.go index 6e9de246..75789c4a 100644 --- a/core/http/app_test.go +++ b/core/http/app_test.go @@ -222,6 +222,8 @@ var _ = Describe("API test", func() { Expect(err).ToNot(HaveOccurred()) modelDir = filepath.Join(tmpdir, "models") + err = os.Mkdir(modelDir, 0750) + Expect(err).ToNot(HaveOccurred()) backendAssetsDir := filepath.Join(tmpdir, "backend-assets") err = os.Mkdir(backendAssetsDir, 0750) Expect(err).ToNot(HaveOccurred()) @@ -242,13 +244,13 @@ var _ = Describe("API test", func() { } out, err := yaml.Marshal(g) Expect(err).ToNot(HaveOccurred()) - err = os.WriteFile(filepath.Join(tmpdir, "gallery_simple.yaml"), out, 0600) + err = os.WriteFile(filepath.Join(modelDir, "gallery_simple.yaml"), out, 0600) Expect(err).ToNot(HaveOccurred()) galleries := []gallery.Gallery{ { Name: "test", - URL: "file://" + filepath.Join(tmpdir, "gallery_simple.yaml"), + URL: "file://" + filepath.Join(modelDir, "gallery_simple.yaml"), }, } diff --git a/core/services/gallery.go b/core/services/gallery.go index e20e733a..384f0c12 100644 --- a/core/services/gallery.go +++ b/core/services/gallery.go @@ -3,6 +3,7 @@ package services import ( "context" "encoding/json" + "fmt" "os" "path/filepath" "strings" @@ -16,21 +17,21 @@ import ( ) type GalleryService struct { - modelPath string + appConfig *config.ApplicationConfig sync.Mutex C chan gallery.GalleryOp statuses map[string]*gallery.GalleryOpStatus } -func NewGalleryService(modelPath string) *GalleryService { +func NewGalleryService(appConfig *config.ApplicationConfig) *GalleryService { return &GalleryService{ - modelPath: modelPath, + appConfig: appConfig, C: make(chan gallery.GalleryOp), statuses: make(map[string]*gallery.GalleryOpStatus), } } -func prepareModel(modelPath string, req gallery.GalleryModel, cl *config.BackendConfigLoader, downloadStatus func(string, string, string, float64)) error { +func prepareModel(modelPath string, req gallery.GalleryModel, downloadStatus func(string, string, string, float64)) error { config, err := gallery.GetGalleryConfigFromURL(req.URL, modelPath) if err != nil { @@ -74,8 +75,15 @@ func (g *GalleryService) Start(c context.Context, cl *config.BackendConfigLoader g.UpdateStatus(op.Id, &gallery.GalleryOpStatus{Message: "processing", Progress: 0}) // updates the status with an error - updateError := func(e error) { - g.UpdateStatus(op.Id, &gallery.GalleryOpStatus{Error: e, Processed: true, Message: "error: " + e.Error()}) + var updateError func(e error) + if !g.appConfig.OpaqueErrors { + updateError = func(e error) { + g.UpdateStatus(op.Id, &gallery.GalleryOpStatus{Error: e, Processed: true, Message: "error: " + e.Error()}) + } + } else { + updateError = func(_ error) { + g.UpdateStatus(op.Id, &gallery.GalleryOpStatus{Error: fmt.Errorf("an error occurred"), Processed: true}) + } } // displayDownload displays the download progress @@ -90,7 +98,7 @@ func (g *GalleryService) Start(c context.Context, cl *config.BackendConfigLoader if op.Delete { modelConfig := &config.BackendConfig{} // Galleryname is the name of the model in this case - dat, err := os.ReadFile(filepath.Join(g.modelPath, op.GalleryModelName+".yaml")) + dat, err := os.ReadFile(filepath.Join(g.appConfig.ModelPath, op.GalleryModelName+".yaml")) if err != nil { updateError(err) continue @@ -111,20 +119,20 @@ func (g *GalleryService) Start(c context.Context, cl *config.BackendConfigLoader files = append(files, modelConfig.MMProjFileName()) } - err = gallery.DeleteModelFromSystem(g.modelPath, op.GalleryModelName, files) + err = gallery.DeleteModelFromSystem(g.appConfig.ModelPath, op.GalleryModelName, files) } else { // if the request contains a gallery name, we apply the gallery from the gallery list if op.GalleryModelName != "" { if strings.Contains(op.GalleryModelName, "@") { - err = gallery.InstallModelFromGallery(op.Galleries, op.GalleryModelName, g.modelPath, op.Req, progressCallback) + err = gallery.InstallModelFromGallery(op.Galleries, op.GalleryModelName, g.appConfig.ModelPath, op.Req, progressCallback) } else { - err = gallery.InstallModelFromGalleryByName(op.Galleries, op.GalleryModelName, g.modelPath, op.Req, progressCallback) + err = gallery.InstallModelFromGalleryByName(op.Galleries, op.GalleryModelName, g.appConfig.ModelPath, op.Req, progressCallback) } } else if op.ConfigURL != "" { - startup.PreloadModelsConfigurations(op.ConfigURL, g.modelPath, op.ConfigURL) - err = cl.Preload(g.modelPath) + startup.PreloadModelsConfigurations(op.ConfigURL, g.appConfig.ModelPath, op.ConfigURL) + err = cl.Preload(g.appConfig.ModelPath) } else { - err = prepareModel(g.modelPath, op.Req, cl, progressCallback) + err = prepareModel(g.appConfig.ModelPath, op.Req, progressCallback) } } @@ -134,13 +142,13 @@ func (g *GalleryService) Start(c context.Context, cl *config.BackendConfigLoader } // Reload models - err = cl.LoadBackendConfigsFromPath(g.modelPath) + err = cl.LoadBackendConfigsFromPath(g.appConfig.ModelPath) if err != nil { updateError(err) continue } - err = cl.Preload(g.modelPath) + err = cl.Preload(g.appConfig.ModelPath) if err != nil { updateError(err) continue @@ -163,12 +171,12 @@ type galleryModel struct { ID string `json:"id"` } -func processRequests(modelPath, s string, cm *config.BackendConfigLoader, galleries []gallery.Gallery, requests []galleryModel) error { +func processRequests(modelPath string, galleries []gallery.Gallery, requests []galleryModel) error { var err error for _, r := range requests { utils.ResetDownloadTimers() if r.ID == "" { - err = prepareModel(modelPath, r.GalleryModel, cm, utils.DisplayDownloadFunction) + err = prepareModel(modelPath, r.GalleryModel, utils.DisplayDownloadFunction) } else { if strings.Contains(r.ID, "@") { @@ -183,7 +191,7 @@ func processRequests(modelPath, s string, cm *config.BackendConfigLoader, galler return err } -func ApplyGalleryFromFile(modelPath, s string, cl *config.BackendConfigLoader, galleries []gallery.Gallery) error { +func ApplyGalleryFromFile(modelPath, s string, galleries []gallery.Gallery) error { dat, err := os.ReadFile(s) if err != nil { return err @@ -194,15 +202,15 @@ func ApplyGalleryFromFile(modelPath, s string, cl *config.BackendConfigLoader, g return err } - return processRequests(modelPath, s, cl, galleries, requests) + return processRequests(modelPath, galleries, requests) } -func ApplyGalleryFromString(modelPath, s string, cl *config.BackendConfigLoader, galleries []gallery.Gallery) error { +func ApplyGalleryFromString(modelPath, s string, galleries []gallery.Gallery) error { var requests []galleryModel err := json.Unmarshal([]byte(s), &requests) if err != nil { return err } - return processRequests(modelPath, s, cl, galleries, requests) + return processRequests(modelPath, galleries, requests) } diff --git a/core/startup/startup.go b/core/startup/startup.go index c337afb1..6e6c6707 100644 --- a/core/startup/startup.go +++ b/core/startup/startup.go @@ -82,13 +82,13 @@ func Startup(opts ...config.AppOption) (*config.BackendConfigLoader, *model.Mode } if options.PreloadJSONModels != "" { - if err := services.ApplyGalleryFromString(options.ModelPath, options.PreloadJSONModels, cl, options.Galleries); err != nil { + if err := services.ApplyGalleryFromString(options.ModelPath, options.PreloadJSONModels, options.Galleries); err != nil { return nil, nil, nil, err } } if options.PreloadModelsFromPath != "" { - if err := services.ApplyGalleryFromFile(options.ModelPath, options.PreloadModelsFromPath, cl, options.Galleries); err != nil { + if err := services.ApplyGalleryFromFile(options.ModelPath, options.PreloadModelsFromPath, options.Galleries); err != nil { return nil, nil, nil, err } } @@ -164,7 +164,7 @@ func createApplication(appConfig *config.ApplicationConfig) *core.Application { // app.TextToSpeechBackendService = backend.NewTextToSpeechBackendService(app.ModelLoader, app.BackendConfigLoader, app.ApplicationConfig) app.BackendMonitorService = services.NewBackendMonitorService(app.ModelLoader, app.BackendConfigLoader, app.ApplicationConfig) - app.GalleryService = services.NewGalleryService(app.ApplicationConfig.ModelPath) + app.GalleryService = services.NewGalleryService(app.ApplicationConfig) app.ListModelsService = services.NewListModelsService(app.ModelLoader, app.BackendConfigLoader, app.ApplicationConfig) // app.OpenAIService = services.NewOpenAIService(app.ModelLoader, app.BackendConfigLoader, app.ApplicationConfig, app.LLMBackendService) diff --git a/pkg/downloader/uri.go b/pkg/downloader/uri.go index 0848a238..86e93874 100644 --- a/pkg/downloader/uri.go +++ b/pkg/downloader/uri.go @@ -33,11 +33,17 @@ func GetURI(url string, basePath string, f func(url string, i []byte) error) err if err != nil { return err } - // Check if the local file is rooted in basePath - err = utils.VerifyPath(resolvedFile, basePath) + // ??? + resolvedBasePath, err := filepath.EvalSymlinks(basePath) if err != nil { return err } + // Check if the local file is rooted in basePath + err = utils.InTrustedRoot(resolvedFile, resolvedBasePath) + if err != nil { + log.Debug().Str("resolvedFile", resolvedFile).Str("basePath", basePath).Msg("downloader.GetURI blocked an attempt to ready a file url outside of basePath") + return err + } // Read the response body body, err := os.ReadFile(resolvedFile) if err != nil { diff --git a/pkg/gallery/models_test.go b/pkg/gallery/models_test.go index bfc2b9a6..cae301b8 100644 --- a/pkg/gallery/models_test.go +++ b/pkg/gallery/models_test.go @@ -50,13 +50,14 @@ var _ = Describe("Model test", func() { }} out, err := yaml.Marshal(gallery) Expect(err).ToNot(HaveOccurred()) - err = os.WriteFile(filepath.Join(tempdir, "gallery_simple.yaml"), out, 0600) + galleryFilePath := filepath.Join(tempdir, "gallery_simple.yaml") + err = os.WriteFile(galleryFilePath, out, 0600) Expect(err).ToNot(HaveOccurred()) - + Expect(filepath.IsAbs(galleryFilePath)).To(BeTrue(), galleryFilePath) galleries := []Gallery{ { Name: "test", - URL: "file://" + filepath.Join(tempdir, "gallery_simple.yaml"), + URL: "file://" + galleryFilePath, }, } diff --git a/pkg/model/initializers.go b/pkg/model/initializers.go index e9001f0a..ec58c279 100644 --- a/pkg/model/initializers.go +++ b/pkg/model/initializers.go @@ -466,10 +466,10 @@ func (ml *ModelLoader) GreedyLoader(opts ...Option) (grpc.Backend, error) { log.Info().Msgf("[%s] Loads OK", key) return model, nil } else if modelerr != nil { - err = errors.Join(err, modelerr) + err = errors.Join(err, fmt.Errorf("[%s]: %w", key, modelerr)) log.Info().Msgf("[%s] Fails: %s", key, modelerr.Error()) } else if model == nil { - err = errors.Join(err, fmt.Errorf("backend returned no usable model")) + err = errors.Join(err, fmt.Errorf("backend %s returned no usable model", key)) log.Info().Msgf("[%s] Fails: %s", key, "backend returned no usable model") } } diff --git a/pkg/utils/path.go b/pkg/utils/path.go index 9982bc1e..c1d3e86d 100644 --- a/pkg/utils/path.go +++ b/pkg/utils/path.go @@ -12,7 +12,7 @@ func ExistsInPath(path string, s string) bool { return err == nil } -func inTrustedRoot(path string, trustedRoot string) error { +func InTrustedRoot(path string, trustedRoot string) error { for path != "/" { path = filepath.Dir(path) if path == trustedRoot { @@ -25,7 +25,7 @@ func inTrustedRoot(path string, trustedRoot string) error { // VerifyPath verifies that path is based in basePath. func VerifyPath(path, basePath string) error { c := filepath.Clean(filepath.Join(basePath, path)) - return inTrustedRoot(c, filepath.Clean(basePath)) + return InTrustedRoot(c, filepath.Clean(basePath)) } // SanitizeFileName sanitizes the given filename