mirror of
https://github.com/lightninglabs/aperture.git
synced 2025-12-17 00:54:20 +01:00
auth: LsatAuthenticator -> L402Authenticator sed -i 's/LsatAuthenticator/L402Authenticator/g' aperture.go auth/authenticator.go auth/authenticator_test.go rename package lsat to l402 git mv lsat/ l402 sed 's@aperture/lsat@aperture/l402@g' -i `git grep -l aperture/lsat` sed -i 's@package lsat@package l402@' `git grep -l 'package lsat'` sed -i 's@lsat\.@l402.@g' -i `git grep -l 'lsat\.'` sed 's@l402.Id@lsat.Id@' -i mint/mint_test.go replace lsat with l402 in the code sed 's@lsat@l402@' -i mint/mint_test.go sed 's@Lsat@L402@' -i l402/client_interceptor.go sed 's@lsatstore@l402store@' -i l402/store_test.go replace LSAT to L402 in comments sed '/\/\//s@LSAT@L402@g' -i `git grep -l '//.*LSAT'` replace LSAT -> L402 in the code, skip when a string starts with it sed 's@\([^"/]\)LSAT@\1L402@g' -i `git grep -l LSAT`
225 lines
6.6 KiB
Go
225 lines
6.6 KiB
Go
package l402
|
|
|
|
import (
|
|
"errors"
|
|
"fmt"
|
|
"os"
|
|
"path/filepath"
|
|
"strings"
|
|
)
|
|
|
|
var (
|
|
// ErrNoToken is the error returned when the store doesn't contain a
|
|
// token yet.
|
|
ErrNoToken = errors.New("no token in store")
|
|
|
|
// storeFileName is the name of the file where we store the final,
|
|
// valid, token to.
|
|
storeFileName = "l402.token"
|
|
|
|
// storeFileNamePending is the name of the file where we store a pending
|
|
// token until it was successfully paid for.
|
|
storeFileNamePending = "l402.token.pending"
|
|
|
|
// errNoReplace is the error that is returned if a new token is
|
|
// being written to a store that already contains a paid token.
|
|
errNoReplace = errors.New("won't replace existing paid token with " +
|
|
"new token. " + manualRetryHint)
|
|
)
|
|
|
|
// Store is an interface that allows users to store and retrieve an L402 token.
|
|
type Store interface {
|
|
// CurrentToken returns the token that is currently contained in the
|
|
// store or an error if there is none.
|
|
CurrentToken() (*Token, error)
|
|
|
|
// AllTokens returns all tokens that the store has knowledge of, even
|
|
// if they might be expired. The tokens are mapped by their identifying
|
|
// attribute like file name or storage key.
|
|
AllTokens() (map[string]*Token, error)
|
|
|
|
// StoreToken saves a token to the store. Old tokens should be kept for
|
|
// accounting purposes but marked as invalid somehow.
|
|
StoreToken(*Token) error
|
|
|
|
// RemovePendingToken removes a pending token from the store or returns
|
|
// ErrNoToken if there is no pending token.
|
|
RemovePendingToken() error
|
|
}
|
|
|
|
// FileStore is an implementation of the Store interface that files to save the
|
|
// serialized tokens. There is always just one current token that is either
|
|
// pending or fully paid.
|
|
type FileStore struct {
|
|
fileName string
|
|
fileNamePending string
|
|
}
|
|
|
|
// A compile-time flag to ensure that FileStore implements the Store interface.
|
|
var _ Store = (*FileStore)(nil)
|
|
|
|
// NewFileStore creates a new file based token store, creating its file in the
|
|
// provided directory. If the directory does not exist, it will be created.
|
|
func NewFileStore(storeDir string) (*FileStore, error) {
|
|
// If the target path for the token store doesn't exist, then we'll
|
|
// create it now before we proceed.
|
|
if !fileExists(storeDir) {
|
|
if err := os.MkdirAll(storeDir, 0700); err != nil {
|
|
return nil, err
|
|
}
|
|
}
|
|
|
|
return &FileStore{
|
|
fileName: filepath.Join(storeDir, storeFileName),
|
|
fileNamePending: filepath.Join(storeDir, storeFileNamePending),
|
|
}, nil
|
|
}
|
|
|
|
// CurrentToken returns the token that is currently contained in the store or an
|
|
// error if there is none.
|
|
//
|
|
// NOTE: This is part of the Store interface.
|
|
func (f *FileStore) CurrentToken() (*Token, error) {
|
|
// As this is only a wrapper for external users to make sure the store
|
|
// is locked, the actual implementation is in the non-exported method.
|
|
return f.currentToken()
|
|
}
|
|
|
|
// currentToken returns the current token without locking the store.
|
|
func (f *FileStore) currentToken() (*Token, error) {
|
|
switch {
|
|
case fileExists(f.fileName):
|
|
return readTokenFile(f.fileName)
|
|
|
|
case fileExists(f.fileNamePending):
|
|
return readTokenFile(f.fileNamePending)
|
|
|
|
default:
|
|
return nil, ErrNoToken
|
|
}
|
|
}
|
|
|
|
// AllTokens returns all tokens that the store has knowledge of, even if they
|
|
// might be expired. The tokens are mapped by their identifying attribute like
|
|
// file name or storage key.
|
|
//
|
|
// NOTE: This is part of the Store interface.
|
|
func (f *FileStore) AllTokens() (map[string]*Token, error) {
|
|
tokens := make(map[string]*Token)
|
|
|
|
// All tokens start with the same name so we can get them by the prefix.
|
|
// As the tokens don't expire yet, there currently can't be more than
|
|
// just one token, either pending or paid.
|
|
// TODO(guggero): Update comment once tokens expire and we keep backups.
|
|
tokenDir := filepath.Dir(f.fileName)
|
|
files, err := os.ReadDir(tokenDir)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
for _, file := range files {
|
|
name := file.Name()
|
|
if !strings.HasPrefix(name, storeFileName) {
|
|
continue
|
|
}
|
|
fileName := filepath.Join(tokenDir, name)
|
|
token, err := readTokenFile(fileName)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
tokens[fileName] = token
|
|
}
|
|
|
|
return tokens, nil
|
|
}
|
|
|
|
// StoreToken saves a token to the store, overwriting any old token if there is
|
|
// one.
|
|
//
|
|
// NOTE: This is part of the Store interface.
|
|
func (f *FileStore) StoreToken(newToken *Token) error {
|
|
// Serialize the token first, before we rename anything.
|
|
bytes, err := serializeToken(newToken)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
// We'll need to know if there is any other token already in place,
|
|
// either pending or not, that we need to delete or overwrite.
|
|
currentToken, err := f.currentToken()
|
|
|
|
switch {
|
|
// No token in the store yet, just write it to the corresponding file.
|
|
case err == ErrNoToken:
|
|
// What's the target file name we are going to write?
|
|
newFileName := f.fileName
|
|
if newToken.isPending() {
|
|
newFileName = f.fileNamePending
|
|
}
|
|
return os.WriteFile(newFileName, bytes, 0600)
|
|
|
|
// Fail on any other error.
|
|
case err != nil:
|
|
return err
|
|
|
|
// Replace a pending token with a paid one.
|
|
case currentToken.isPending() && !newToken.isPending():
|
|
// Make sure we replace the the same token, just with a
|
|
// different state.
|
|
if currentToken.PaymentHash != newToken.PaymentHash {
|
|
return fmt.Errorf("new paid token doesn't match " +
|
|
"existing pending token")
|
|
}
|
|
|
|
// Write the new token first, so we still have the pending
|
|
// around if something goes wrong.
|
|
err := os.WriteFile(f.fileName, bytes, 0600)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
// We were able to write the new token so removing the old one
|
|
// can be just best effort. By default, the valid one will be
|
|
// read by the store if both exist.
|
|
_ = os.Remove(f.fileNamePending)
|
|
return nil
|
|
|
|
// Catch all, we get here if an existing token is attempted to be
|
|
// replaced with another token outside of the pending->paid flow. The
|
|
// user should manually remove the token in that case.
|
|
// TODO(guggero): Once tokens expire, this logic has to be adapted
|
|
// accordingly.
|
|
default:
|
|
return errNoReplace
|
|
}
|
|
}
|
|
|
|
// RemovePendingToken removes a pending token from the store or returns
|
|
// ErrNoToken if there is no pending token.
|
|
func (f *FileStore) RemovePendingToken() error {
|
|
if !fileExists(f.fileNamePending) {
|
|
return ErrNoToken
|
|
}
|
|
|
|
return os.Remove(f.fileNamePending)
|
|
}
|
|
|
|
// readTokenFile reads a single token from a file and returns it deserialized.
|
|
func readTokenFile(tokenFile string) (*Token, error) {
|
|
bytes, err := os.ReadFile(tokenFile)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return deserializeToken(bytes)
|
|
}
|
|
|
|
// fileExists returns true if the file exists, and false otherwise.
|
|
func fileExists(path string) bool {
|
|
if _, err := os.Stat(path); err != nil {
|
|
if os.IsNotExist(err) {
|
|
return false
|
|
}
|
|
}
|
|
|
|
return true
|
|
}
|