package hub import ( "bytes" "crypto/aes" "crypto/cipher" "crypto/sha1" "crypto/sha256" "encoding/hex" "encoding/json" "errors" "fmt" "net/http" "os" "path/filepath" "strings" "time" "github.com/photoprism/photoprism/internal/hub/places" "github.com/photoprism/photoprism/pkg/fs" "github.com/photoprism/photoprism/pkg/txt" "gopkg.in/yaml.v2" ) // Config represents backend api credentials for maps & geodata. type Config struct { Key string `json:"key" yaml:"Key"` Secret string `json:"secret" yaml:"Secret"` Session string `json:"session" yaml:"Session"` Status string `json:"status" yaml:"Status"` Version string `json:"version" yaml:"Version"` Serial string `json:"serial" yaml:"Serial"` FileName string `json:"-" yaml:"-"` } // NewConfig creates a new backend api credentials instance. func NewConfig(version, fileName, serial string) *Config { return &Config{ Key: "", Secret: "", Session: "", Status: "", Version: version, Serial: serial, FileName: fileName, } } // MapKey returns the maps api key. func (c *Config) MapKey() string { if sess, err := c.DecodeSession(); err != nil { return "" } else { return sess.MapKey } } // Propagate updates backend api credentials in other packages. func (c *Config) Propagate() { places.Key = c.Key places.Secret = c.Secret } // Sanitize verifies and sanitizes backend api credentials. func (c *Config) Sanitize() { c.Key = strings.ToLower(c.Key) if c.Secret != "" { if c.Key != fmt.Sprintf("%x", sha1.Sum([]byte(c.Secret))) { c.Key = "" c.Secret = "" c.Session = "" c.Status = "" } } } // DecodeSession decodes backend api session data. func (c *Config) DecodeSession() (Session, error) { c.Sanitize() result := Session{} if c.Session == "" { return result, fmt.Errorf("empty session") } s, err := hex.DecodeString(c.Session) if err != nil { return result, err } hash := sha256.New() if _, err := hash.Write([]byte(c.Secret)); err != nil { return result, err } var b []byte block, err := aes.NewCipher(hash.Sum(b)) if err != nil { return result, err } iv := s[:aes.BlockSize] plaintext := make([]byte, len(s)) stream := cipher.NewCTR(block, iv) stream.XORKeyStream(plaintext, s[aes.BlockSize:]) plaintext = bytes.Trim(plaintext, "\x00") if err := json.Unmarshal(plaintext, &result); err != nil { return result, err } return result, nil } // Refresh updates backend api credentials. func (c *Config) Refresh() (err error) { mutex.Lock() defer mutex.Unlock() if err := os.MkdirAll(filepath.Dir(c.FileName), os.ModePerm); err != nil { return err } c.Sanitize() client := &http.Client{Timeout: 60 * time.Second} url := ServiceURL method := http.MethodPost var req *http.Request if c.Key != "" { url = fmt.Sprintf(ServiceURL+"/%s", c.Key) method = http.MethodPut log.Debugf("getting updated api key for maps & places from %s", ApiHost()) } else { log.Debugf("requesting api key for maps & places from %s", ApiHost()) } if j, err := json.Marshal(NewRequest(c.Version, c.Serial)); err != nil { return err } else if req, err = http.NewRequest(method, url, bytes.NewReader(j)); err != nil { return err } req.Header.Add("Content-Type", "application/json") var r *http.Response for i := 0; i < 3; i++ { r, err = client.Do(req) if err == nil { break } } if err != nil { return err } else if r.StatusCode >= 400 { err = fmt.Errorf("getting api key from %s failed (error %d)", ApiHost(), r.StatusCode) return err } err = json.NewDecoder(r.Body).Decode(c) if err != nil { return err } return nil } // Load backend api credentials from a YAML file. func (c *Config) Load() error { if !fs.FileExists(c.FileName) { return fmt.Errorf("settings file not found: %s", txt.Quote(c.FileName)) } mutex.Lock() defer mutex.Unlock() yamlConfig, err := os.ReadFile(c.FileName) if err != nil { return err } if err := yaml.Unmarshal(yamlConfig, c); err != nil { return err } c.Sanitize() c.Propagate() if sess, err := c.DecodeSession(); err != nil { return err } else if sess.Expired() { return errors.New("session expired") } return nil } // Save backend api credentials to a YAML file. func (c *Config) Save() error { mutex.Lock() defer mutex.Unlock() c.Sanitize() data, err := yaml.Marshal(c) if err != nil { return err } c.Propagate() if err := os.MkdirAll(filepath.Dir(c.FileName), os.ModePerm); err != nil { return err } if err := os.WriteFile(c.FileName, data, os.ModePerm); err != nil { return err } c.Propagate() return nil }