1package libkb
2
3import (
4	"strings"
5	"sync"
6	"time"
7
8	keybase1 "github.com/keybase/client/go/protocol/keybase1"
9)
10
11type Feature string
12type FeatureFlags []Feature
13
14const (
15	EnvironmentFeatureAllowHighSkips   = Feature("env_allow_high_skips")
16	EnvironmentFeatureMerkleCheckpoint = Feature("merkle_checkpoint")
17)
18
19// StringToFeatureFlags returns a set of feature flags
20func StringToFeatureFlags(s string) (ret FeatureFlags) {
21	s = strings.TrimSpace(s)
22	if len(s) == 0 {
23		return ret
24	}
25	v := strings.Split(s, ",")
26	for _, f := range v {
27		ret = append(ret, Feature(strings.TrimSpace(f)))
28	}
29	return ret
30}
31
32// Admin returns true if the admin feature set is on or the user is a keybase
33// admin.
34func (set FeatureFlags) Admin(uid keybase1.UID) bool {
35	for _, f := range set {
36		if f == Feature("admin") {
37			return true
38		}
39	}
40	return IsKeybaseAdmin(uid)
41}
42
43func (set FeatureFlags) HasFeature(feature Feature) bool {
44	for _, f := range set {
45		if f == feature {
46			return true
47		}
48	}
49	return false
50}
51
52func (set FeatureFlags) Empty() bool {
53	return len(set) == 0
54}
55
56type featureSlot struct {
57	on         bool
58	cacheUntil time.Time
59}
60
61// FeatureFlagSet is a set of feature flags for a given user. It will keep track
62// of whether a feature is on or off, and how long until we should check to
63// update
64type FeatureFlagSet struct {
65	sync.RWMutex
66	features map[Feature]*featureSlot
67}
68
69const (
70	FeatureBoxAuditor                 = Feature("box_auditor3")
71	ExperimentalGenericProofs         = Feature("experimental_generic_proofs")
72	FeatureCheckForHiddenChainSupport = Feature("check_for_hidden_chain_support")
73
74	// Show journeycards. This 'preview' flag is for development and admin testing.
75	// This 'preview' flag is known to clients with old buggy journeycard code. For that reason, don't enable it for external users.
76	FeatureJourneycardPreview = Feature("journeycard_preview")
77	FeatureJourneycard        = Feature("journeycard")
78)
79
80// getInitialFeatures returns the features which a new FeatureFlagSet should
81// contain so that they are prefetched the first time the set is used.
82func getInitialFeatures() []Feature {
83	return []Feature{
84		FeatureBoxAuditor,
85		ExperimentalGenericProofs,
86		FeatureCheckForHiddenChainSupport,
87		FeatureJourneycardPreview,
88		FeatureJourneycard}
89}
90
91// NewFeatureFlagSet makes a new set of feature flags.
92func NewFeatureFlagSet() *FeatureFlagSet {
93	features := make(map[Feature]*featureSlot)
94	for _, f := range getInitialFeatures() {
95		features[f] = &featureSlot{}
96	}
97	return &FeatureFlagSet{features: features}
98}
99
100type rawFeatureSlot struct {
101	Value    bool `json:"value"`
102	CacheSec int  `json:"cache_sec"`
103}
104
105type rawFeatures struct {
106	Status   AppStatus                 `json:"status"`
107	Features map[string]rawFeatureSlot `json:"features"`
108}
109
110func (r *rawFeatures) GetAppStatus() *AppStatus {
111	return &r.Status
112}
113
114func (f *featureSlot) readFrom(m MetaContext, r rawFeatureSlot) {
115	f.on = r.Value
116	f.cacheUntil = m.G().Clock().Now().Add(time.Duration(r.CacheSec) * time.Second)
117}
118
119func (s *FeatureFlagSet) InvalidateCache(m MetaContext, f Feature) {
120	s.Lock()
121	defer s.Unlock()
122	slot, found := s.features[f]
123	if !found {
124		return
125	}
126	slot.cacheUntil = m.G().Clock().Now().Add(time.Duration(-1) * time.Second)
127}
128
129func (s *FeatureFlagSet) refreshAllLocked(m MetaContext) (err error) {
130	// collect all feature names in the set, regardless of state
131	var features []string
132	for f := range s.features {
133		features = append(features, string(f))
134	}
135
136	var raw rawFeatures
137	arg := NewAPIArg("user/features")
138	arg.SessionType = APISessionTypeREQUIRED
139	arg.Args = HTTPArgs{
140		"features": S{Val: strings.Join(features, ",")},
141	}
142	err = m.G().API.GetDecode(m, arg, &raw)
143	switch err.(type) {
144	case nil:
145	case LoginRequiredError:
146		// No features for logged-out users
147		return nil
148	default:
149		return err
150	}
151
152	for f, slot := range s.features {
153		rawFeature, ok := raw.Features[string(f)]
154		if !ok {
155			m.Debug("Feature %q wasn't returned from server, not updating", f)
156			continue
157		}
158		slot.readFrom(m, rawFeature)
159		m.Debug("Feature (fetched) %q -> %v (will cache for %ds)", f, slot.on, rawFeature.CacheSec)
160	}
161	return nil
162}
163
164// enabledInCacheRLocked must be called while holding (at least) the read lock on s
165func (s *FeatureFlagSet) enabledInCacheRLocked(m MetaContext, f Feature) (on bool, found bool) {
166	slot, found := s.features[f]
167	if !found {
168		return false, false
169	}
170	if m.G().Clock().Now().Before(slot.cacheUntil) {
171		m.G().GetVDebugLog().CLogf(m.Ctx(), VLog1, "Feature (cached) %q -> %v", f, slot.on)
172		return slot.on, true
173	}
174	return false, false
175}
176
177// EnabledWithError returns if the given feature is enabled, it will return true if it's
178// enabled, and an error if one occurred.
179func (s *FeatureFlagSet) EnabledWithError(m MetaContext, f Feature) (on bool, err error) {
180	m = m.WithLogTag("FEAT")
181
182	s.RLock()
183	if on, found := s.enabledInCacheRLocked(m, f); found {
184		s.RUnlock()
185		return on, nil
186	}
187	s.RUnlock()
188
189	// cache did not help, we need to lock for writing and update
190	s.Lock()
191	defer s.Unlock()
192	// while we were waiting for the write lock, other threads might have already
193	// updated this, check again
194	if on, found := s.enabledInCacheRLocked(m, f); found {
195		return on, nil
196	}
197
198	if _, found := s.features[f]; !found {
199		s.features[f] = &featureSlot{}
200	}
201	err = s.refreshAllLocked(m)
202	if err != nil {
203		return false, err
204	}
205	return s.features[f].on, nil
206}
207
208// Enabled returns if the feature flag is enabled. It ignore errors and just acts
209// as if the feature is off.
210func (s *FeatureFlagSet) Enabled(m MetaContext, f Feature) (on bool) {
211	on, err := s.EnabledWithError(m, f)
212	if err != nil {
213		m.Debug("Error checking feature %q: %v", f, err)
214		return false
215	}
216	return on
217}
218
219// Clear clears out the cached feature flags, for instance if the user
220// is going to logout.
221func (s *FeatureFlagSet) Clear() {
222	s.Lock()
223	defer s.Unlock()
224	s.features = make(map[Feature]*featureSlot)
225}
226
227// FeatureFlagGate allows the server to disable certain features by replying with a
228// FEATURE_FLAG API status code, which is then translated into a FeatureFlagError.
229// We cache these errors for a given amount of time, so we're not spamming the
230// same attempt over and over again.
231type FeatureFlagGate struct {
232	sync.Mutex
233	lastCheck time.Time
234	lastError error
235	feature   Feature
236	cacheFor  time.Duration
237}
238
239// NewFeatureFlagGate makes a gate for the given feature that will cache for the given
240// duration.
241func NewFeatureFlagGate(f Feature, d time.Duration) *FeatureFlagGate {
242	return &FeatureFlagGate{
243		feature:  f,
244		cacheFor: d,
245	}
246}
247
248// DigestError should be called on the result of an API call. It will allow this gate
249// to digest the error and maybe set up its internal caching for when to retry this
250// feature.
251func (f *FeatureFlagGate) DigestError(m MetaContext, err error) {
252	if err == nil {
253		return
254	}
255	ffe, ok := err.(FeatureFlagError)
256	if !ok {
257		return
258	}
259	if ffe.Feature() != f.feature {
260		m.Debug("Got feature flag error for wrong feature: %v", err)
261		return
262	}
263
264	m.Debug("Server reports feature %q is flagged off", f.feature)
265
266	f.Lock()
267	defer f.Unlock()
268	f.lastCheck = m.G().Clock().Now()
269	f.lastError = err
270}
271
272// ErrorIfFlagged should be called to avoid a feature if it's recently
273// been feature-flagged "off" by the server.  In that case, it will return
274// the error that was originally returned by the server.
275func (f *FeatureFlagGate) ErrorIfFlagged(m MetaContext) (err error) {
276	f.Lock()
277	defer f.Unlock()
278	if f.lastError == nil {
279		return nil
280	}
281	diff := m.G().Clock().Now().Sub(f.lastCheck)
282	if diff > f.cacheFor {
283		m.Debug("Feature flag %q expired %d ago, let's give it another try", f.feature, diff)
284		f.lastError = nil
285		f.lastCheck = time.Time{}
286	}
287	return f.lastError
288}
289
290func (f *FeatureFlagGate) Clear() {
291	f.Lock()
292	defer f.Unlock()
293	f.lastError = nil
294}
295