github.com/nats-io/nats-server/v2@v2.11.0-preview.2/server/dirstore.go (about) 1 // Copyright 2012-2021 The NATS Authors 2 // Licensed under the Apache License, Version 2.0 (the "License"); 3 // you may not use this file except in compliance with the License. 4 // You may obtain a copy of the License at 5 // 6 // http://www.apache.org/licenses/LICENSE-2.0 7 // 8 // Unless required by applicable law or agreed to in writing, software 9 // distributed under the License is distributed on an "AS IS" BASIS, 10 // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 11 // See the License for the specific language governing permissions and 12 // limitations under the License. 13 14 package server 15 16 import ( 17 "bytes" 18 "container/heap" 19 "container/list" 20 "crypto/sha256" 21 "errors" 22 "fmt" 23 "math" 24 "os" 25 "path/filepath" 26 "strings" 27 "sync" 28 "time" 29 30 "github.com/nats-io/nkeys" 31 32 "github.com/nats-io/jwt/v2" // only used to decode, not for storage 33 ) 34 35 const ( 36 fileExtension = ".jwt" 37 ) 38 39 // validatePathExists checks that the provided path exists and is a dir if requested 40 func validatePathExists(path string, dir bool) (string, error) { 41 if path == _EMPTY_ { 42 return _EMPTY_, errors.New("path is not specified") 43 } 44 45 abs, err := filepath.Abs(path) 46 if err != nil { 47 return _EMPTY_, fmt.Errorf("error parsing path [%s]: %v", abs, err) 48 } 49 50 var finfo os.FileInfo 51 if finfo, err = os.Stat(abs); os.IsNotExist(err) { 52 return _EMPTY_, fmt.Errorf("the path [%s] doesn't exist", abs) 53 } 54 55 mode := finfo.Mode() 56 if dir && mode.IsRegular() { 57 return _EMPTY_, fmt.Errorf("the path [%s] is not a directory", abs) 58 } 59 60 if !dir && mode.IsDir() { 61 return _EMPTY_, fmt.Errorf("the path [%s] is not a file", abs) 62 } 63 64 return abs, nil 65 } 66 67 // ValidateDirPath checks that the provided path exists and is a dir 68 func validateDirPath(path string) (string, error) { 69 return validatePathExists(path, true) 70 } 71 72 // JWTChanged functions are called when the store file watcher notices a JWT changed 73 type JWTChanged func(publicKey string) 74 75 // DirJWTStore implements the JWT Store interface, keeping JWTs in an optionally sharded 76 // directory structure 77 type DirJWTStore struct { 78 sync.Mutex 79 directory string 80 shard bool 81 readonly bool 82 deleteType deleteType 83 operator map[string]struct{} 84 expiration *expirationTracker 85 changed JWTChanged 86 deleted JWTChanged 87 } 88 89 func newDir(dirPath string, create bool) (string, error) { 90 fullPath, err := validateDirPath(dirPath) 91 if err != nil { 92 if !create { 93 return _EMPTY_, err 94 } 95 if err = os.MkdirAll(dirPath, defaultDirPerms); err != nil { 96 return _EMPTY_, err 97 } 98 if fullPath, err = validateDirPath(dirPath); err != nil { 99 return _EMPTY_, err 100 } 101 } 102 return fullPath, nil 103 } 104 105 // future proofing in case new options will be added 106 type dirJWTStoreOption any 107 108 // Creates a directory based jwt store. 109 // Reads files only, does NOT watch directories and files. 110 func NewImmutableDirJWTStore(dirPath string, shard bool, _ ...dirJWTStoreOption) (*DirJWTStore, error) { 111 theStore, err := NewDirJWTStore(dirPath, shard, false, nil) 112 if err != nil { 113 return nil, err 114 } 115 theStore.readonly = true 116 return theStore, nil 117 } 118 119 // Creates a directory based jwt store. 120 // Operates on files only, does NOT watch directories and files. 121 func NewDirJWTStore(dirPath string, shard bool, create bool, _ ...dirJWTStoreOption) (*DirJWTStore, error) { 122 fullPath, err := newDir(dirPath, create) 123 if err != nil { 124 return nil, err 125 } 126 theStore := &DirJWTStore{ 127 directory: fullPath, 128 shard: shard, 129 } 130 return theStore, nil 131 } 132 133 type deleteType int 134 135 const ( 136 NoDelete deleteType = iota 137 RenameDeleted 138 HardDelete 139 ) 140 141 // Creates a directory based jwt store. 142 // 143 // When ttl is set deletion of file is based on it and not on the jwt expiration 144 // To completely disable expiration (including expiration in jwt) set ttl to max duration time.Duration(math.MaxInt64) 145 // 146 // limit defines how many files are allowed at any given time. Set to math.MaxInt64 to disable. 147 // evictOnLimit determines the behavior once limit is reached. 148 // * true - Evict based on lru strategy 149 // * false - return an error 150 func NewExpiringDirJWTStore(dirPath string, shard bool, create bool, delete deleteType, expireCheck time.Duration, limit int64, 151 evictOnLimit bool, ttl time.Duration, changeNotification JWTChanged, _ ...dirJWTStoreOption) (*DirJWTStore, error) { 152 fullPath, err := newDir(dirPath, create) 153 if err != nil { 154 return nil, err 155 } 156 theStore := &DirJWTStore{ 157 directory: fullPath, 158 shard: shard, 159 deleteType: delete, 160 changed: changeNotification, 161 } 162 if expireCheck <= 0 { 163 if ttl != 0 { 164 expireCheck = ttl / 2 165 } 166 if expireCheck == 0 || expireCheck > time.Minute { 167 expireCheck = time.Minute 168 } 169 } 170 if limit <= 0 { 171 limit = math.MaxInt64 172 } 173 theStore.startExpiring(expireCheck, limit, evictOnLimit, ttl) 174 theStore.Lock() 175 err = filepath.Walk(dirPath, func(path string, info os.FileInfo, err error) error { 176 if strings.HasSuffix(path, fileExtension) { 177 if theJwt, err := os.ReadFile(path); err == nil { 178 hash := sha256.Sum256(theJwt) 179 _, file := filepath.Split(path) 180 theStore.expiration.track(strings.TrimSuffix(file, fileExtension), &hash, string(theJwt)) 181 } 182 } 183 return nil 184 }) 185 theStore.Unlock() 186 if err != nil { 187 theStore.Close() 188 return nil, err 189 } 190 return theStore, err 191 } 192 193 func (store *DirJWTStore) IsReadOnly() bool { 194 return store.readonly 195 } 196 197 func (store *DirJWTStore) LoadAcc(publicKey string) (string, error) { 198 return store.load(publicKey) 199 } 200 201 func (store *DirJWTStore) SaveAcc(publicKey string, theJWT string) error { 202 return store.save(publicKey, theJWT) 203 } 204 205 func (store *DirJWTStore) LoadAct(hash string) (string, error) { 206 return store.load(hash) 207 } 208 209 func (store *DirJWTStore) SaveAct(hash string, theJWT string) error { 210 return store.save(hash, theJWT) 211 } 212 213 func (store *DirJWTStore) Close() { 214 store.Lock() 215 defer store.Unlock() 216 if store.expiration != nil { 217 store.expiration.close() 218 store.expiration = nil 219 } 220 } 221 222 // Pack up to maxJWTs into a package 223 func (store *DirJWTStore) Pack(maxJWTs int) (string, error) { 224 count := 0 225 var pack []string 226 if maxJWTs > 0 { 227 pack = make([]string, 0, maxJWTs) 228 } else { 229 pack = []string{} 230 } 231 store.Lock() 232 err := filepath.Walk(store.directory, func(path string, info os.FileInfo, err error) error { 233 if !info.IsDir() && strings.HasSuffix(path, fileExtension) { // this is a JWT 234 if count == maxJWTs { // won't match negative 235 return nil 236 } 237 pubKey := strings.TrimSuffix(filepath.Base(path), fileExtension) 238 if store.expiration != nil { 239 if _, ok := store.expiration.idx[pubKey]; !ok { 240 return nil // only include indexed files 241 } 242 } 243 jwtBytes, err := os.ReadFile(path) 244 if err != nil { 245 return err 246 } 247 if store.expiration != nil { 248 claim, err := jwt.DecodeGeneric(string(jwtBytes)) 249 if err == nil && claim.Expires > 0 && claim.Expires < time.Now().Unix() { 250 return nil 251 } 252 } 253 pack = append(pack, fmt.Sprintf("%s|%s", pubKey, string(jwtBytes))) 254 count++ 255 } 256 return nil 257 }) 258 store.Unlock() 259 if err != nil { 260 return _EMPTY_, err 261 } else { 262 return strings.Join(pack, "\n"), nil 263 } 264 } 265 266 // Pack up to maxJWTs into a message and invoke callback with it 267 func (store *DirJWTStore) PackWalk(maxJWTs int, cb func(partialPackMsg string)) error { 268 if maxJWTs <= 0 || cb == nil { 269 return errors.New("bad arguments to PackWalk") 270 } 271 var packMsg []string 272 store.Lock() 273 dir := store.directory 274 exp := store.expiration 275 store.Unlock() 276 err := filepath.Walk(dir, func(path string, info os.FileInfo, err error) error { 277 if info != nil && !info.IsDir() && strings.HasSuffix(path, fileExtension) { // this is a JWT 278 pubKey := strings.TrimSuffix(filepath.Base(path), fileExtension) 279 store.Lock() 280 if exp != nil { 281 if _, ok := exp.idx[pubKey]; !ok { 282 store.Unlock() 283 return nil // only include indexed files 284 } 285 } 286 store.Unlock() 287 jwtBytes, err := os.ReadFile(path) 288 if err != nil { 289 return err 290 } 291 if len(jwtBytes) == 0 { 292 // Skip if no contents in the JWT. 293 return nil 294 } 295 if exp != nil { 296 claim, err := jwt.DecodeGeneric(string(jwtBytes)) 297 if err == nil && claim.Expires > 0 && claim.Expires < time.Now().Unix() { 298 return nil 299 } 300 } 301 packMsg = append(packMsg, fmt.Sprintf("%s|%s", pubKey, string(jwtBytes))) 302 if len(packMsg) == maxJWTs { // won't match negative 303 cb(strings.Join(packMsg, "\n")) 304 packMsg = nil 305 } 306 } 307 return nil 308 }) 309 if packMsg != nil { 310 cb(strings.Join(packMsg, "\n")) 311 } 312 return err 313 } 314 315 // Merge takes the JWTs from package and adds them to the store 316 // Merge is destructive in the sense that it doesn't check if the JWT 317 // is newer or anything like that. 318 func (store *DirJWTStore) Merge(pack string) error { 319 newJWTs := strings.Split(pack, "\n") 320 for _, line := range newJWTs { 321 if line == _EMPTY_ { // ignore blank lines 322 continue 323 } 324 split := strings.Split(line, "|") 325 if len(split) != 2 { 326 return fmt.Errorf("line in package didn't contain 2 entries: %q", line) 327 } 328 pubKey := split[0] 329 if !nkeys.IsValidPublicAccountKey(pubKey) { 330 return fmt.Errorf("key to merge is not a valid public account key") 331 } 332 if err := store.saveIfNewer(pubKey, split[1]); err != nil { 333 return err 334 } 335 } 336 return nil 337 } 338 339 func (store *DirJWTStore) Reload() error { 340 store.Lock() 341 exp := store.expiration 342 if exp == nil || store.readonly { 343 store.Unlock() 344 return nil 345 } 346 idx := exp.idx 347 changed := store.changed 348 isCache := store.expiration.evictOnLimit 349 // clear out indexing data structures 350 exp.heap = make([]*jwtItem, 0, len(exp.heap)) 351 exp.idx = make(map[string]*list.Element) 352 exp.lru = list.New() 353 exp.hash = [sha256.Size]byte{} 354 store.Unlock() 355 return filepath.Walk(store.directory, func(path string, info os.FileInfo, err error) error { 356 if strings.HasSuffix(path, fileExtension) { 357 if theJwt, err := os.ReadFile(path); err == nil { 358 hash := sha256.Sum256(theJwt) 359 _, file := filepath.Split(path) 360 pkey := strings.TrimSuffix(file, fileExtension) 361 notify := isCache // for cache, issue cb even when file not present (may have been evicted) 362 if i, ok := idx[pkey]; ok { 363 notify = !bytes.Equal(i.Value.(*jwtItem).hash[:], hash[:]) 364 } 365 store.Lock() 366 exp.track(pkey, &hash, string(theJwt)) 367 store.Unlock() 368 if notify && changed != nil { 369 changed(pkey) 370 } 371 } 372 } 373 return nil 374 }) 375 } 376 377 func (store *DirJWTStore) pathForKey(publicKey string) string { 378 if len(publicKey) < 2 { 379 return _EMPTY_ 380 } 381 if !nkeys.IsValidPublicKey(publicKey) { 382 return _EMPTY_ 383 } 384 fileName := fmt.Sprintf("%s%s", publicKey, fileExtension) 385 if store.shard { 386 last := publicKey[len(publicKey)-2:] 387 return filepath.Join(store.directory, last, fileName) 388 } else { 389 return filepath.Join(store.directory, fileName) 390 } 391 } 392 393 // Load checks the memory store and returns the matching JWT or an error 394 // Assumes lock is NOT held 395 func (store *DirJWTStore) load(publicKey string) (string, error) { 396 store.Lock() 397 defer store.Unlock() 398 if path := store.pathForKey(publicKey); path == _EMPTY_ { 399 return _EMPTY_, fmt.Errorf("invalid public key") 400 } else if data, err := os.ReadFile(path); err != nil { 401 return _EMPTY_, err 402 } else { 403 if store.expiration != nil { 404 store.expiration.updateTrack(publicKey) 405 } 406 return string(data), nil 407 } 408 } 409 410 // write that keeps hash of all jwt in sync 411 // Assumes the lock is held. Does return true or an error never both. 412 func (store *DirJWTStore) write(path string, publicKey string, theJWT string) (bool, error) { 413 if len(theJWT) == 0 { 414 return false, fmt.Errorf("invalid JWT") 415 } 416 var newHash *[sha256.Size]byte 417 if store.expiration != nil { 418 h := sha256.Sum256([]byte(theJWT)) 419 newHash = &h 420 if v, ok := store.expiration.idx[publicKey]; ok { 421 store.expiration.updateTrack(publicKey) 422 // this write is an update, move to back 423 it := v.Value.(*jwtItem) 424 oldHash := it.hash[:] 425 if bytes.Equal(oldHash, newHash[:]) { 426 return false, nil 427 } 428 } else if int64(store.expiration.Len()) >= store.expiration.limit { 429 if !store.expiration.evictOnLimit { 430 return false, errors.New("jwt store is full") 431 } 432 // this write is an add, pick the least recently used value for removal 433 i := store.expiration.lru.Front().Value.(*jwtItem) 434 if err := os.Remove(store.pathForKey(i.publicKey)); err != nil { 435 return false, err 436 } else { 437 store.expiration.unTrack(i.publicKey) 438 } 439 } 440 } 441 if err := os.WriteFile(path, []byte(theJWT), defaultFilePerms); err != nil { 442 return false, err 443 } else if store.expiration != nil { 444 store.expiration.track(publicKey, newHash, theJWT) 445 } 446 return true, nil 447 } 448 449 func (store *DirJWTStore) delete(publicKey string) error { 450 if store.readonly { 451 return fmt.Errorf("store is read-only") 452 } else if store.deleteType == NoDelete { 453 return fmt.Errorf("store is not set up to for delete") 454 } 455 store.Lock() 456 defer store.Unlock() 457 name := store.pathForKey(publicKey) 458 if store.deleteType == RenameDeleted { 459 if err := os.Rename(name, name+".deleted"); err != nil { 460 if os.IsNotExist(err) { 461 return nil 462 } 463 return err 464 } 465 } else if err := os.Remove(name); err != nil { 466 if os.IsNotExist(err) { 467 return nil 468 } 469 return err 470 } 471 store.expiration.unTrack(publicKey) 472 store.deleted(publicKey) 473 return nil 474 } 475 476 // Save puts the JWT in a map by public key and performs update callbacks 477 // Assumes lock is NOT held 478 func (store *DirJWTStore) save(publicKey string, theJWT string) error { 479 if store.readonly { 480 return fmt.Errorf("store is read-only") 481 } 482 store.Lock() 483 path := store.pathForKey(publicKey) 484 if path == _EMPTY_ { 485 store.Unlock() 486 return fmt.Errorf("invalid public key") 487 } 488 dirPath := filepath.Dir(path) 489 if _, err := validateDirPath(dirPath); err != nil { 490 if err := os.MkdirAll(dirPath, defaultDirPerms); err != nil { 491 store.Unlock() 492 return err 493 } 494 } 495 changed, err := store.write(path, publicKey, theJWT) 496 cb := store.changed 497 store.Unlock() 498 if changed && cb != nil { 499 cb(publicKey) 500 } 501 return err 502 } 503 504 // Assumes the lock is NOT held, and only updates if the jwt is new, or the one on disk is older 505 // When changed, invokes jwt changed callback 506 func (store *DirJWTStore) saveIfNewer(publicKey string, theJWT string) error { 507 if store.readonly { 508 return fmt.Errorf("store is read-only") 509 } 510 path := store.pathForKey(publicKey) 511 if path == _EMPTY_ { 512 return fmt.Errorf("invalid public key") 513 } 514 dirPath := filepath.Dir(path) 515 if _, err := validateDirPath(dirPath); err != nil { 516 if err := os.MkdirAll(dirPath, defaultDirPerms); err != nil { 517 return err 518 } 519 } 520 if _, err := os.Stat(path); err == nil { 521 if newJWT, err := jwt.DecodeGeneric(theJWT); err != nil { 522 return err 523 } else if existing, err := os.ReadFile(path); err != nil { 524 return err 525 } else if existingJWT, err := jwt.DecodeGeneric(string(existing)); err != nil { 526 // skip if it can't be decoded 527 } else if existingJWT.ID == newJWT.ID { 528 return nil 529 } else if existingJWT.IssuedAt > newJWT.IssuedAt { 530 return nil 531 } else if newJWT.Subject != publicKey { 532 return fmt.Errorf("jwt subject nkey and provided nkey do not match") 533 } else if existingJWT.Subject != newJWT.Subject { 534 return fmt.Errorf("subject of existing and new jwt do not match") 535 } 536 } 537 store.Lock() 538 cb := store.changed 539 changed, err := store.write(path, publicKey, theJWT) 540 store.Unlock() 541 if err != nil { 542 return err 543 } else if changed && cb != nil { 544 cb(publicKey) 545 } 546 return nil 547 } 548 549 func xorAssign(lVal *[sha256.Size]byte, rVal [sha256.Size]byte) { 550 for i := range rVal { 551 (*lVal)[i] ^= rVal[i] 552 } 553 } 554 555 // returns a hash representing all indexed jwt 556 func (store *DirJWTStore) Hash() [sha256.Size]byte { 557 store.Lock() 558 defer store.Unlock() 559 if store.expiration == nil { 560 return [sha256.Size]byte{} 561 } else { 562 return store.expiration.hash 563 } 564 } 565 566 // An jwtItem is something managed by the priority queue 567 type jwtItem struct { 568 index int 569 publicKey string 570 expiration int64 // consists of unix time of expiration (ttl when set or jwt expiration) in seconds 571 hash [sha256.Size]byte 572 } 573 574 // A expirationTracker implements heap.Interface and holds Items. 575 type expirationTracker struct { 576 heap []*jwtItem // sorted by jwtItem.expiration 577 idx map[string]*list.Element 578 lru *list.List // keep which jwt are least used 579 limit int64 // limit how many jwt are being tracked 580 evictOnLimit bool // when limit is hit, error or evict using lru 581 ttl time.Duration 582 hash [sha256.Size]byte // xor of all jwtItem.hash in idx 583 quit chan struct{} 584 wg sync.WaitGroup 585 } 586 587 func (q *expirationTracker) Len() int { return len(q.heap) } 588 589 func (q *expirationTracker) Less(i, j int) bool { 590 pq := q.heap 591 return pq[i].expiration < pq[j].expiration 592 } 593 594 func (q *expirationTracker) Swap(i, j int) { 595 pq := q.heap 596 pq[i], pq[j] = pq[j], pq[i] 597 pq[i].index = i 598 pq[j].index = j 599 } 600 601 func (q *expirationTracker) Push(x any) { 602 n := len(q.heap) 603 item := x.(*jwtItem) 604 item.index = n 605 q.heap = append(q.heap, item) 606 q.idx[item.publicKey] = q.lru.PushBack(item) 607 } 608 609 func (q *expirationTracker) Pop() any { 610 old := q.heap 611 n := len(old) 612 item := old[n-1] 613 old[n-1] = nil // avoid memory leak 614 item.index = -1 615 q.heap = old[0 : n-1] 616 q.lru.Remove(q.idx[item.publicKey]) 617 delete(q.idx, item.publicKey) 618 return item 619 } 620 621 func (pq *expirationTracker) updateTrack(publicKey string) { 622 if e, ok := pq.idx[publicKey]; ok { 623 i := e.Value.(*jwtItem) 624 if pq.ttl != 0 { 625 // only update expiration when set 626 i.expiration = time.Now().Add(pq.ttl).UnixNano() 627 heap.Fix(pq, i.index) 628 } 629 if pq.evictOnLimit { 630 pq.lru.MoveToBack(e) 631 } 632 } 633 } 634 635 func (pq *expirationTracker) unTrack(publicKey string) { 636 if it, ok := pq.idx[publicKey]; ok { 637 xorAssign(&pq.hash, it.Value.(*jwtItem).hash) 638 heap.Remove(pq, it.Value.(*jwtItem).index) 639 delete(pq.idx, publicKey) 640 } 641 } 642 643 func (pq *expirationTracker) track(publicKey string, hash *[sha256.Size]byte, theJWT string) { 644 var exp int64 645 // prioritize ttl over expiration 646 if pq.ttl != 0 { 647 if pq.ttl == time.Duration(math.MaxInt64) { 648 exp = math.MaxInt64 649 } else { 650 exp = time.Now().Add(pq.ttl).UnixNano() 651 } 652 } else { 653 if g, err := jwt.DecodeGeneric(theJWT); err == nil { 654 exp = time.Unix(g.Expires, 0).UnixNano() 655 } 656 if exp == 0 { 657 exp = math.MaxInt64 // default to indefinite 658 } 659 } 660 if e, ok := pq.idx[publicKey]; ok { 661 i := e.Value.(*jwtItem) 662 xorAssign(&pq.hash, i.hash) // remove old hash 663 i.expiration = exp 664 i.hash = *hash 665 heap.Fix(pq, i.index) 666 } else { 667 heap.Push(pq, &jwtItem{-1, publicKey, exp, *hash}) 668 } 669 xorAssign(&pq.hash, *hash) // add in new hash 670 } 671 672 func (pq *expirationTracker) close() { 673 if pq == nil || pq.quit == nil { 674 return 675 } 676 close(pq.quit) 677 pq.quit = nil 678 } 679 680 func (store *DirJWTStore) startExpiring(reCheck time.Duration, limit int64, evictOnLimit bool, ttl time.Duration) { 681 store.Lock() 682 defer store.Unlock() 683 quit := make(chan struct{}) 684 pq := &expirationTracker{ 685 make([]*jwtItem, 0, 10), 686 make(map[string]*list.Element), 687 list.New(), 688 limit, 689 evictOnLimit, 690 ttl, 691 [sha256.Size]byte{}, 692 quit, 693 sync.WaitGroup{}, 694 } 695 store.expiration = pq 696 pq.wg.Add(1) 697 go func() { 698 t := time.NewTicker(reCheck) 699 defer t.Stop() 700 defer pq.wg.Done() 701 for { 702 now := time.Now().UnixNano() 703 store.Lock() 704 if pq.Len() > 0 { 705 if it := pq.heap[0]; it.expiration <= now { 706 path := store.pathForKey(it.publicKey) 707 if err := os.Remove(path); err == nil { 708 heap.Pop(pq) 709 pq.unTrack(it.publicKey) 710 xorAssign(&pq.hash, it.hash) 711 store.Unlock() 712 continue // we removed an entry, check next one right away 713 } 714 } 715 } 716 store.Unlock() 717 select { 718 case <-t.C: 719 case <-quit: 720 return 721 } 722 } 723 }() 724 }