mirror of
https://github.com/lightninglabs/lightning-terminal.git
synced 2026-08-13 12:33:36 +02:00
As the `MacaroonRootKeyID` field of the `AddActionReq` struct also contains the 4 bytes of the `MacaroonIdentifier`, we change all call sites to instead use the last 4 bytes of the `MacaroonRootKeyID` field. As the `MacaroonIdentifier` field therefore becomes redundant, we also remove it.
259 lines
6.8 KiB
Go
259 lines
6.8 KiB
Go
package firewall
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
"strings"
|
|
"sync"
|
|
|
|
"github.com/lightninglabs/lightning-terminal/firewalldb"
|
|
litmac "github.com/lightninglabs/lightning-terminal/macaroons"
|
|
mid "github.com/lightninglabs/lightning-terminal/rpcmiddleware"
|
|
"github.com/lightningnetwork/lnd/fn"
|
|
"github.com/lightningnetwork/lnd/lnrpc"
|
|
"github.com/lightningnetwork/lnd/macaroons"
|
|
)
|
|
|
|
const (
|
|
// RequestLoggerName is the name of the RequestLogger interceptor.
|
|
RequestLoggerName = "lit-macaroon-firewall-logger"
|
|
)
|
|
|
|
var (
|
|
// uriSkipList is a map of URIs that we don't want to log or persist
|
|
// actions for.
|
|
uriSkipList = map[string]bool{
|
|
// We skip the CheckMacaroonPermissions uri since this method is
|
|
// called each time a call to a non-Litd endpoint needs to be
|
|
// validated and persisting the macaroon each time bloats the
|
|
// DB.
|
|
"/lnrpc.Lightning/CheckMacaroonPermissions": true,
|
|
}
|
|
|
|
// A compile-time assertion that RuleEnforcer is a
|
|
// rpcmiddleware.RequestInterceptor.
|
|
_ mid.RequestInterceptor = (*RequestLogger)(nil)
|
|
)
|
|
|
|
type RequestLoggerLevel string
|
|
|
|
const (
|
|
RequestLoggerLevelInterceptor = "interceptor"
|
|
RequestLoggerLevelAll = "all"
|
|
RequestLoggerLevelFull = "full"
|
|
)
|
|
|
|
// RequestLogger is a RequestInterceptor that just logs incoming RPC requests.
|
|
type RequestLogger struct {
|
|
actionsDB firewalldb.ActionsWriteDB
|
|
|
|
shouldLogAction func(ri *RequestInfo) (bool, bool)
|
|
|
|
// reqIDToAction is a map from request ID to an ActionLocator that can
|
|
// be used to find the corresponding action. This is used so that
|
|
// requests and responses can be easily linked. The mu mutex must be
|
|
// used when accessing this map.
|
|
reqIDToAction map[uint64]firewalldb.ActionLocator
|
|
mu sync.Mutex
|
|
}
|
|
|
|
// NewRequestLogger creates a new RequestLogger.
|
|
func NewRequestLogger(cfg *RequestLoggerConfig,
|
|
actionsDB firewalldb.ActionsWriteDB) (*RequestLogger, error) {
|
|
|
|
hasInterceptorCaveat := func(caveats []string) bool {
|
|
for _, c := range caveats {
|
|
if strings.HasPrefix(c, macaroons.CondLndCustom) {
|
|
return true
|
|
}
|
|
}
|
|
|
|
return false
|
|
}
|
|
|
|
var shouldLogAction func(ri *RequestInfo) (bool, bool)
|
|
switch cfg.RequestLoggerLevel {
|
|
// Only log requests that have an interceptor caveat attached.
|
|
case RequestLoggerLevelInterceptor:
|
|
shouldLogAction = func(ri *RequestInfo) (bool, bool) {
|
|
if hasInterceptorCaveat(ri.Caveats) {
|
|
return true, true
|
|
}
|
|
|
|
return false, false
|
|
}
|
|
|
|
// Log all requests but only log request params if the request
|
|
// has an interceptor caveat.
|
|
case RequestLoggerLevelAll:
|
|
shouldLogAction = func(ri *RequestInfo) (bool, bool) {
|
|
return true, hasInterceptorCaveat(ri.Caveats)
|
|
}
|
|
|
|
// Log all requests will all request parameters.
|
|
case RequestLoggerLevelFull:
|
|
shouldLogAction = func(ri *RequestInfo) (bool, bool) {
|
|
return true, true
|
|
}
|
|
|
|
default:
|
|
return nil, fmt.Errorf("unknown request logger level: %s. "+
|
|
"Expected either 'interceptor', 'all' or 'full'",
|
|
cfg.RequestLoggerLevel)
|
|
}
|
|
|
|
return &RequestLogger{
|
|
shouldLogAction: shouldLogAction,
|
|
actionsDB: actionsDB,
|
|
reqIDToAction: make(map[uint64]firewalldb.ActionLocator),
|
|
}, nil
|
|
}
|
|
|
|
// Name returns the name of the interceptor.
|
|
func (r *RequestLogger) Name() string {
|
|
return RequestLoggerName
|
|
}
|
|
|
|
// 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 (r *RequestLogger) ReadOnly() bool {
|
|
return true
|
|
}
|
|
|
|
// 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 (r *RequestLogger) CustomCaveatName() string {
|
|
return ""
|
|
}
|
|
|
|
// Intercept processes an RPC middleware interception request and returns the
|
|
// interception result which either accepts or rejects the intercepted message.
|
|
func (r *RequestLogger) Intercept(ctx context.Context,
|
|
req *lnrpc.RPCMiddlewareRequest) (*lnrpc.RPCMiddlewareResponse, error) {
|
|
|
|
ri, err := NewInfoFromRequest(req)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("error parsing incoming RPC middleware "+
|
|
"interception request: %v", err)
|
|
}
|
|
|
|
// If this request is for any URI in the uriSkipList map, then we do not
|
|
// log or persist it.
|
|
if uriSkipList[ri.URI] {
|
|
return mid.RPCOk(req)
|
|
}
|
|
|
|
shouldLogAction, withPayloadData := r.shouldLogAction(ri)
|
|
if !shouldLogAction {
|
|
return mid.RPCOk(req)
|
|
}
|
|
|
|
log.Tracef("RequestLogger: Intercepting %v", ri)
|
|
|
|
switch ri.MWRequestType {
|
|
case MWRequestTypeStreamAuth:
|
|
return mid.RPCOk(req)
|
|
|
|
// Parse incoming requests and act on them.
|
|
case MWRequestTypeRequest:
|
|
return mid.RPCErr(req, r.addNewAction(ctx, ri, withPayloadData))
|
|
|
|
// Parse and possibly manipulate outgoing responses.
|
|
case MWRequestTypeResponse:
|
|
var (
|
|
state = firewalldb.ActionStateDone
|
|
errReason string
|
|
)
|
|
if ri.IsError {
|
|
state = firewalldb.ActionStateError
|
|
errReason = mid.ParseResponseErr(ri.Serialized).Error()
|
|
}
|
|
|
|
return mid.RPCErr(
|
|
req, r.MarkAction(ctx, ri.RequestID, state, errReason),
|
|
)
|
|
|
|
default:
|
|
return mid.RPCErrString(req, "invalid intercept type: %v", r)
|
|
}
|
|
}
|
|
|
|
// addNewAction persists the new action to the db.
|
|
func (r *RequestLogger) addNewAction(ctx context.Context, ri *RequestInfo,
|
|
withPayloadData bool) error {
|
|
|
|
var (
|
|
rootKeyID fn.Option[uint64]
|
|
)
|
|
|
|
if ri.Macaroon != nil {
|
|
var err error
|
|
|
|
fullRootKeyID, err := litmac.RootKeyIDFromMacaroon(
|
|
ri.Macaroon,
|
|
)
|
|
if err != nil {
|
|
return fmt.Errorf("could not extract root key ID from "+
|
|
"macaroon: %w", err)
|
|
}
|
|
rootKeyID = fn.Some(fullRootKeyID)
|
|
}
|
|
|
|
actionReq := &firewalldb.AddActionReq{
|
|
SessionID: ri.SessionID,
|
|
AccountID: ri.AccountID,
|
|
MacaroonRootKeyID: rootKeyID,
|
|
RPCMethod: ri.URI,
|
|
}
|
|
|
|
if withPayloadData {
|
|
msg, err := mid.ParseProtobuf(ri.GRPCMessageType, ri.Serialized)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
jsonBytes, err := lnrpc.ProtoJSONMarshalOpts.Marshal(msg)
|
|
if err != nil {
|
|
return fmt.Errorf("unable to decode response: %v", err)
|
|
}
|
|
|
|
actionReq.RPCParamsJson = jsonBytes
|
|
|
|
meta := ri.MetaInfo
|
|
if meta != nil {
|
|
actionReq.ActorName = meta.ActorName
|
|
actionReq.FeatureName = meta.Feature
|
|
actionReq.Trigger = meta.Trigger
|
|
actionReq.Intent = meta.Intent
|
|
actionReq.StructuredJsonData = meta.StructuredJsonData
|
|
}
|
|
}
|
|
|
|
locator, err := r.actionsDB.AddAction(ctx, actionReq)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
r.mu.Lock()
|
|
r.reqIDToAction[ri.RequestID] = locator
|
|
r.mu.Unlock()
|
|
|
|
return nil
|
|
}
|
|
|
|
// MarkAction can be used to set the state of an action identified by the given
|
|
// requestID.
|
|
func (r *RequestLogger) MarkAction(ctx context.Context, reqID uint64,
|
|
state firewalldb.ActionState, errReason string) error {
|
|
|
|
r.mu.Lock()
|
|
defer r.mu.Unlock()
|
|
|
|
actionLocator, ok := r.reqIDToAction[reqID]
|
|
if !ok {
|
|
return nil
|
|
}
|
|
delete(r.reqIDToAction, reqID)
|
|
|
|
return r.actionsDB.SetActionState(ctx, actionLocator, state, errReason)
|
|
}
|