1package authenticator
2
3import (
4	"encoding/json"
5	"fmt"
6	"github.com/transip/gotransip/v6/jwt"
7	"io/ioutil"
8	"os"
9	"sync"
10)
11
12// cacheItem is one named item inside the filesystem cache
13type cacheItem struct {
14	// Key of the cache item, containing
15	Key string `json:"key"`
16	// Data containing the content of the cache item
17	Data []byte `json:"data"`
18}
19
20// FileTokenCache is a cache that takes a path and writes a json marshalled File to it,
21// it decodes it when created with the NewFileTokenCache method.
22// It has a Set method to save a token by name as jwt.Token
23// and a Get method one to get a previously acquired token by name returned as jwt.Token
24type FileTokenCache struct {
25	// File contains the cache file
26	File *os.File
27	// CacheItems contains a list of cache items, all of them have a key
28	CacheItems []cacheItem `json:"items"`
29	// prevent simultaneous cache writes
30	writeLock sync.RWMutex
31}
32
33// NewFileTokenCache opens or creates a filesystem cache File on the specified path
34func NewFileTokenCache(path string) (*FileTokenCache, error) {
35	// open the File or create a new one on the given location
36	file, err := os.OpenFile(path, os.O_RDWR|os.O_CREATE, 0644)
37	if err != nil {
38		return &FileTokenCache{}, fmt.Errorf("error opening cache File: %w", err)
39	}
40
41	cache := FileTokenCache{File: file}
42
43	// try to read the File
44	fileContent, err := ioutil.ReadAll(file)
45	if err != nil {
46		return &FileTokenCache{}, fmt.Errorf("error reading cache File: %w", err)
47	}
48
49	if len(fileContent) > 0 {
50		// read the cached File data as json
51		if err := json.Unmarshal(fileContent, &cache); err != nil {
52			return &FileTokenCache{}, fmt.Errorf("error unmarshalling cache File: %w", err)
53		}
54	}
55
56	return &cache, nil
57}
58
59// Set will save a token by name as jwt.Token
60func (f *FileTokenCache) Set(key string, token jwt.Token) error {
61	for idx, item := range f.CacheItems {
62		if item.Key == key {
63			f.CacheItems[idx].Data = []byte(token.String())
64
65			// persist this change to the cache File
66			return f.writeCacheToFile()
67		}
68	}
69
70	// if the key did not exist before, we append a new item to the cache item list
71	f.CacheItems = append(f.CacheItems, cacheItem{Key: key, Data: []byte(token.String())})
72
73	return f.writeCacheToFile()
74}
75
76func (f *FileTokenCache) writeCacheToFile() error {
77	// try to convert the cache to json, so we can write it to File
78	cacheData, err := json.Marshal(f)
79	if err != nil {
80		return fmt.Errorf("error marshalling cache File: %w", err)
81	}
82
83	f.writeLock.Lock()
84	defer f.writeLock.Unlock()
85	// write the cache data to the File cache
86	if err := f.File.Truncate(0); err != nil {
87		return fmt.Errorf("error while truncating cache File: %w", err)
88	}
89	_, err = f.File.WriteAt(cacheData, 0)
90
91	return err
92}
93
94// Get a previously acquired token by name returned as jwt.Token
95func (f *FileTokenCache) Get(key string) (jwt.Token, error) {
96	for _, item := range f.CacheItems {
97		if item.Key == key {
98			dataAsString := string(item.Data)
99
100			return jwt.New(dataAsString)
101		}
102	}
103
104	return jwt.Token{}, nil
105}
106