mirror of
https://github.com/lightninglabs/lightning-terminal.git
synced 2026-08-13 12:33:36 +02:00
255 lines
7.4 KiB
Go
255 lines
7.4 KiB
Go
package accounts
|
|
|
|
import (
|
|
"context"
|
|
"encoding/hex"
|
|
"errors"
|
|
"fmt"
|
|
"strings"
|
|
|
|
mid "github.com/lightninglabs/lightning-terminal/rpcmiddleware"
|
|
"github.com/lightningnetwork/lnd/fn"
|
|
"github.com/lightningnetwork/lnd/lnrpc"
|
|
"github.com/lightningnetwork/lnd/macaroons"
|
|
"google.golang.org/protobuf/proto"
|
|
"gopkg.in/macaroon-bakery.v2/bakery/checkers"
|
|
"gopkg.in/macaroon.v2"
|
|
)
|
|
|
|
const (
|
|
// CondAccount is the custom caveat condition that binds a macaroon to a
|
|
// certain account.
|
|
CondAccount = "account"
|
|
|
|
// accountMiddlewareName is the name that is used for the account system
|
|
// when registering it to lnd as an RPC middleware.
|
|
accountMiddlewareName = "lit-account"
|
|
)
|
|
|
|
var (
|
|
// caveatPrefix is the prefix that is used for custom caveats that are
|
|
// used by the account system. This prefix is used to identify the
|
|
// custom caveat and extract the condition (the AccountID) from it.
|
|
caveatPrefix = []byte(fmt.Sprintf(
|
|
"%s %s ", macaroons.CondLndCustom, CondAccount,
|
|
))
|
|
)
|
|
|
|
// Name returns the name of the interceptor.
|
|
func (s *InterceptorService) Name() string {
|
|
return accountMiddlewareName
|
|
}
|
|
|
|
// ReadOnly returns true if this interceptor should be registered in read-only
|
|
// mode. In read-only mode no custom caveat name can be specified.
|
|
func (s *InterceptorService) ReadOnly() bool {
|
|
return false
|
|
}
|
|
|
|
// CustomCaveatName returns the name of the custom caveat that is expected to be
|
|
// handled by this interceptor. Cannot be specified in read-only mode.
|
|
func (s *InterceptorService) CustomCaveatName() string {
|
|
return CondAccount
|
|
}
|
|
|
|
// Intercept processes an RPC middleware interception request and returns the
|
|
// interception result which either accepts or rejects the intercepted message.
|
|
func (s *InterceptorService) Intercept(ctx context.Context,
|
|
req *lnrpc.RPCMiddlewareRequest) (*lnrpc.RPCMiddlewareResponse, error) {
|
|
|
|
// We only allow a single request or response to be handled at the same
|
|
// time. This should already be serialized by the RPC stream itself, but
|
|
// with the lock we prevent a new request to be handled before we finish
|
|
// handling the previous one.
|
|
s.requestMtx.Lock()
|
|
defer s.requestMtx.Unlock()
|
|
|
|
// If the account service is not running, we reject all requests.
|
|
// Note that this is by no means a guarantee that the account service
|
|
// will be running throughout processing the request, but at least we
|
|
// can stop requests early if the service was already disabled when the
|
|
// request came in.
|
|
if !s.IsRunning() {
|
|
return mid.RPCErrString(
|
|
req, "the account service has been stopped",
|
|
)
|
|
}
|
|
|
|
mac := &macaroon.Macaroon{}
|
|
err := mac.UnmarshalBinary(req.RawMacaroon)
|
|
if err != nil {
|
|
return mid.RPCErrString(req, "error parsing macaroon: %v", err)
|
|
}
|
|
|
|
acctID, err := IDFromCaveats(mac.Caveats())
|
|
if err != nil {
|
|
return mid.RPCErrString(
|
|
req, "error parsing account from macaroon: %v", err,
|
|
)
|
|
}
|
|
|
|
// No account lock in the macaroon, something's weird. The interceptor
|
|
// wouldn't have been triggered if there was no caveat, so we do expect
|
|
// a macaroon here.
|
|
accountID, err := acctID.UnwrapOrErr(
|
|
fmt.Errorf("expected account ID in macaroon caveat"),
|
|
)
|
|
if err != nil {
|
|
return mid.RPCErr(req, err)
|
|
}
|
|
|
|
acct, err := s.Account(ctx, accountID)
|
|
if err != nil {
|
|
return mid.RPCErrString(
|
|
req, "error getting account %x: %v", accountID[:], err,
|
|
)
|
|
}
|
|
|
|
log.Debugf("Account auth intercepted, ID=%x, balance_sat=%d, "+
|
|
"expired=%v", acct.ID[:], acct.CurrentBalanceSats(),
|
|
acct.HasExpired())
|
|
|
|
if acct.HasExpired() {
|
|
return mid.RPCErrString(
|
|
req, "account %x has expired", acct.ID[:],
|
|
)
|
|
}
|
|
|
|
// We now add the account and request ID to the incoming context to give
|
|
// each checker access to them if required.
|
|
ctx = AddAccountToContext(ctx, acct)
|
|
ctx = AddRequestIDToContext(ctx, req.RequestId)
|
|
|
|
switch r := req.InterceptType.(type) {
|
|
// In the authentication phase we just check that the account hasn't
|
|
// expired yet (which we already did). This is only be used for
|
|
// establishing streams, so we don't see a request yet.
|
|
case *lnrpc.RPCMiddlewareRequest_StreamAuth:
|
|
return mid.RPCOk(req)
|
|
|
|
// Parse incoming requests and act on them.
|
|
case *lnrpc.RPCMiddlewareRequest_Request:
|
|
msg, err := parseRPCMessage(r.Request)
|
|
if err != nil {
|
|
return mid.RPCErr(req, err)
|
|
}
|
|
|
|
return mid.RPCErr(req, s.checkers.checkIncomingRequest(
|
|
ctx, r.Request.MethodFullUri, msg,
|
|
))
|
|
|
|
// Parse and possibly manipulate outgoing responses.
|
|
case *lnrpc.RPCMiddlewareRequest_Response:
|
|
if r.Response.IsError {
|
|
parsedErr := mid.ParseResponseErr(r.Response.Serialized)
|
|
|
|
replacementErr, err := s.checkers.handleErrorResponse(
|
|
ctx, r.Response.MethodFullUri, parsedErr,
|
|
)
|
|
if err != nil {
|
|
return mid.RPCErr(req, err)
|
|
}
|
|
|
|
// No error occurred but the response error should be
|
|
// replaced with the given custom error. Wrap it in the
|
|
// correct RPC response of the interceptor now.
|
|
if replacementErr != nil {
|
|
return mid.RPCErrReplacement(
|
|
req, replacementErr,
|
|
)
|
|
}
|
|
|
|
// No error and no replacement, just return an empty
|
|
// response of the correct type.
|
|
return mid.RPCOk(req)
|
|
}
|
|
|
|
msg, err := parseRPCMessage(r.Response)
|
|
if err != nil {
|
|
return mid.RPCErr(req, err)
|
|
}
|
|
|
|
replacement, err := s.checkers.replaceOutgoingResponse(
|
|
ctx, r.Response.MethodFullUri, msg,
|
|
)
|
|
if err != nil {
|
|
return mid.RPCErr(req, err)
|
|
}
|
|
|
|
// No error occurred but the response should be replaced with
|
|
// the given custom response. Wrap it in the correct RPC
|
|
// response of the interceptor now.
|
|
if replacement != nil {
|
|
return mid.RPCReplacement(req, replacement)
|
|
}
|
|
|
|
// No error and no replacement, just return an empty response of
|
|
// the correct type.
|
|
return mid.RPCOk(req)
|
|
|
|
default:
|
|
return mid.RPCErrString(req, "invalid intercept type: %v", r)
|
|
}
|
|
}
|
|
|
|
// parseRPCMessage parses a raw RPC message into the original protobuf message
|
|
// type.
|
|
func parseRPCMessage(msg *lnrpc.RPCMessage) (proto.Message, error) {
|
|
// Are we intercepting an error message being returned?
|
|
if msg.TypeName == "error" {
|
|
return nil, errors.New(string(msg.Serialized))
|
|
}
|
|
|
|
// No, it's a normal message.
|
|
parsedMsg, err := mid.ParseProtobuf(msg.TypeName, msg.Serialized)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("error parsing proto of type %v: %w",
|
|
msg.TypeName, err)
|
|
}
|
|
|
|
return parsedMsg, nil
|
|
}
|
|
|
|
// CaveatFromID creates a custom caveat that can be used to bind a macaroon to
|
|
// a certain account.
|
|
func CaveatFromID(id AccountID) macaroon.Caveat {
|
|
condition := checkers.Condition(macaroons.CondLndCustom, fmt.Sprintf(
|
|
"%s %x", CondAccount, id[:],
|
|
))
|
|
|
|
return macaroon.Caveat{Id: []byte(condition)}
|
|
}
|
|
|
|
// IDFromCaveats attempts to extract an AccountID from the given set of caveats
|
|
// by looking for the custom caveat that binds a macaroon to a certain account.
|
|
func IDFromCaveats(caveats []macaroon.Caveat) (fn.Option[AccountID], error) {
|
|
var accountIDStr string
|
|
for _, caveat := range caveats {
|
|
// The caveat id has a format of
|
|
// "lnd-custom [custom-caveat-name] [custom-caveat-condition]"
|
|
// and we only want the condition part. If we match the prefix
|
|
// part we return the condition that comes after the prefix.
|
|
_, after, found := strings.Cut(
|
|
string(caveat.Id), string(caveatPrefix),
|
|
)
|
|
if !found {
|
|
continue
|
|
}
|
|
|
|
accountIDStr = after
|
|
}
|
|
|
|
if accountIDStr == "" {
|
|
return fn.None[AccountID](), nil
|
|
}
|
|
|
|
var accountID AccountID
|
|
accountIDBytes, err := hex.DecodeString(accountIDStr)
|
|
if err != nil {
|
|
return fn.None[AccountID](), err
|
|
}
|
|
|
|
copy(accountID[:], accountIDBytes)
|
|
|
|
return fn.Some(accountID), nil
|
|
}
|