package config import ( "bufio" "os" "strings" "sync" ) // EngineCredentials reads the Basic credentials the engine generated for // itself, from the file it writes them to. // // The engine only invents a credential when none is configured and its API // listens on a routable address - which is the DEFAULT configuration, so this // is the ordinary case and not an edge one. `paths.APICredentials` has existed // since the agent was written, with a comment saying the agent reads the file // "rather than storing a second copy, so a regenerated credential does not // silently break the tray". Nothing read it. On a stock install the agent's // api_user was therefore empty and every call it makes to the engine - health, // stats, camera sync, embeddings for a visit - came back 401: the tray red, the // cameras never reconciled, and no error anywhere saying why. // // A missing or unreadable file is not an error. A PC where the operator set // BEHAVISION_API_USER has no such file and needs none. func EngineCredentials(path string) (user, password string) { f, err := os.Open(path) if err != nil { return "", "" } defer f.Close() sc := bufio.NewScanner(f) for sc.Scan() { // `key=value`, and `key: value` too: the file is also read by people, // and which separator the engine used is not worth a support call. line := strings.TrimSpace(sc.Text()) k, v, ok := strings.Cut(line, "=") if !ok { k, v, ok = strings.Cut(line, ":") } if !ok { continue } switch strings.TrimSpace(k) { case "username": user = strings.TrimSpace(v) case "password": password = strings.TrimSpace(v) } } return user, password } // WithEngineCredentials fills in the engine's Basic credentials from the file // when the config carries none. Configured values always win: an operator who // set BEHAVISION_API_USER means it. func (c Config) WithEngineCredentials(path string) Config { if c.APIUser != "" || c.APIPassword != "" { return c } c.APIUser, c.APIPassword = EngineCredentials(path) return c } // Creds resolves the engine's Basic credentials, re-reading the file when it // has none. // // Reading once at startup is wrong on a fresh install, and that is the case // that matters: the agent starts the engine, the engine generates its // credential and writes the file a few seconds later, and an agent that read // the file before that holds "" forever. Every call it makes - health, stats, // camera sync, the embedding for a visit - then comes back 401 for the life of // the process, on a brand new shop PC, with the tray showing a red engine that // is running perfectly. Measured on a fresh state directory: three 401s and no // camera ever reconciled. // // A configured credential is never re-read: an operator who set // BEHAVISION_API_USER means it. type Creds struct { path string mu sync.Mutex user string pass string fixed bool } // NewCreds takes whatever the config already has. Non-empty means configured, // and is used unchanged. func NewCreds(path, user, password string) *Creds { c := &Creds{path: path, user: user, pass: password} c.fixed = user != "" || password != "" return c } // Get returns the current pair, reading the file if it has nothing yet. func (c *Creds) Get() (string, string) { c.mu.Lock() defer c.mu.Unlock() if c.user == "" && !c.fixed { c.user, c.pass = EngineCredentials(c.path) } return c.user, c.pass } // Refresh re-reads the file after a rejection and reports whether the pair // changed. Callers retry once when it did - which covers both the fresh-install // race and a credential the engine regenerated under a running agent. func (c *Creds) Refresh() bool { c.mu.Lock() defer c.mu.Unlock() if c.fixed { return false } u, p := EngineCredentials(c.path) if u == c.user && p == c.pass { return false } c.user, c.pass = u, p return u != "" }