diff --git a/internal/server/app_apis.go b/internal/server/app_apis.go index 616310d..7ba0ee2 100644 --- a/internal/server/app_apis.go +++ b/internal/server/app_apis.go @@ -508,21 +508,12 @@ func (s *Server) verifyClientCerts(r *http.Request, authName string) error { func (s *Server) MatchApp(hostHeader, matchPath string) (types.AppInfo, error) { //s.Trace().Msgf("MatchApp %s %s", hostHeader, matchPath) - apps, err := s.apps.GetAllApps() + apps, domainMap, err := s.apps.GetAppInfo() if err != nil { return types.AppInfo{}, err } matchPath = normalizePath(matchPath) - // Find unique domains - domainMap := map[string]bool{} - for _, appInfo := range apps { - if !domainMap[appInfo.Domain] { - domainMap[appInfo.Domain] = true - // TODO : cache domain list - } - } - // Check if host header matches a known domain checkDomain := false if hostHeader != "" && domainMap[hostHeader] { @@ -530,14 +521,22 @@ func (s *Server) MatchApp(hostHeader, matchPath string) (types.AppInfo, error) { } for _, appInfo := range apps { - if checkDomain && appInfo.Domain != hostHeader { - // Request uses known domain, but app is not for this domain - continue - } + if s.config.System.DisableUnknownDomains { + appDomain := cmp.Or(appInfo.Domain, s.config.System.DefaultDomain) + if hostHeader != appDomain { + // Host header does not match + continue + } + } else { + if checkDomain && appInfo.Domain != hostHeader { + // Request uses known domain, but app is not for this domain + continue + } - if !checkDomain && appInfo.Domain != "" { - // Request does not use known domain, but app is for a domain - continue + if !checkDomain && appInfo.Domain != "" { + // Request does not use known domain, but app is for a domain + continue + } } if strings.HasPrefix(matchPath, appInfo.Path) { diff --git a/internal/server/apps.go b/internal/server/apps.go index f5d2c91..780135e 100644 --- a/internal/server/apps.go +++ b/internal/server/apps.go @@ -12,12 +12,13 @@ import ( "github.com/claceio/clace/internal/types" ) -// AppStore is a store of apps. Apps are initialized lazily, the first GetApp call on each app -// will load the app from the database. +// AppStore is a store of apps. List of apps is stored in memory. Apps are initialized lazily, +// AddApp has to be called before GetApp to initialize the app type AppStore struct { *types.Logger - server *Server - allApps []types.AppInfo + server *Server + allApps []types.AppInfo + allDomains map[string]bool mu sync.RWMutex appMap map[types.AppPathDomain]*app.App @@ -31,21 +32,78 @@ func NewAppStore(logger *types.Logger, server *Server) *AppStore { } } -func (a *AppStore) GetAllApps() ([]types.AppInfo, error) { +func (a *AppStore) GetAppInfo() ([]types.AppInfo, map[string]bool, error) { + a.mu.RLock() + if a.allApps != nil { + a.mu.RUnlock() + return a.allApps, a.allDomains, nil + } + a.mu.RUnlock() + + // Get exclusive lock a.mu.Lock() defer a.mu.Unlock() + err := a.updateAppInfo(a.allApps) + if err != nil { + return nil, nil, err + } + return a.allApps, a.allDomains, nil +} + +func (a *AppStore) GetAllApps() ([]types.AppInfo, error) { + a.mu.RLock() if a.allApps != nil { + a.mu.RUnlock() return a.allApps, nil } + a.mu.RUnlock() + + // Get exclusive lock + a.mu.Lock() + defer a.mu.Unlock() + + err := a.updateAppInfo(a.allApps) + if err != nil { + return nil, err + } + return a.allApps, nil +} + +func (a *AppStore) GetAllDomains() (map[string]bool, error) { + a.mu.RLock() + if a.allDomains != nil { + a.mu.RUnlock() + return a.allDomains, nil + } + a.mu.RUnlock() + + // Get exclusive lock + a.mu.Lock() + defer a.mu.Unlock() + + err := a.updateAppInfo(a.allApps) + if err != nil { + return nil, err + } + return a.allDomains, nil +} +func (a *AppStore) updateAppInfo(allApps []types.AppInfo) error { var err error a.allApps, err = a.server.db.GetAllApps(true) if err != nil { - return nil, err + return err } - return a.allApps, nil + a.allDomains = make(map[string]bool) + a.allDomains[a.server.config.System.DefaultDomain] = true + for _, appInfo := range allApps { + if appInfo.Domain != "" { + a.allDomains[appInfo.Domain] = true + } + } + return nil } func (a *AppStore) ClearAllAppCache() { @@ -53,6 +111,7 @@ func (a *AppStore) ClearAllAppCache() { defer a.mu.Unlock() a.allApps = nil + a.allDomains = nil } func (a *AppStore) GetApp(pathDomain types.AppPathDomain) (*app.App, error) { @@ -87,6 +146,7 @@ func (a *AppStore) DeleteLinkedApps(pathDomain types.AppPathDomain) error { a.clearApp(pathDomain) a.allApps = nil + a.allDomains = nil return nil } @@ -106,6 +166,7 @@ func (a *AppStore) DeleteApps(pathDomain []types.AppPathDomain) { a.clearApp(pd) } a.allApps = nil + a.allDomains = nil } func (a *AppStore) UpdateApps(apps []*app.App) { @@ -118,4 +179,5 @@ func (a *AppStore) UpdateApps(apps []*app.App) { a.appMap[types.CreateAppPathDomain(app.Path, app.Domain)] = app } a.allApps = nil + a.allDomains = nil } diff --git a/internal/server/server.go b/internal/server/server.go index 7686f99..0a2c3d1 100644 --- a/internal/server/server.go +++ b/internal/server/server.go @@ -442,8 +442,19 @@ func (s *Server) setupHTTPSServer() (*http.Server, error) { magicConfig := certmagic.NewDefault() magicConfig.OnDemand = &certmagic.OnDemandConfig{ DecisionFunc: func(name string) error { - // Always issue a certificate - return nil + if !s.config.System.DisableUnknownDomains { + // Allow on-demand certificates for unknown domains + return nil + } + + allDomains, err := s.apps.GetAllDomains() + if err != nil { + return err + } + if allDomains[name] { + return nil + } + return fmt.Errorf("unknown domain %s", name) }, } tlsConfig = magicConfig.TLSConfig() diff --git a/internal/system/clace.default.toml b/internal/system/clace.default.toml index f539dd1..b5503b0 100644 --- a/internal/system/clace.default.toml +++ b/internal/system/clace.default.toml @@ -53,6 +53,8 @@ tailwindcss_command = "tailwindcss" file_watcher_debounce_millis = 300 node_path = "" # node module lookup paths https://esbuild.github.io/api/#node-paths container_command = "auto" # "auto" or "docker" or "podman" +default_domain = "localhost" # default domain for apps +disable_unknown_domains = true # disable unknown domains, if default domain is set. Otherwise, unknown domains are allowed [plugin."store.in"] db_connection = "sqlite:$CL_HOME/clace_app.db" diff --git a/internal/system/config_test.go b/internal/system/config_test.go index 0c64e1a..37c2ea3 100644 --- a/internal/system/config_test.go +++ b/internal/system/config_test.go @@ -46,6 +46,8 @@ func TestServerConfig(t *testing.T) { testutil.AssertEqualsString(t, "tailwind command", "tailwindcss", c.System.TailwindCSSCommand) testutil.AssertEqualsInt(t, "file debounce", 300, c.System.FileWatcherDebounceMillis) testutil.AssertEqualsString(t, "node path", "", c.System.NodePath) + testutil.AssertEqualsString(t, "default domain", "localhost", c.System.DefaultDomain) + testutil.AssertEqualsBool(t, "unknown domains", true, c.System.DisableUnknownDomains) // Global Settings testutil.AssertEqualsString(t, "server uri", "$CL_HOME/run/clace.sock", c.ServerUri) diff --git a/internal/types/types.go b/internal/types/types.go index 7e2e121..d9f2e60 100644 --- a/internal/types/types.go +++ b/internal/types/types.go @@ -170,6 +170,8 @@ type SystemConfig struct { FileWatcherDebounceMillis int `toml:"file_watcher_debounce_millis"` NodePath string `toml:"node_path"` ContainerCommand string `toml:"container_command"` + DefaultDomain string `toml:"default_domain"` + DisableUnknownDomains bool `toml:"disable_unknown_domains"` } // GitAuth is a github auth config entry diff --git a/tests/commander/test_load_app.yaml b/tests/commander/test_load_app.yaml index 3aaee01..9f69ace 100644 --- a/tests/commander/test_load_app.yaml +++ b/tests/commander/test_load_app.yaml @@ -82,7 +82,7 @@ tests: stdout: "Disallow: /" exit-code: 0 load0070: # Update app code - command: perl -pi -e 's/Disk Usage/DiskTest Usage/g' ./disk_usage/app.star && sleep 3 + command: perl -pi -e 's/Disk Usage/DiskTest Usage/g' ./disk_usage/app.star && sleep 4 load0080: # with --dev, changes are picked up immediately command: curl -su "admin:qwerty" localhost:25222/disk_usage_dev stderr: