summaryrefslogtreecommitdiff
path: root/internal/client/config/cache.go
diff options
context:
space:
mode:
authorKyren223 <Kyren223@proton.me>2025-07-11 16:07:15 +0300
committerKyren223 <Kyren223@proton.me>2025-07-19 18:32:12 +0300
commitdc6c10155c33033b386665e7882b91be9837f55a (patch)
tree64ca15fdb2aec5a17c93bae99508f70e61b951a6 /internal/client/config/cache.go
parent86c3f3f730dd8867f4d5df6fef428df36ff2d193 (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.go126
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()
+}