mirror of
https://github.com/lightninglabs/lightning-terminal.git
synced 2026-08-13 12:33:36 +02:00
Update the PrivacyMapDB interface methods to take contexts (both the methods themselves and the call-back params) and then ensure all implementations are updated and all call-sites pass contexts through correctly.
286 lines
7.6 KiB
Go
286 lines
7.6 KiB
Go
package rules
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
"time"
|
|
|
|
"github.com/lightninglabs/lightning-terminal/firewalldb"
|
|
"github.com/lightninglabs/lightning-terminal/litrpc"
|
|
"github.com/lightninglabs/lightning-terminal/session"
|
|
"google.golang.org/protobuf/proto"
|
|
"gopkg.in/macaroon-bakery.v2/bakery"
|
|
)
|
|
|
|
var (
|
|
// Compile-time checks to ensure that RateLimit, RateLimitMgr
|
|
// and RateLimitEnforcer implement the appropriate Manager, Enforcer
|
|
// and Values interface.
|
|
_ Manager = (*RateLimitMgr)(nil)
|
|
_ Enforcer = (*RateLimitEnforcer)(nil)
|
|
_ Values = (*RateLimit)(nil)
|
|
)
|
|
|
|
// RateLimitName is the string identifier of the RateLimitMgr values.
|
|
const RateLimitName = "rate-limit"
|
|
|
|
// RateLimitMgr represents the rate limit values.
|
|
type RateLimitMgr struct{}
|
|
|
|
// Stop cleans up the resources held by the manager.
|
|
//
|
|
// NOTE: This is part of the Manager interface.
|
|
func (r *RateLimitMgr) Stop() error {
|
|
return nil
|
|
}
|
|
|
|
// NewEnforcer constructs a new RateLimit rule enforcer using the passed values
|
|
// and config.
|
|
//
|
|
// NOTE: This is part of the Manager interface.
|
|
func (r *RateLimitMgr) NewEnforcer(_ context.Context, cfg Config,
|
|
values Values) (Enforcer, error) {
|
|
|
|
limits, ok := values.(*RateLimit)
|
|
if !ok {
|
|
return nil, fmt.Errorf("values must be of type "+
|
|
"RateLimit, got %T", values)
|
|
}
|
|
|
|
return &RateLimitEnforcer{
|
|
rateLimitConfig: cfg,
|
|
RateLimit: limits,
|
|
}, nil
|
|
}
|
|
|
|
// NewValueFromProto converts the given proto value into a RateLimit Value
|
|
// object.
|
|
//
|
|
// NOTE: This is part of the Manager interface.
|
|
func (r *RateLimitMgr) NewValueFromProto(v *litrpc.RuleValue) (Values, error) {
|
|
rv, ok := v.Value.(*litrpc.RuleValue_RateLimit)
|
|
if !ok {
|
|
return nil, fmt.Errorf("incorrect RuleValue type")
|
|
}
|
|
|
|
budget := rv.RateLimit
|
|
readLim := budget.ReadLimit
|
|
writeLim := budget.WriteLimit
|
|
|
|
return &RateLimit{
|
|
ReadLimit: &Rate{
|
|
Iterations: readLim.Iterations,
|
|
NumHours: readLim.NumHours,
|
|
},
|
|
WriteLimit: &Rate{
|
|
Iterations: writeLim.Iterations,
|
|
NumHours: writeLim.NumHours,
|
|
},
|
|
}, nil
|
|
}
|
|
|
|
// EmptyValue returns a new RateLimit instance.
|
|
func (r *RateLimitMgr) EmptyValue() Values {
|
|
return &RateLimit{}
|
|
}
|
|
|
|
// rateLimitConfig is the config required by RateLimitMgr. It can be derived
|
|
// from the main rules Config struct.
|
|
type rateLimitConfig interface {
|
|
GetActionsDB() firewalldb.ActionsDB
|
|
GetMethodPerms() func(string) ([]bakery.Op, bool)
|
|
}
|
|
|
|
// RateLimitEnforcer enforces requests and responses against a RateLimit rule.
|
|
type RateLimitEnforcer struct {
|
|
rateLimitConfig
|
|
*RateLimit
|
|
}
|
|
|
|
// HandleResponse handles and possible alters a response. This is a noop for the
|
|
// RateLimitMgr values.
|
|
//
|
|
// NOTE: this is part of the Rule interface.
|
|
func (r *RateLimitEnforcer) HandleResponse(_ context.Context, _ string,
|
|
_ proto.Message) (proto.Message, error) {
|
|
|
|
return nil, nil
|
|
}
|
|
|
|
// HandleRequest checks the validity of a request. It checks if the request is a
|
|
// read or a write request. Then, using the past actions DB, it determines if
|
|
// letting this request through would violate the rate limit rules.
|
|
//
|
|
// NOTE: this is part of the Rule interface.
|
|
func (r *RateLimitEnforcer) HandleRequest(ctx context.Context, uri string,
|
|
_ proto.Message) (proto.Message, error) {
|
|
|
|
// First, we need to classify if this is a read or write call.
|
|
read := r.isRead(uri)
|
|
|
|
// Based on the above, we can extract the relevant rate limit values
|
|
// that apply for this call.
|
|
rateLim := r.WriteLimit
|
|
if read {
|
|
rateLim = r.ReadLimit
|
|
}
|
|
|
|
// Now we need to go and count all the previous read or write actions.
|
|
actions, err := r.GetActionsDB().ListActions(ctx)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
// Determine the start time of the actions window.
|
|
startTime := time.Now().Add(
|
|
-time.Duration(rateLim.NumHours) * time.Hour,
|
|
)
|
|
|
|
// Now count all relevant actions which have taken place after the
|
|
// start time.
|
|
var count uint32
|
|
for _, action := range actions {
|
|
if read != r.isRead(action.Method) {
|
|
continue
|
|
}
|
|
|
|
if action.PerformedAt.Before(startTime) {
|
|
continue
|
|
}
|
|
|
|
count++
|
|
}
|
|
|
|
if count >= rateLim.Iterations {
|
|
return nil, fmt.Errorf("too many requests received")
|
|
}
|
|
|
|
return nil, nil
|
|
}
|
|
|
|
// HandleErrorResponse handles and possible alters an error. This is a noop for
|
|
// the RateLimitEnforcer rule.
|
|
//
|
|
// NOTE: this is part of the Enforcer interface.
|
|
func (r *RateLimitEnforcer) HandleErrorResponse(_ context.Context, _ string,
|
|
_ error) (error, error) {
|
|
|
|
return nil, nil
|
|
}
|
|
|
|
// isRead is a helper that returns true if the given method/URI only requires
|
|
// read-permissions and false otherwise.
|
|
func (r *RateLimitEnforcer) isRead(method string) bool {
|
|
perms, ok := r.GetMethodPerms()(method)
|
|
if !ok {
|
|
return false
|
|
}
|
|
|
|
for _, p := range perms {
|
|
if p.Action != "read" {
|
|
return false
|
|
}
|
|
}
|
|
return true
|
|
}
|
|
|
|
// Rate describes a rate limit in iterations per number of hours.
|
|
type Rate struct {
|
|
Iterations uint32 `json:"iterations"`
|
|
NumHours uint32 `json:"num_hours"`
|
|
}
|
|
|
|
// RateLimit represents the rules values.
|
|
type RateLimit struct {
|
|
WriteLimit *Rate `json:"write_limit"`
|
|
ReadLimit *Rate `json:"read_limit"`
|
|
}
|
|
|
|
// VerifySane checks that the value of the values is ok given the min and max
|
|
// allowed values.
|
|
//
|
|
// NOTE: this is part of the Values interface.
|
|
func (r *RateLimit) VerifySane(minVal, maxVal Values) error {
|
|
minRL, ok := minVal.(*RateLimit)
|
|
if !ok {
|
|
return fmt.Errorf("min value is not of type RateLimit")
|
|
}
|
|
|
|
maxRL, ok := maxVal.(*RateLimit)
|
|
if !ok {
|
|
return fmt.Errorf("max value is not of type RateLimit")
|
|
}
|
|
|
|
// Check that our read limit is between the min and max.
|
|
if r.ReadLimit.lessThan(minRL.ReadLimit) ||
|
|
maxRL.ReadLimit.lessThan(r.ReadLimit) {
|
|
|
|
return fmt.Errorf("read limit is not between the min and max")
|
|
}
|
|
|
|
// Check that our write limit is between the min and max.
|
|
if r.WriteLimit.lessThan(minRL.WriteLimit) ||
|
|
maxRL.WriteLimit.lessThan(r.WriteLimit) {
|
|
|
|
return fmt.Errorf("write limit is not between the min and max")
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// lessThan is a helper function that checks if the current rate is less than
|
|
// another rate.
|
|
func (r *Rate) lessThan(other *Rate) bool {
|
|
return float64(r.Iterations)/float64(r.NumHours) <
|
|
float64(other.Iterations)/float64(other.NumHours)
|
|
}
|
|
|
|
// RuleName returns the name of the rule that these values are to be used with.
|
|
//
|
|
// NOTE: this is part of the Values interface.
|
|
func (r *RateLimit) RuleName() string {
|
|
return RateLimitName
|
|
}
|
|
|
|
// ToProto converts the rule Values to the litrpc counterpart.
|
|
//
|
|
// NOTE: this is part of the Values interface.
|
|
func (r *RateLimit) ToProto() *litrpc.RuleValue {
|
|
return &litrpc.RuleValue{
|
|
Value: &litrpc.RuleValue_RateLimit{
|
|
RateLimit: &litrpc.RateLimit{
|
|
ReadLimit: &litrpc.Rate{
|
|
Iterations: r.ReadLimit.Iterations,
|
|
NumHours: r.ReadLimit.NumHours,
|
|
},
|
|
WriteLimit: &litrpc.Rate{
|
|
Iterations: r.WriteLimit.Iterations,
|
|
NumHours: r.WriteLimit.NumHours,
|
|
},
|
|
},
|
|
},
|
|
}
|
|
}
|
|
|
|
// PseudoToReal attempts to convert any appropriate pseudo fields in the rule
|
|
// Values to their corresponding real values. It uses the passed PrivacyMapDB to
|
|
// find the real values. This is a no-op for the RateLimit rule.
|
|
//
|
|
// NOTE: this is part of the Values interface.
|
|
func (r *RateLimit) PseudoToReal(_ context.Context,
|
|
_ firewalldb.PrivacyMapDB, _ session.PrivacyFlags) (Values, error) {
|
|
|
|
return r, nil
|
|
}
|
|
|
|
// RealToPseudo converts the rule Values to a new one that uses pseudo keys,
|
|
// channel IDs, channel points etc. It returns a map of real to pseudo strings
|
|
// that should be persisted. This is a no-op for the RateLimit rule.
|
|
//
|
|
// NOTE: this is part of the Values interface.
|
|
func (r *RateLimit) RealToPseudo(_ context.Context,
|
|
_ firewalldb.PrivacyMapReader, flags session.PrivacyFlags) (Values,
|
|
map[string]string, error) {
|
|
|
|
return r, nil, nil
|
|
}
|