get.pme.sh/pnats@v0.0.0-20240304004023-26bb5a137ed0/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 interface{}
   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 interface{}) {
   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() interface{} {
   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  }