github.com/keybase/client/go@v0.0.0-20241007131713-f10651d043c8/uidmap/uidmap.go (about) 1 package uidmap 2 3 import ( 4 "errors" 5 "fmt" 6 "strings" 7 "sync" 8 "time" 9 10 lru "github.com/hashicorp/golang-lru" 11 "github.com/keybase/client/go/libkb" 12 keybase1 "github.com/keybase/client/go/protocol/keybase1" 13 "golang.org/x/net/context" 14 ) 15 16 type UIDMap struct { 17 sync.Mutex 18 usernameCache map[keybase1.UID]libkb.NormalizedUsername 19 fullNameCache *lru.Cache 20 testBatchIterHook func() 21 testNoCachingMode bool 22 serverRefreshers map[keybase1.UID]keybase1.Seqno 23 } 24 25 func NewUIDMap(fullNameCacheSize int) *UIDMap { 26 cache, err := lru.New(fullNameCacheSize) 27 if err != nil { 28 panic(fmt.Sprintf("failed to make an LRU size=%d: %s", fullNameCacheSize, err)) 29 } 30 return &UIDMap{ 31 usernameCache: make(map[keybase1.UID]libkb.NormalizedUsername), 32 fullNameCache: cache, 33 serverRefreshers: make(map[keybase1.UID]keybase1.Seqno), 34 } 35 } 36 37 func usernameDBKey(u keybase1.UID) libkb.DbKey { 38 return libkb.DbKey{Typ: libkb.DBUidToUsername, Key: string(u)} 39 } 40 41 func fullNameDBKey(u keybase1.UID) libkb.DbKey { 42 return libkb.DbKey{Typ: libkb.DBUidToFullName, Key: string(u)} 43 } 44 45 type mapStatus int 46 47 const ( 48 foundHardCoded mapStatus = iota 49 foundInMem mapStatus = iota 50 foundOnDisk mapStatus = iota 51 notFound mapStatus = iota 52 stale mapStatus = iota 53 ) 54 55 // The number of UIDs per batch to send. It's not `const` so we can twiddle it in our tests. 56 var batchSize = 250 57 58 func (u *UIDMap) SetTestingNoCachingMode(enabled bool) { 59 u.testNoCachingMode = enabled 60 } 61 62 func (u *UIDMap) Clear() { 63 u.Lock() 64 defer u.Unlock() 65 u.usernameCache = make(map[keybase1.UID]libkb.NormalizedUsername) 66 u.fullNameCache.Purge() 67 } 68 69 func (u *UIDMap) findUsernamePackageLocally(ctx context.Context, g libkb.UIDMapperContext, uid keybase1.UID, fullNameFreshness time.Duration) (ret *libkb.UsernamePackage, stats mapStatus) { 70 nun, usernameStatus := u.findUsernameLocally(ctx, g, uid) 71 if usernameStatus == notFound { 72 return nil, notFound 73 } 74 fullName, fullNameStatus := u.findFullNameLocally(ctx, g, uid, fullNameFreshness) 75 return &libkb.UsernamePackage{NormalizedUsername: nun, FullName: fullName}, fullNameStatus 76 } 77 78 const CurrentFullNamePackageVersion = keybase1.FullNamePackageVersion_V2 79 80 func isStale(g libkb.UIDMapperContext, m keybase1.FullNamePackage, dur time.Duration) (time.Duration, bool) { 81 if dur == time.Duration(0) { 82 return time.Duration(0), false 83 } 84 now := g.GetClock().Now() 85 cachedAt := m.CachedAt.Time() 86 diff := now.Sub(cachedAt) 87 expired := (diff > dur) 88 return diff, expired 89 } 90 91 func (u *UIDMap) findFullNameLocally(ctx context.Context, g libkb.UIDMapperContext, uid keybase1.UID, fullNameFreshness time.Duration) (ret *keybase1.FullNamePackage, status mapStatus) { 92 93 var staleFullName *keybase1.FullNamePackage 94 var staleExpired time.Duration 95 96 doNotFoundReturn := func() (*keybase1.FullNamePackage, mapStatus) { 97 if staleFullName != nil { 98 return staleFullName, stale 99 } 100 return nil, notFound 101 } 102 103 voidp, ok := u.fullNameCache.Get(uid) 104 if ok { 105 tmp, ok := voidp.(keybase1.FullNamePackage) 106 if !ok { 107 g.GetLog().CDebugf(ctx, "Found non-FullNamePackage in LRU cache for uid=%s", uid) 108 } else if when, expired := isStale(g, tmp, fullNameFreshness); expired { 109 staleFullName = &tmp 110 staleExpired = when 111 g.GetVDebugLog().CLogf(ctx, libkb.VLog0, "fullName memory mapping %s -> %+v is expired (%s ago)", uid, tmp, when) 112 } else { 113 ret = &tmp 114 return ret, foundInMem 115 } 116 } 117 118 var tmp keybase1.FullNamePackage 119 key := fullNameDBKey(uid) 120 found, err := g.GetKVStore().GetInto(&tmp, key) 121 if err != nil { 122 g.GetLog().CDebugf(ctx, "findFullNameLocally: failed to get dbkey %v: %s", key, err) 123 return doNotFoundReturn() 124 } 125 if !found { 126 return doNotFoundReturn() 127 } 128 129 if tmp.Version != CurrentFullNamePackageVersion { 130 g.GetLog().CDebugf(ctx, "Old version (=%d) found for dbkey %s", tmp.Version, key) 131 return doNotFoundReturn() 132 } 133 134 if when, expired := isStale(g, tmp, fullNameFreshness); expired { 135 g.GetVDebugLog().CLogf(ctx, libkb.VLog0, "fullName disk mapping %s -> %+v is expired (%s ago)", uid, tmp, when) 136 if when < staleExpired { 137 staleFullName = &tmp 138 } 139 return doNotFoundReturn() 140 } 141 142 u.fullNameCache.Add(uid, tmp) 143 return ret, foundOnDisk 144 } 145 146 func (u *UIDMap) findUsernameLocally(ctx context.Context, g libkb.UIDMapperContext, uid keybase1.UID) (libkb.NormalizedUsername, mapStatus) { 147 un := findHardcoded(uid) 148 if !un.IsNil() { 149 return un, foundHardCoded 150 } 151 un, ok := u.usernameCache[uid] 152 if ok { 153 return un, foundInMem 154 } 155 var s string 156 key := usernameDBKey(uid) 157 found, err := g.GetKVStore().GetInto(&s, key) 158 if err != nil { 159 g.GetLog().CDebugf(ctx, "findUsernameLocally: failed to get dbkey %v: %s", key, err) 160 return libkb.NormalizedUsername(""), notFound 161 } 162 if !found { 163 return libkb.NormalizedUsername(""), notFound 164 } 165 ret := libkb.NewNormalizedUsername(s) 166 u.usernameCache[uid] = ret 167 return ret, foundOnDisk 168 } 169 170 type apiRow struct { 171 Username string `json:"username"` 172 FullName string `json:"full_name,omitempty"` 173 EldestSeqno keybase1.Seqno `json:"eldest_seqno"` 174 Status keybase1.StatusCode `json:"status"` 175 } 176 177 type apiReply struct { 178 Status libkb.AppStatus `json:"status"` 179 Users map[keybase1.UID]apiRow `json:"users"` 180 } 181 182 func (a *apiReply) GetAppStatus() *libkb.AppStatus { 183 return &a.Status 184 } 185 186 func (u *UIDMap) refreshersForUIDs(uids []keybase1.UID) string { 187 var v []string 188 for _, uid := range uids { 189 if eldestSeqno, found := u.serverRefreshers[uid]; found { 190 v = append(v, (keybase1.UserVersion{Uid: uid, EldestSeqno: eldestSeqno}).String()) 191 } 192 } 193 return strings.Join(v, ",") 194 } 195 196 func (u *UIDMap) lookupFromServerBatch(ctx context.Context, g libkb.UIDMapperContext, uids []keybase1.UID, networkTimeBudget time.Duration) ([]libkb.UsernamePackage, error) { 197 noCache := u.testNoCachingMode 198 arg := libkb.NewRetryAPIArg("user/names") 199 arg.SessionType = libkb.APISessionTypeNONE 200 refreshers := u.refreshersForUIDs(uids) 201 if len(refreshers) > 0 { 202 g.GetLog().CDebugf(ctx, "user/names refreshers: %s", refreshers) 203 } 204 arg.Args = libkb.HTTPArgs{ 205 "uids": libkb.S{Val: libkb.UidsToString(uids)}, 206 "no_cache": libkb.B{Val: noCache}, 207 "refreshers": libkb.S{Val: refreshers}, 208 } 209 if networkTimeBudget > time.Duration(0) { 210 arg.InitialTimeout = networkTimeBudget 211 arg.RetryCount = 0 212 } 213 var r apiReply 214 err := g.GetAPI().PostDecodeCtx(ctx, arg, &r) 215 if err != nil { 216 return nil, err 217 } 218 ret := make([]libkb.UsernamePackage, len(uids)) 219 cachedAt := keybase1.ToTime(g.GetClock().Now()) 220 for i, uid := range uids { 221 if row, ok := r.Users[uid]; ok { 222 nun := libkb.NewNormalizedUsername(row.Username) 223 if !u.CheckUIDAgainstUsername(uid, nun) { 224 g.GetLog().CWarningf(ctx, "Server returned bad UID -> username mapping: %s -> %s", uid, nun) 225 } else { 226 ret[i] = libkb.UsernamePackage{ 227 NormalizedUsername: nun, 228 FullName: &keybase1.FullNamePackage{ 229 Version: CurrentFullNamePackageVersion, 230 FullName: keybase1.FullName(row.FullName), 231 EldestSeqno: row.EldestSeqno, 232 Status: row.Status, 233 CachedAt: cachedAt, 234 }, 235 } 236 } 237 } 238 } 239 return ret, nil 240 } 241 242 func (u *UIDMap) lookupFromServer(ctx context.Context, g libkb.UIDMapperContext, uids []keybase1.UID, networkTimeBudget time.Duration) ([]libkb.UsernamePackage, error) { 243 244 start := g.GetClock().Now() 245 end := start.Add(networkTimeBudget) 246 247 g.GetLog().CDebugf(ctx, "looking up %d uids from server", len(uids)) 248 var ret []libkb.UsernamePackage 249 for i := 0; i < len(uids); i += batchSize { 250 high := i + batchSize 251 if high > len(uids) { 252 high = len(uids) 253 } 254 inb := uids[i:high] 255 var budget time.Duration 256 257 // Only useful for testing... 258 if u.testBatchIterHook != nil { 259 u.testBatchIterHook() 260 } 261 262 if networkTimeBudget > time.Duration(0) { 263 now := g.GetClock().Now() 264 if now.After(end) { 265 return ret, errors.New("ran out of time") 266 } 267 budget = end.Sub(now) 268 } 269 outb, err := u.lookupFromServerBatch(ctx, g, inb, budget) 270 if err != nil { 271 return ret, err 272 } 273 ret = append(ret, outb...) 274 } 275 return ret, nil 276 } 277 278 // InformOfEldestSeqno informs the mapper of an up-to-date (uid,eldestSeqno) pair. 279 // If the cache has a different value, it will clear the cache and then plumb 280 // the pair all the way through to the server, whose cache may also be in need 281 // of busting. Will return true if the cached value was up-to-date, and false 282 // otherwise. 283 func (u *UIDMap) InformOfEldestSeqno(ctx context.Context, g libkb.UIDMapperContext, uv keybase1.UserVersion) (isCurrent bool, err error) { 284 285 // No entry/exit tracing, or common-case tracing, in this function since otherwise 286 // the spam is overwhelming. 287 288 u.Lock() 289 defer u.Unlock() 290 291 uid := uv.Uid 292 isCurrent = true 293 updateDisk := true 294 295 voidp, ok := u.fullNameCache.Get(uid) 296 if ok { 297 tmp, ok := voidp.(keybase1.FullNamePackage) 298 switch { 299 case !ok: 300 g.GetLog().CDebugf(ctx, "Found non-FullNamePackage in LRU cache for uid=%s", uid) 301 case tmp.EldestSeqno < uv.EldestSeqno: 302 g.GetLog().CDebugf(ctx, "Stale eldest memory mapping for uid=%s; we had %d, but latest is %d", uid, tmp.EldestSeqno, uv.EldestSeqno) 303 u.fullNameCache.Remove(uid) 304 isCurrent = false 305 default: 306 // If the memory state of this UID->Eldest mapping is correct, 307 // then there is no reason to check the disk state, since we should 308 // never have a case that the memory state is newer than the disk 309 // state. And hopefully this is the common case! 310 updateDisk = false 311 } 312 } 313 314 if updateDisk { 315 var tmp keybase1.FullNamePackage 316 key := fullNameDBKey(uid) 317 found, err := g.GetKVStore().GetInto(&tmp, key) 318 if err != nil { 319 g.GetLog().CDebugf(ctx, "Error reading %s from UID map disk-backed cache: %s", uid, err) 320 } 321 if found && tmp.EldestSeqno < uv.EldestSeqno { 322 g.GetLog().CDebugf(ctx, "Stale eldest disk mapping for uid=%s; we had %d, but latest is %d", uid, tmp.EldestSeqno, uv.EldestSeqno) 323 if err := g.GetKVStore().Delete(key); err != nil { 324 return false, err 325 } 326 isCurrent = false 327 } 328 } 329 330 if !isCurrent { 331 u.serverRefreshers[uid] = uv.EldestSeqno 332 } 333 334 return isCurrent, nil 335 } 336 337 // MapUIDsToUsernamePackages maps the given set of UIDs to the username 338 // packages, which include a username and a fullname, and when the mapping was 339 // loaded from the server. It blocks on the network until all usernames are 340 // known. If the `forceNetworkForFullNames` flag is specified, it will block on 341 // the network too. If the flag is not specified, then stale values (or unknown 342 // values) are OK, we won't go to network if we lack them. All network calls 343 // are limited by the given timeBudget, or if 0 is specified, there is 344 // indefinite budget. In the response, a nil FullNamePackage means that the 345 // lookup failed. A non-nil FullNamePackage means that some previous lookup 346 // worked, but might be arbitrarily out of date (depending on the cachedAt 347 // time). A non-nil FullNamePackage with an empty fullName field means that the 348 // user just hasn't supplied a fullName. FullNames can be cached by the UIDMap, 349 // but expire after networkTimeBudget duration. If that value is 0, then 350 // infinitely stale names are allowed. If non-zero, and some names aren't 351 // stale, we'll have to go to the network. 352 // 353 // *NOTE* that this function can return useful data and an error. In this 354 // regard, the error is more like a warning. But if, for instance, the mapper 355 // runs out of time budget, it will return the data it was able to get, and 356 // also the error. 357 func (u *UIDMap) MapUIDsToUsernamePackages(ctx context.Context, g libkb.UIDMapperContext, 358 uids []keybase1.UID, fullNameFreshness, networkTimeBudget time.Duration, 359 forceNetworkForFullNames bool) (res []libkb.UsernamePackage, err error) { 360 361 u.Lock() 362 defer u.Unlock() 363 364 res = make([]libkb.UsernamePackage, len(uids)) 365 apiLookupIndex := make(map[int]int) 366 367 var uidsToLookup []keybase1.UID 368 for i, uid := range uids { 369 up, status := u.findUsernamePackageLocally(ctx, g, uid, fullNameFreshness) 370 // If we successfully looked up some of the user, set the return slot here. 371 if up != nil { 372 res[i] = *up 373 } 374 375 // There are 3 important cases when we should go to network: 376 // 377 // 1. No username is found (up == nil) 378 // 2. No FullName found and we've asked to force network lookups (status == notFound && forceNetworkForNullNames) 379 // 3. The FullName found was stale (status == stale). 380 // 381 // Thus, if you provide forceNetworkForFullName=false, and fullNameFreshness=0, you can avoid 382 // the network trip as long as all of your username lookups hit the cache or are hardcoded. 383 if u.testNoCachingMode || up == nil || 384 (status == notFound && forceNetworkForFullNames) || (status == stale) { 385 apiLookupIndex[len(uidsToLookup)] = i 386 uidsToLookup = append(uidsToLookup, uid) 387 } 388 } 389 390 if len(uidsToLookup) > 0 { 391 var apiResults []libkb.UsernamePackage 392 393 apiResults, err = u.lookupFromServer(ctx, g, uidsToLookup, networkTimeBudget) 394 if err == nil { 395 for i, row := range apiResults { 396 uid := uidsToLookup[i] 397 if row.FullName != nil { 398 g.GetVDebugLog().CLogf(ctx, libkb.VLog0, "| API server resolution %s -> (%s, %v, %v)", uid, 399 row.NormalizedUsername, row.FullName.FullName, row.FullName.EldestSeqno) 400 } else { 401 g.GetVDebugLog().CLogf(ctx, libkb.VLog0, "| API server resolution %s -> (%s, <no fn res>)", uid, 402 row.NormalizedUsername) 403 } 404 405 // Always write these results out if the cached value is unset. 406 // Or, see below for other case... 407 writeResults := res[apiLookupIndex[i]].NormalizedUsername.IsNil() 408 409 // Fill in caches independently after a successful return. First fill in 410 // the username cache... 411 if nun := row.NormalizedUsername; !nun.IsNil() { 412 413 // If we get a non-nil NormalizedUsername from the server, then also 414 // write results out... 415 writeResults = true 416 u.usernameCache[uid] = nun 417 key := usernameDBKey(uid) 418 err := g.GetKVStore().PutObj(key, nil, nun.String()) 419 if err != nil { 420 g.GetLog().CDebugf(ctx, "failed to put %v -> %s: %s", key, nun, err) 421 } 422 } 423 424 // Then fill in the fullName cache... 425 if fn := row.FullName; fn != nil { 426 u.fullNameCache.Add(uid, *fn) 427 key := fullNameDBKey(uid) 428 err := g.GetKVStore().PutObj(key, nil, *fn) 429 if err != nil { 430 g.GetLog().CDebugf(ctx, "failed to put %v -> %v: %s", key, *fn, err) 431 } 432 // If we had previously busted this lookup, then clear the refresher 433 // on the server, so we don't have to do it next time through. 434 delete(u.serverRefreshers, uid) 435 } 436 437 if writeResults { 438 // Overwrite the row with whatever was returned from the server. 439 res[apiLookupIndex[i]] = row 440 } 441 } 442 } 443 } 444 445 return res, err 446 } 447 448 func (u *UIDMap) CheckUIDAgainstUsername(uid keybase1.UID, un libkb.NormalizedUsername) bool { 449 return checkUIDAgainstUsername(uid, un) 450 } 451 452 func (u *UIDMap) MapHardcodedUsernameToUID(un libkb.NormalizedUsername) keybase1.UID { 453 return findHardcodedUsername(un) 454 } 455 456 func (u *UIDMap) ClearUIDFullName(ctx context.Context, g libkb.UIDMapperContext, uid keybase1.UID) error { 457 u.Lock() 458 defer u.Unlock() 459 460 u.fullNameCache.Remove(uid) 461 key := fullNameDBKey(uid) 462 if err := g.GetKVStore().Delete(key); err != nil { 463 return err 464 } 465 return nil 466 } 467 468 func (u *UIDMap) ClearUIDAtEldestSeqno(ctx context.Context, g libkb.UIDMapperContext, uid keybase1.UID, s keybase1.Seqno) error { 469 u.Lock() 470 defer u.Unlock() 471 472 voidp, ok := u.fullNameCache.Get(uid) 473 clearDB := false 474 if ok { 475 tmp, ok := voidp.(keybase1.FullNamePackage) 476 if !ok || tmp.EldestSeqno == s { 477 g.GetLog().CDebugf(ctx, "UIDMap: Clearing %s%%%d", uid, s) 478 u.fullNameCache.Remove(uid) 479 clearDB = true 480 } 481 } else { 482 clearDB = true 483 } 484 if clearDB { 485 key := fullNameDBKey(uid) 486 if err := g.GetKVStore().Delete(key); err != nil { 487 return err 488 } 489 } 490 return nil 491 } 492 493 func (u *UIDMap) MapUIDsToUsernamePackagesOffline(ctx context.Context, g libkb.UIDMapperContext, 494 uids []keybase1.UID, fullNameFreshness time.Duration) (res []libkb.UsernamePackage, err error) { 495 // Like MapUIDsToUsernamePackages, but never makes any network calls, 496 // returns only cached values. UIDs that were not cached at all result in 497 // default UsernamePackage, caller has to check if the result is present 498 // using `res[i].NormalizedUsername.IsNil()`. 499 500 u.Lock() 501 defer u.Unlock() 502 503 res = make([]libkb.UsernamePackage, len(uids)) 504 for i, uid := range uids { 505 up, _ := u.findUsernamePackageLocally(ctx, g, uid, fullNameFreshness) 506 // If we successfully looked up some of the user, set the return slot here. 507 if up != nil { 508 res[i] = *up 509 } 510 } 511 512 return res, nil 513 } 514 515 func MapUIDsReturnMap(ctx context.Context, u libkb.UIDMapper, g libkb.UIDMapperContext, uids []keybase1.UID, fullNameFreshness time.Duration, 516 networkTimeBudget time.Duration, forceNetworkForFullNames bool) (res map[keybase1.UID]libkb.UsernamePackage, err error) { 517 518 var uidList []keybase1.UID 519 uidSet := map[keybase1.UID]bool{} 520 521 for _, uid := range uids { 522 _, found := uidSet[uid] 523 if !found { 524 uidSet[uid] = true 525 uidList = append(uidList, uid) 526 } 527 } 528 529 resultList, err := u.MapUIDsToUsernamePackages(ctx, g, uidList, fullNameFreshness, networkTimeBudget, forceNetworkForFullNames) 530 if err != nil && len(resultList) != len(uidList) { 531 return res, err 532 } 533 534 res = make(map[keybase1.UID]libkb.UsernamePackage) 535 for i, uid := range uidList { 536 res[uid] = resultList[i] 537 } 538 return res, err 539 } 540 541 func MapUIDsReturnMapMctx(mctx libkb.MetaContext, uids []keybase1.UID, fullNameFreshness time.Duration, networkTimeBudget time.Duration, 542 forceNetworkForFullNames bool) (res map[keybase1.UID]libkb.UsernamePackage, err error) { 543 // Same as MapUIDsReturnMap, but takes less arguments because of mctx, 544 // which encapsulates ctx, g, and u (g.UIDMapper). 545 g := mctx.G() 546 return MapUIDsReturnMap(mctx.Ctx(), g.UIDMapper, g, uids, fullNameFreshness, networkTimeBudget, forceNetworkForFullNames) 547 } 548 549 var _ libkb.UIDMapper = (*UIDMap)(nil) 550 551 type OfflineUIDMap struct{} 552 553 func (o *OfflineUIDMap) CheckUIDAgainstUsername(uid keybase1.UID, un libkb.NormalizedUsername) bool { 554 return true 555 } 556 557 func (o *OfflineUIDMap) MapHardcodedUsernameToUID(un libkb.NormalizedUsername) keybase1.UID { 558 return findHardcodedUsername(un) 559 } 560 561 func (o *OfflineUIDMap) MapUIDsToUsernamePackages(ctx context.Context, g libkb.UIDMapperContext, uids []keybase1.UID, fullNameFreshness time.Duration, networktimeBudget time.Duration, forceNetworkForFullNames bool) ([]libkb.UsernamePackage, error) { 562 return nil, errors.New("offline uid map always fails") 563 } 564 565 func (o *OfflineUIDMap) SetTestingNoCachingMode(enabled bool) { 566 567 } 568 569 func (o *OfflineUIDMap) ClearUIDFullName(ctx context.Context, g libkb.UIDMapperContext, uid keybase1.UID) error { 570 return nil 571 } 572 573 func (o *OfflineUIDMap) ClearUIDAtEldestSeqno(ctx context.Context, g libkb.UIDMapperContext, uid keybase1.UID, s keybase1.Seqno) error { 574 return nil 575 } 576 577 func (o *OfflineUIDMap) InformOfEldestSeqno(ctx context.Context, g libkb.UIDMapperContext, uv keybase1.UserVersion) (bool, error) { 578 return true, nil 579 } 580 581 func (o *OfflineUIDMap) MapUIDsToUsernamePackagesOffline(ctx context.Context, g libkb.UIDMapperContext, uids []keybase1.UID, fullNameFreshness time.Duration) (res []libkb.UsernamePackage, err error) { 582 // Offline uid map offline call always succeeds but returns nothing. 583 res = make([]libkb.UsernamePackage, len(uids)) 584 return res, nil 585 } 586 587 var _ libkb.UIDMapper = (*OfflineUIDMap)(nil)