diff --git a/lsat/client_interceptor.go b/interceptor/client_interceptor.go similarity index 95% rename from lsat/client_interceptor.go rename to interceptor/client_interceptor.go index b6a277b..e4ba1df 100644 --- a/lsat/client_interceptor.go +++ b/interceptor/client_interceptor.go @@ -1,4 +1,4 @@ -package lsat +package interceptor import ( "context" @@ -11,6 +11,7 @@ import ( "time" "github.com/btcsuite/btcd/btcutil" + "github.com/lightninglabs/aperture/lsat" "github.com/lightninglabs/lndclient" "github.com/lightningnetwork/lnd/lnrpc" "github.com/lightningnetwork/lnd/lnwire" @@ -77,7 +78,7 @@ var ( // connection to lnd to automatically pay for an authentication token. type ClientInterceptor struct { lnd *lndclient.LndServices - store Store + store lsat.Store callTimeout time.Duration maxCost btcutil.Amount maxFee btcutil.Amount @@ -85,10 +86,10 @@ type ClientInterceptor struct { allowInsecure bool } -// NewInterceptor creates a new gRPC client interceptor that uses the provided -// lnd connection to automatically acquire and pay for LSAT tokens, unless the -// indicated store already contains a usable token. -func NewInterceptor(lnd *lndclient.LndServices, store Store, +// NewClientInterceptor creates a new gRPC client interceptor that uses the +// provided lnd connection to automatically acquire and pay for LSAT tokens, +// unless the indicated store already contains a usable token. +func NewClientInterceptor(lnd *lndclient.LndServices, store lsat.Store, rpcCallTimeout time.Duration, maxCost, maxFee btcutil.Amount, allowInsecure bool) *ClientInterceptor { @@ -108,7 +109,7 @@ type interceptContext struct { mainCtx context.Context opts []grpc.CallOption metadata *metadata.MD - token *Token + token *lsat.Token } // UnaryInterceptor is an interceptor method that can be used directly by gRPC @@ -223,7 +224,7 @@ func (i *ClientInterceptor) newInterceptContext(ctx context.Context, iCtx.token, err = i.store.CurrentToken() switch { // If there is no token yet, nothing to do at this point. - case err == ErrNoToken: + case err == lsat.ErrNoToken: // Some other error happened that we have to surface. case err != nil: @@ -235,7 +236,7 @@ func (i *ClientInterceptor) newInterceptContext(ctx context.Context, // payment just yet, since we don't even know if a token is required for // this call. We also never send a pending payment to the server since // we know it's not valid. - case !iCtx.token.isPending(): + case !iCtx.token.IsPending(): if err = i.addLsatCredentials(iCtx); err != nil { log.Errorf("Adding macaroon to request failed: %v", err) return nil, fmt.Errorf("adding macaroon failed: %v", @@ -257,7 +258,7 @@ func (i *ClientInterceptor) newInterceptContext(ctx context.Context, func (i *ClientInterceptor) handlePayment(iCtx *interceptContext) error { switch { // Resume/track a pending payment if it was interrupted for some reason. - case iCtx.token != nil && iCtx.token.isPending(): + case iCtx.token != nil && iCtx.token.IsPending(): log.Infof("Payment of LSAT token is required, resuming/" + "tracking previous payment from pending LSAT token") err := i.trackPayment(iCtx.mainCtx, iCtx.token) @@ -321,7 +322,7 @@ func (i *ClientInterceptor) addLsatCredentials(iCtx *interceptContext) error { return err } iCtx.opts = append(iCtx.opts, grpc.PerRPCCredentials( - NewMacaroonCredential(macaroon, i.allowInsecure), + lsat.NewMacaroonCredential(macaroon, i.allowInsecure), )) return nil } @@ -330,7 +331,7 @@ func (i *ClientInterceptor) addLsatCredentials(iCtx *interceptContext) error { // to pay the invoice encoded in them, returning a paid LSAT token if // successful. func (i *ClientInterceptor) payLsatToken(ctx context.Context, md *metadata.MD) ( - *Token, error) { + *lsat.Token, error) { // First parse the authentication header that was stored in the // metadata. @@ -367,7 +368,7 @@ func (i *ClientInterceptor) payLsatToken(ctx context.Context, md *metadata.MD) ( // Create and store the pending token so we can resume the payment in // case the payment is interrupted somehow. - token, err := tokenFromChallenge(macBytes, invoice.PaymentHash) + token, err := lsat.TokenFromChallenge(macBytes, invoice.PaymentHash) if err != nil { return nil, fmt.Errorf("unable to create token: %v", err) } @@ -407,7 +408,9 @@ func (i *ClientInterceptor) payLsatToken(ctx context.Context, md *metadata.MD) ( // trackPayment tries to resume a pending payment by tracking its state and // waiting for a conclusive result. -func (i *ClientInterceptor) trackPayment(ctx context.Context, token *Token) error { +func (i *ClientInterceptor) trackPayment(ctx context.Context, + token *lsat.Token) error { + // Lookup state of the payment. paymentStateCtx, cancel := context.WithCancel(ctx) defer cancel() @@ -486,7 +489,7 @@ func IsPaymentRequired(err error) bool { // extractPaymentDetails extracts the preimage and amounts paid for a payment // from the payment status and stores them in the token. -func extractPaymentDetails(token *Token, status lndclient.PaymentStatus) { +func extractPaymentDetails(token *lsat.Token, status lndclient.PaymentStatus) { token.Preimage = status.Preimage token.AmountPaid = status.Value token.RoutingFeePaid = status.Fee diff --git a/lsat/client_interceptor_test.go b/interceptor/client_interceptor_test.go similarity index 92% rename from lsat/client_interceptor_test.go rename to interceptor/client_interceptor_test.go index 624ed97..4e9f1a4 100644 --- a/lsat/client_interceptor_test.go +++ b/interceptor/client_interceptor_test.go @@ -1,4 +1,4 @@ -package lsat +package interceptor import ( "context" @@ -10,6 +10,7 @@ import ( "time" "github.com/lightninglabs/aperture/internal/test" + "github.com/lightninglabs/aperture/lsat" "github.com/lightninglabs/lndclient" "github.com/lightningnetwork/lnd/lnrpc" "github.com/lightningnetwork/lnd/lntypes" @@ -36,21 +37,21 @@ type interceptTestCase struct { } type mockStore struct { - token *Token + token *lsat.Token } -func (s *mockStore) CurrentToken() (*Token, error) { +func (s *mockStore) CurrentToken() (*lsat.Token, error) { if s.token == nil { - return nil, ErrNoToken + return nil, lsat.ErrNoToken } return s.token, nil } -func (s *mockStore) AllTokens() (map[string]*Token, error) { - return map[string]*Token{"foo": s.token}, nil +func (s *mockStore) AllTokens() (map[string]*lsat.Token, error) { + return map[string]*lsat.Token{"foo": s.token}, nil } -func (s *mockStore) StoreToken(token *Token) error { +func (s *mockStore) StoreToken(token *lsat.Token) error { s.token = token return nil } @@ -64,11 +65,11 @@ var ( lnd = test.NewMockLnd() store = &mockStore{} testTimeout = 5 * time.Second - interceptor = NewInterceptor( + interceptor = NewClientInterceptor( &lnd.LndServices, store, testTimeout, DefaultMaxCostSats, DefaultMaxRoutingFeeSats, false, ) - testMac = makeMac() + testMac = lsat.MakeMac() testMacBytes = serializeMac(testMac) testMacHex = hex.EncodeToString(testMacBytes) paidPreimage = lntypes.Preimage{1, 2, 3, 4, 5} @@ -135,7 +136,7 @@ var ( expectMacaroonCall2: false, }, { name: "auth required, has pending token", - initialPreimage: &zeroPreimage, + initialPreimage: &lsat.ZeroPreimage, interceptor: interceptor, resetCb: func() { resetBackend( @@ -166,7 +167,7 @@ var ( expectMacaroonCall2: true, }, { name: "auth required, has pending but expired token", - initialPreimage: &zeroPreimage, + initialPreimage: &lsat.ZeroPreimage, interceptor: interceptor, resetCb: func() { resetBackend( @@ -207,7 +208,7 @@ var ( }, { name: "auth required, no token yet, cost limit", initialPreimage: nil, - interceptor: NewInterceptor( + interceptor: NewClientInterceptor( &lnd.LndServices, store, testTimeout, 100, DefaultMaxRoutingFeeSats, false, ), @@ -393,7 +394,7 @@ func testInterceptor(t *testing.T, tc interceptTestCase, require.NoError(t, err) require.Equal(t, paidPreimage, storeToken.Preimage) } else { - require.Equal(t, ErrNoToken, err) + require.Equal(t, lsat.ErrNoToken, err) } if tc.expectMacaroonCall2 { require.Len(t, callMD, 1) @@ -405,27 +406,16 @@ func testInterceptor(t *testing.T, tc interceptTestCase, require.Equal(t, tc.expectBackendCalls, numBackendCalls) } -func makeToken(preimage *lntypes.Preimage) *Token { +func makeToken(preimage *lntypes.Preimage) *lsat.Token { if preimage == nil { return nil } - return &Token{ + return &lsat.Token{ Preimage: *preimage, - baseMac: testMac, + BaseMac: testMac, } } -func makeMac() *macaroon.Macaroon { - dummyMac, err := macaroon.New( - []byte("aabbccddeeff00112233445566778899"), []byte("AA=="), - "LSAT", macaroon.LatestVersion, - ) - if err != nil { - panic(fmt.Errorf("unable to create macaroon: %v", err)) - } - return dummyMac -} - func serializeMac(mac *macaroon.Macaroon) []byte { macBytes, err := mac.MarshalBinary() if err != nil { diff --git a/interceptor/log.go b/interceptor/log.go new file mode 100644 index 0000000..7aa32f6 --- /dev/null +++ b/interceptor/log.go @@ -0,0 +1,26 @@ +package interceptor + +import ( + "github.com/btcsuite/btclog" + "github.com/lightningnetwork/lnd/build" +) + +// Subsystem defines the sub system name of this package. +const Subsystem = "INPR" + +// log is a logger that is initialized with no output filters. This +// means the package will not perform any logging by default until the caller +// requests it. +var log btclog.Logger + +// The default amount of logging is none. +func init() { + UseLogger(build.NewSubLogger(Subsystem, nil)) +} + +// UseLogger uses a specified Logger to output package logging info. +// This should be used in preference to SetLogWriter if the caller is also +// using btclog. +func UseLogger(logger btclog.Logger) { + log = logger +} diff --git a/log.go b/log.go index 6111bb6..1688d89 100644 --- a/log.go +++ b/log.go @@ -4,6 +4,7 @@ import ( "github.com/btcsuite/btclog" "github.com/lightninglabs/aperture/auth" "github.com/lightninglabs/aperture/challenger" + "github.com/lightninglabs/aperture/interceptor" "github.com/lightninglabs/aperture/lsat" "github.com/lightninglabs/aperture/proxy" "github.com/lightninglabs/lndclient" @@ -31,6 +32,9 @@ func SetupLoggers(root *build.RotatingLogWriter, intercept signal.Interceptor) { lnd.AddSubLogger( root, challenger.Subsystem, intercept, challenger.UseLogger, ) + lnd.AddSubLogger( + root, interceptor.Subsystem, intercept, interceptor.UseLogger, + ) lnd.AddSubLogger(root, lsat.Subsystem, intercept, lsat.UseLogger) lnd.AddSubLogger(root, proxy.Subsystem, intercept, proxy.UseLogger) lnd.AddSubLogger(root, "LNDC", intercept, lndclient.UseLogger) diff --git a/lsat/store.go b/lsat/store.go index 736e19f..b4eaafe 100644 --- a/lsat/store.go +++ b/lsat/store.go @@ -24,7 +24,12 @@ var ( // 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) + "new token. " + ManualRetryHint) + + // ManualRetryHint is the error text we return to tell the user how a + // token payment can be retried if the payment fails. + ManualRetryHint = "consider removing pending token file if error " + + "persists. use 'listauth' command to find out token file name" ) // Store is an interface that allows users to store and retrieve an LSAT token. @@ -152,7 +157,7 @@ func (f *FileStore) StoreToken(newToken *Token) error { case err == ErrNoToken: // What's the target file name we are going to write? newFileName := f.fileName - if newToken.isPending() { + if newToken.IsPending() { newFileName = f.fileNamePending } return os.WriteFile(newFileName, bytes, 0600) @@ -162,7 +167,7 @@ func (f *FileStore) StoreToken(newToken *Token) error { return err // Replace a pending token with a paid one. - case currentToken.isPending() && !newToken.isPending(): + case currentToken.IsPending() && !newToken.IsPending(): // Make sure we replace the the same token, just with a // different state. if currentToken.PaymentHash != newToken.PaymentHash { diff --git a/lsat/store_test.go b/lsat/store_test.go index 0c80a3c..403b7fc 100644 --- a/lsat/store_test.go +++ b/lsat/store_test.go @@ -22,11 +22,11 @@ func TestFileStore(t *testing.T) { paidPreimage = lntypes.Preimage{1, 2, 3, 4, 5} paidToken = &Token{ Preimage: paidPreimage, - baseMac: makeMac(), + BaseMac: MakeMac(), } pendingToken = &Token{ - Preimage: zeroPreimage, - baseMac: makeMac(), + Preimage: ZeroPreimage, + BaseMac: MakeMac(), } ) @@ -61,7 +61,7 @@ func TestFileStore(t *testing.T) { if err != nil { t.Fatalf("could not read pending token: %v", err) } - if !token.baseMac.Equal(pendingToken.baseMac) { + if !token.BaseMac.Equal(pendingToken.BaseMac) { t.Fatalf("expected macaroon to match") } tokens, err = store.AllTokens() @@ -73,7 +73,7 @@ func TestFileStore(t *testing.T) { len(tokens), 1) } for key := range tokens { - if !tokens[key].baseMac.Equal(pendingToken.baseMac) { + if !tokens[key].BaseMac.Equal(pendingToken.BaseMac) { t.Fatalf("expected macaroon to match") } } @@ -96,7 +96,7 @@ func TestFileStore(t *testing.T) { if err != nil { t.Fatalf("could not read pending token: %v", err) } - if !token.baseMac.Equal(paidToken.baseMac) { + if !token.BaseMac.Equal(paidToken.BaseMac) { t.Fatalf("expected macaroon to match") } tokens, err = store.AllTokens() @@ -108,7 +108,7 @@ func TestFileStore(t *testing.T) { len(tokens), 1) } for key := range tokens { - if !tokens[key].baseMac.Equal(paidToken.baseMac) { + if !tokens[key].BaseMac.Equal(paidToken.BaseMac) { t.Fatalf("expected macaroon to match") } } diff --git a/lsat/test_utils.go b/lsat/test_utils.go new file mode 100644 index 0000000..cba947d --- /dev/null +++ b/lsat/test_utils.go @@ -0,0 +1,18 @@ +package lsat + +import ( + "fmt" + + "gopkg.in/macaroon.v2" +) + +func MakeMac() *macaroon.Macaroon { + dummyMac, err := macaroon.New( + []byte("aabbccddeeff00112233445566778899"), []byte("AA=="), + "LSAT", macaroon.LatestVersion, + ) + if err != nil { + panic(fmt.Errorf("unable to create macaroon: %v", err)) + } + return dummyMac +} diff --git a/lsat/token.go b/lsat/token.go index 1be010e..40f8d83 100644 --- a/lsat/token.go +++ b/lsat/token.go @@ -12,9 +12,9 @@ import ( ) var ( - // zeroPreimage is an empty, invalid payment preimage that is used to + // ZeroPreimage is an empty, invalid payment preimage that is used to // initialize pending tokens with. - zeroPreimage lntypes.Preimage + ZeroPreimage lntypes.Preimage ) // Token is the main type to store an LSAT token in. @@ -40,15 +40,15 @@ type Token struct { // TimeCreated is the moment when this token was created. TimeCreated time.Time - // baseMac is the base macaroon in its original form as baked by the + // BaseMac is the base macaroon in its original form as baked by the // authentication server. No client side caveats have been added to it // yet. - baseMac *macaroon.Macaroon + BaseMac *macaroon.Macaroon } -// tokenFromChallenge parses the parts that are present in the challenge part +// TokenFromChallenge parses the parts that are present in the challenge part // of the LSAT auth protocol which is the macaroon and the payment hash. -func tokenFromChallenge(baseMac []byte, paymentHash *[32]byte) (*Token, error) { +func TokenFromChallenge(baseMac []byte, paymentHash *[32]byte) (*Token, error) { // First, validate that the macaroon is valid and can be unmarshaled. mac := &macaroon.Macaroon{} err := mac.UnmarshalBinary(baseMac) @@ -58,8 +58,8 @@ func tokenFromChallenge(baseMac []byte, paymentHash *[32]byte) (*Token, error) { token := &Token{ TimeCreated: time.Now(), - baseMac: mac, - Preimage: zeroPreimage, + BaseMac: mac, + Preimage: ZeroPreimage, } hash, err := lntypes.MakeHash(paymentHash[:]) if err != nil { @@ -72,7 +72,7 @@ func tokenFromChallenge(baseMac []byte, paymentHash *[32]byte) (*Token, error) { // BaseMacaroon returns the base macaroon as received from the authentication // server. func (t *Token) BaseMacaroon() *macaroon.Macaroon { - return t.baseMac.Clone() + return t.BaseMac.Clone() } // PaidMacaroon returns the base macaroon with the proof of payment (preimage) @@ -96,17 +96,17 @@ func (t *Token) IsValid() bool { return true } -// isPending returns true if the payment for the LSAT is still in flight and we +// IsPending returns true if the payment for the LSAT is still in flight and we // haven't received the preimage yet. -func (t *Token) isPending() bool { - return t.Preimage == zeroPreimage +func (t *Token) IsPending() bool { + return t.Preimage == ZeroPreimage } // serializeToken returns a byte-serialized representation of the token. func serializeToken(t *Token) ([]byte, error) { var b bytes.Buffer - baseMacBytes, err := t.baseMac.MarshalBinary() + baseMacBytes, err := t.BaseMac.MarshalBinary() if err != nil { return nil, err } @@ -163,7 +163,7 @@ func deserializeToken(value []byte) (*Token, error) { return nil, err } - token, err := tokenFromChallenge(macBytes, &paymentHash) + token, err := TokenFromChallenge(macBytes, &paymentHash) if err != nil { return nil, err } diff --git a/proxy/proxy_test.go b/proxy/proxy_test.go index 1e0e439..3225a5d 100644 --- a/proxy/proxy_test.go +++ b/proxy/proxy_test.go @@ -15,7 +15,7 @@ import ( "time" "github.com/lightninglabs/aperture/auth" - "github.com/lightninglabs/aperture/lsat" + "github.com/lightninglabs/aperture/interceptor" "github.com/lightninglabs/aperture/proxy" proxytest "github.com/lightninglabs/aperture/proxy/testdata" "github.com/lightningnetwork/lnd/cert" @@ -309,7 +309,7 @@ func runGRPCTest(t *testing.T, tc *testCase) { grpc.Trailer(&captureMetadata), ) require.Error(t, err) - require.True(t, lsat.IsPaymentRequired(err)) + require.True(t, interceptor.IsPaymentRequired(err)) // We expect the WWW-Authenticate header field to be set to an LSAT // auth response.