|
| 1 | +package rules |
| 2 | + |
| 3 | +import ( |
| 4 | + "context" |
| 5 | + "fmt" |
| 6 | + "time" |
| 7 | + |
| 8 | + "github.com/lightninglabs/lightning-terminal/firewalldb" |
| 9 | + "github.com/lightninglabs/lightning-terminal/litrpc" |
| 10 | + "google.golang.org/protobuf/proto" |
| 11 | + "gopkg.in/macaroon-bakery.v2/bakery" |
| 12 | +) |
| 13 | + |
| 14 | +var ( |
| 15 | + // Compile-time checks to ensure that RateLimit, RateLimitMgr |
| 16 | + // and RateLimitEnforcer implement the appropriate Manager, Enforcer |
| 17 | + // and Values interface. |
| 18 | + _ Manager = (*RateLimitMgr)(nil) |
| 19 | + _ Enforcer = (*RateLimitEnforcer)(nil) |
| 20 | + _ Values = (*RateLimit)(nil) |
| 21 | +) |
| 22 | + |
| 23 | +// RateLimitName is the string identifier of the RateLimitMgr values. |
| 24 | +const RateLimitName = "rate-limit" |
| 25 | + |
| 26 | +// RateLimitMgr represents the rate limit values. |
| 27 | +type RateLimitMgr struct{} |
| 28 | + |
| 29 | +// Stop cleans up the resources held by the manager. |
| 30 | +// |
| 31 | +// NOTE: This is part of the Manager interface. |
| 32 | +func (r *RateLimitMgr) Stop() error { |
| 33 | + return nil |
| 34 | +} |
| 35 | + |
| 36 | +// NewEnforcer constructs a new RateLimit rule enforcer using the passed values |
| 37 | +// and config. |
| 38 | +// |
| 39 | +// NOTE: This is part of the Manager interface. |
| 40 | +func (r *RateLimitMgr) NewEnforcer(cfg Config, values Values) (Enforcer, |
| 41 | + error) { |
| 42 | + |
| 43 | + limits, ok := values.(*RateLimit) |
| 44 | + if !ok { |
| 45 | + return nil, fmt.Errorf("values must be of type "+ |
| 46 | + "RateLimit, got %T", values) |
| 47 | + } |
| 48 | + |
| 49 | + return &RateLimitEnforcer{ |
| 50 | + rateLimitConfig: cfg, |
| 51 | + RateLimit: limits, |
| 52 | + }, nil |
| 53 | +} |
| 54 | + |
| 55 | +// NewValueFromProto converts the given proto value into a RateLimit Value |
| 56 | +// object. |
| 57 | +// |
| 58 | +// NOTE: This is part of the Manager interface. |
| 59 | +func (r *RateLimitMgr) NewValueFromProto(v *litrpc.RuleValue) (Values, error) { |
| 60 | + rv, ok := v.Value.(*litrpc.RuleValue_RateLimit) |
| 61 | + if !ok { |
| 62 | + return nil, fmt.Errorf("incorrect RuleValue type") |
| 63 | + } |
| 64 | + |
| 65 | + budget := rv.RateLimit |
| 66 | + readLim := budget.ReadLimit |
| 67 | + writeLim := budget.WriteLimit |
| 68 | + |
| 69 | + return &RateLimit{ |
| 70 | + ReadLimit: &Rate{ |
| 71 | + Iterations: readLim.Iterations, |
| 72 | + NumHours: readLim.NumHours, |
| 73 | + }, |
| 74 | + WriteLimit: &Rate{ |
| 75 | + Iterations: writeLim.Iterations, |
| 76 | + NumHours: writeLim.NumHours, |
| 77 | + }, |
| 78 | + }, nil |
| 79 | +} |
| 80 | + |
| 81 | +// EmptyValue returns a new RateLimit instance. |
| 82 | +func (r *RateLimitMgr) EmptyValue() Values { |
| 83 | + return &RateLimit{} |
| 84 | +} |
| 85 | + |
| 86 | +// rateLimitConfig is the config required by RateLimitMgr. It can be derived |
| 87 | +// from the main rules Config struct. |
| 88 | +type rateLimitConfig interface { |
| 89 | + GetActionsDB() firewalldb.ActionsDB |
| 90 | + GetMethodPerms() func(string) ([]bakery.Op, bool) |
| 91 | +} |
| 92 | + |
| 93 | +// RateLimitEnforcer enforces requests and responses against a RateLimit rule. |
| 94 | +type RateLimitEnforcer struct { |
| 95 | + rateLimitConfig |
| 96 | + *RateLimit |
| 97 | +} |
| 98 | + |
| 99 | +// HandleResponse handles and possible alters a response. This is a noop for the |
| 100 | +// RateLimitMgr values. |
| 101 | +// |
| 102 | +// NOTE: this is part of the Rule interface. |
| 103 | +func (r *RateLimitEnforcer) HandleResponse(_ context.Context, _ string, |
| 104 | + _ proto.Message) (proto.Message, error) { |
| 105 | + |
| 106 | + return nil, nil |
| 107 | +} |
| 108 | + |
| 109 | +// HandleRequest checks the validity of a request. It checks if the request is a |
| 110 | +// read or a write request. Then, using the past actions DB, it determines if |
| 111 | +// letting this request through would violate the rate limit rules. |
| 112 | +// |
| 113 | +// NOTE: this is part of the Rule interface. |
| 114 | +func (r *RateLimitEnforcer) HandleRequest(ctx context.Context, uri string, |
| 115 | + _ proto.Message) (proto.Message, error) { |
| 116 | + |
| 117 | + // First, we need to classify if this is a read or write call. |
| 118 | + read := r.isRead(uri) |
| 119 | + |
| 120 | + // Based on the above, we can extract the relevant rate limit values |
| 121 | + // that apply for this call. |
| 122 | + rateLim := r.WriteLimit |
| 123 | + if read { |
| 124 | + rateLim = r.ReadLimit |
| 125 | + } |
| 126 | + |
| 127 | + // Now we need to go and count all the previous read or write actions. |
| 128 | + actions, err := r.GetActionsDB().ListActions(ctx) |
| 129 | + if err != nil { |
| 130 | + return nil, err |
| 131 | + } |
| 132 | + |
| 133 | + // Determine the start time of the actions window. |
| 134 | + startTime := time.Now().Add( |
| 135 | + -time.Duration(rateLim.NumHours) * time.Hour, |
| 136 | + ) |
| 137 | + |
| 138 | + // Now count all relevant actions which have taken place after the |
| 139 | + // start time. |
| 140 | + var count uint32 |
| 141 | + for _, action := range actions { |
| 142 | + if read != r.isRead(action.Method) { |
| 143 | + continue |
| 144 | + } |
| 145 | + |
| 146 | + if action.PerformedAt.Before(startTime) { |
| 147 | + continue |
| 148 | + } |
| 149 | + |
| 150 | + count++ |
| 151 | + } |
| 152 | + |
| 153 | + if count >= rateLim.Iterations { |
| 154 | + return nil, fmt.Errorf("too many requests received") |
| 155 | + } |
| 156 | + |
| 157 | + return nil, nil |
| 158 | +} |
| 159 | + |
| 160 | +// HandleErrorResponse handles and possible alters an error. This is a noop for |
| 161 | +// the RateLimitEnforcer rule. |
| 162 | +// |
| 163 | +// NOTE: this is part of the Enforcer interface. |
| 164 | +func (r *RateLimitEnforcer) HandleErrorResponse(_ context.Context, _ string, |
| 165 | + _ error) (error, error) { |
| 166 | + |
| 167 | + return nil, nil |
| 168 | +} |
| 169 | + |
| 170 | +// isRead is a helper that returns true if the given method/URI only requires |
| 171 | +// read-permissions and false otherwise. |
| 172 | +func (r *RateLimitEnforcer) isRead(method string) bool { |
| 173 | + perms, ok := r.GetMethodPerms()(method) |
| 174 | + if !ok { |
| 175 | + return false |
| 176 | + } |
| 177 | + |
| 178 | + for _, p := range perms { |
| 179 | + if p.Action != "read" { |
| 180 | + return false |
| 181 | + } |
| 182 | + } |
| 183 | + return true |
| 184 | +} |
| 185 | + |
| 186 | +// Rate describes a rate limit in iterations per number of hours. |
| 187 | +type Rate struct { |
| 188 | + Iterations uint32 `json:"iterations"` |
| 189 | + NumHours uint32 `json:"num_hours"` |
| 190 | +} |
| 191 | + |
| 192 | +// RateLimit represents the rules values. |
| 193 | +type RateLimit struct { |
| 194 | + WriteLimit *Rate `json:"write_limit"` |
| 195 | + ReadLimit *Rate `json:"read_limit"` |
| 196 | +} |
| 197 | + |
| 198 | +// VerifySane checks that the value of the values is ok given the min and max |
| 199 | +// allowed values. |
| 200 | +// |
| 201 | +// NOTE: this is part of the Values interface. |
| 202 | +func (r *RateLimit) VerifySane(minVal, maxVal Values) error { |
| 203 | + minRL, ok := minVal.(*RateLimit) |
| 204 | + if !ok { |
| 205 | + return fmt.Errorf("min value is not of type RateLimit") |
| 206 | + } |
| 207 | + |
| 208 | + maxRL, ok := maxVal.(*RateLimit) |
| 209 | + if !ok { |
| 210 | + return fmt.Errorf("max value is not of type RateLimit") |
| 211 | + } |
| 212 | + |
| 213 | + // Check that our read limit is between the min and max. |
| 214 | + if r.ReadLimit.lessThan(minRL.ReadLimit) || |
| 215 | + maxRL.ReadLimit.lessThan(r.ReadLimit) { |
| 216 | + |
| 217 | + return fmt.Errorf("read limit is not between the min and max") |
| 218 | + } |
| 219 | + |
| 220 | + // Check that our write limit is between the min and max. |
| 221 | + if r.WriteLimit.lessThan(minRL.WriteLimit) || |
| 222 | + maxRL.WriteLimit.lessThan(r.WriteLimit) { |
| 223 | + |
| 224 | + return fmt.Errorf("write limit is not between the min and max") |
| 225 | + } |
| 226 | + |
| 227 | + return nil |
| 228 | +} |
| 229 | + |
| 230 | +// lessThan is a helper function that checks if the current rate is less than |
| 231 | +// another rate. |
| 232 | +func (r *Rate) lessThan(other *Rate) bool { |
| 233 | + return float64(r.Iterations)/float64(r.NumHours) < |
| 234 | + float64(other.Iterations)/float64(other.NumHours) |
| 235 | +} |
| 236 | + |
| 237 | +// RuleName returns the name of the rule that these values are to be used with. |
| 238 | +// |
| 239 | +// NOTE: this is part of the Values interface. |
| 240 | +func (r *RateLimit) RuleName() string { |
| 241 | + return RateLimitName |
| 242 | +} |
| 243 | + |
| 244 | +// ToProto converts the rule Values to the litrpc counterpart. |
| 245 | +// |
| 246 | +// NOTE: this is part of the Values interface. |
| 247 | +func (r *RateLimit) ToProto() *litrpc.RuleValue { |
| 248 | + return &litrpc.RuleValue{ |
| 249 | + Value: &litrpc.RuleValue_RateLimit{ |
| 250 | + RateLimit: &litrpc.RateLimit{ |
| 251 | + ReadLimit: &litrpc.Rate{ |
| 252 | + Iterations: r.ReadLimit.Iterations, |
| 253 | + NumHours: r.ReadLimit.NumHours, |
| 254 | + }, |
| 255 | + WriteLimit: &litrpc.Rate{ |
| 256 | + Iterations: r.WriteLimit.Iterations, |
| 257 | + NumHours: r.WriteLimit.NumHours, |
| 258 | + }, |
| 259 | + }, |
| 260 | + }, |
| 261 | + } |
| 262 | +} |
| 263 | + |
| 264 | +// PseudoToReal attempts to convert any appropriate pseudo fields in the rule |
| 265 | +// Values to their corresponding real values. It uses the passed PrivacyMapDB to |
| 266 | +// find the real values. This is a no-op for the RateLimit rule. |
| 267 | +// |
| 268 | +// NOTE: this is part of the Values interface. |
| 269 | +func (r *RateLimit) PseudoToReal(_ firewalldb.PrivacyMapDB) (Values, |
| 270 | + error) { |
| 271 | + |
| 272 | + return r, nil |
| 273 | +} |
| 274 | + |
| 275 | +// RealToPseudo converts the rule Values to a new one that uses pseudo keys, |
| 276 | +// channel IDs, channel points etc. It returns a map of real to pseudo strings |
| 277 | +// that should be persisted. This is a no-op for the RateLimit rule. |
| 278 | +// |
| 279 | +// NOTE: this is part of the Values interface. |
| 280 | +func (r *RateLimit) RealToPseudo() (Values, map[string]string, error) { |
| 281 | + return r, nil, nil |
| 282 | +} |
0 commit comments