diff options
| author | Kyren223 <Kyren223@proton.me> | 2025-07-11 16:07:15 +0300 |
|---|---|---|
| committer | Kyren223 <Kyren223@proton.me> | 2025-07-19 18:32:12 +0300 |
| commit | dc6c10155c33033b386665e7882b91be9837f55a (patch) | |
| tree | 64ca15fdb2aec5a17c93bae99508f70e61b951a6 /internal/client/config/cache.go | |
| parent | 86c3f3f730dd8867f4d5df6fef428df36ff2d193 (diff) | |
Added anonymous_device_analytics option on the client and added cache
dir to store things like device ID and TOS hashes (only storing device
ID currently)
Diffstat (limited to 'internal/client/config/cache.go')
| -rw-r--r-- | internal/client/config/cache.go | 126 |
1 files changed, 126 insertions, 0 deletions
diff --git a/internal/client/config/cache.go b/internal/client/config/cache.go new file mode 100644 index 0000000..a78425b --- /dev/null +++ b/internal/client/config/cache.go @@ -0,0 +1,126 @@ +package config + +import ( + "crypto/rand" + "encoding/json" + "errors" + "fmt" + "os" + "path/filepath" + "reflect" + + "github.com/kyren223/eko/pkg/assert" +) + +type Cache struct { + // Key is a server URL like "eko.kyren.codes" and value is the base 16 hash + TosHashes map[string]string `json:"tos_hashes"` + DeviceID string `json:"device_id"` +} + +func DefaultCache() Cache { + return Cache{ + TosHashes: map[string]string{}, + DeviceID: "", + } +} + +func VerifyAndFixCache(cache *Cache) error { + if cache.DeviceID == "" { + cache.DeviceID = GenerateDeviceID() + } + return nil +} + +func GenerateDeviceID() string { + // Generate a random 32-byte device ID + // Gurantees uniqueness, so it's not reverseable and it is annonymized + // For legal reasons: this may still be logged on the server, so associated with an IP address + deviceId := [32]byte{} + _, err := rand.Read(deviceId[:]) + assert.NoError(err, "random never fails") + return fmt.Sprintf("%x", deviceId) +} + +var ( + cache = Cache{} + CacheDir string + CacheFile string +) + +func LoadCache() error { + userCacheDir, err := os.UserCacheDir() + if err != nil { + return err + } + + CacheDir = filepath.Join(userCacheDir, "eko") + err = os.MkdirAll(CacheDir, 0o750) + if err != nil { + return err + } + + CacheFile = filepath.Join(CacheDir, "cache.json") + contents, err := os.ReadFile(CacheFile) // #nosec 304 + if errors.Is(err, os.ErrNotExist) { + cache = DefaultCache() + return writeCache() + } + if err != nil { + return err + } + + var rawMap map[string]json.RawMessage + if err := json.Unmarshal(contents, &rawMap); err != nil { + return err + } + + defaultVal := reflect.ValueOf(DefaultCache()) + finalCache := reflect.New(defaultVal.Type()).Elem() + finalCache.Set(defaultVal) + + for i := 0; i < defaultVal.NumField(); i++ { + field := defaultVal.Type().Field(i) + jsonTag := field.Tag.Get("json") + if jsonTag == "" { + jsonTag = field.Name + } + + rawValue, found := rawMap[jsonTag] + fieldValue := finalCache.Field(i) + if !found || !fieldValue.CanAddr() { + continue + } + + err := json.Unmarshal(rawValue, fieldValue.Addr().Interface()) + if err != nil { + return fmt.Errorf("error unmarshaling field %s: %w", field.Name, err) + } + } + + cache = finalCache.Interface().(Cache) + + err = VerifyAndFixCache(&cache) + if err != nil { + return err + } + + return writeCache() +} + +func writeCache() error { + b, err := json.MarshalIndent(cache, "", " ") + if err != nil { + return err + } + return os.WriteFile(CacheFile, b, 0o600) +} + +func ReadCache() Cache { + return cache +} + +func UseCache(f func(cache *Cache)) error { + f(&cache) + return writeCache() +} |
