Skip to content

Commit

Permalink
Add support for Fastly Rate Limiting (#60)
Browse files Browse the repository at this point in the history
* add penaltybox declaration

* remove newline

* Add penaltybox parser

* fix typos

* add new keywords

* remove example

* add to plugin

* add new rules

* Add penatlybox and ratecounter to parser

* Add support to lint penaltybox and ratecounter

* Add built in functions for ratelimit

* lint empty penaltyboxes and ratecounters blocks

* test linting penaltybox and ratecounter

* fix an error message

* match the nonempty block errors

Co-authored-by: shadi-altarsha <shadi.altarsha@reddit.com>
  • Loading branch information
shadialtarsha and shadi-altarsha authored May 3, 2022
1 parent 4746ad8 commit e85cced
Show file tree
Hide file tree
Showing 20 changed files with 554 additions and 51 deletions.
33 changes: 33 additions & 0 deletions __generator__/builtin.yml
Original file line number Diff line number Diff line change
Expand Up @@ -1217,3 +1217,36 @@ uuid.x500:
on: [RECV, HASH, HIT, MISS, PASS, FETCH, ERROR, DELIVER, LOG]
return: STRING

ratelimit.check_rate:
reference: "https://developer.fastly.com/reference/vcl/functions/rate-limiting/ratelimit-check-rate/"
on: [RECV, HASH, HIT, MISS, PASS, FETCH, ERROR, DELIVER, LOG]
arguments:
- [STRING, ID, INTEGER, INTEGER, INTEGER, ID, TIME]
return: BOOL

ratelimit.check_rates:
reference: "https://developer.fastly.com/reference/vcl/functions/rate-limiting/ratelimit-check-rates/"
on: [RECV, HASH, HIT, MISS, PASS, FETCH, ERROR, DELIVER, LOG]
arguments:
- [STRING, ID, INTEGER, INTEGER, INTEGER, ID, INTEGER, INTEGER, INTEGER, ID, TIME]
return: BOOL

ratelimit.penaltybox_add:
reference: "https://developer.fastly.com/reference/vcl/functions/rate-limiting/ratelimit-penaltybox-add/"
on: [RECV, HASH, HIT, MISS, PASS, FETCH, ERROR, DELIVER, LOG]
arguments:
- [ID, STRING, RTIME]

ratelimit.penaltybox_has:
reference: "https://developer.fastly.com/reference/vcl/functions/rate-limiting/ratelimit-penaltybox-has/"
on: [RECV, HASH, HIT, MISS, PASS, FETCH, ERROR, DELIVER, LOG]
arguments:
- [ID, STRING]
return: BOOL

ratelimit.ratecounter_increment:
reference: "https://developer.fastly.com/reference/vcl/functions/rate-limiting/ratelimit-ratecounter-increment/"
on: [RECV, HASH, HIT, MISS, PASS, FETCH, ERROR, DELIVER, LOG]
arguments:
- [ID, STRING, INTEGER]
return: INTEGER
24 changes: 24 additions & 0 deletions ast/penaltybox_declaration.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
package ast

import "bytes"

type PenaltyboxDeclaration struct {
*Meta
Name *Ident
Block *BlockStatement
}

func (p *PenaltyboxDeclaration) statement() {}
func (p *PenaltyboxDeclaration) GetMeta() *Meta { return p.Meta }
func (p *PenaltyboxDeclaration) String() string {
var buf bytes.Buffer

buf.WriteString(p.LeadingComment())
buf.WriteString("penaltybox ")
buf.WriteString(p.Name.String())
buf.WriteString(" " + p.Block.String())
buf.WriteString(p.TrailingComment())
buf.WriteString("\n")

return buf.String()
}
27 changes: 27 additions & 0 deletions ast/penaltybox_declaration_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
package ast

import (
"testing"
)

func TestPenaltyboxStatement(t *testing.T) {
p := &PenaltyboxDeclaration{
Meta: New(T, 0, comments("// This is comment"), comments("/* This is comment */")),
Name: &Ident{
Meta: New(T, 0),
Value: "ip_pbox",
},
Block: &BlockStatement{
Meta: New(T, 0, comments("/* This is comment */")),
},
}

expect := `// This is comment
penaltybox ip_pbox {
} /* This is comment */
`

if p.String() != expect {
t.Errorf("stringer error.\nexpect:\n%s\nactual:\n%s\n", expect, p.String())
}
}
24 changes: 24 additions & 0 deletions ast/ratecounter_declaration.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
package ast

import "bytes"

type RatecounterDeclaration struct {
*Meta
Name *Ident
Block *BlockStatement
}

func (r *RatecounterDeclaration) statement() {}
func (r *RatecounterDeclaration) GetMeta() *Meta { return r.Meta }
func (r *RatecounterDeclaration) String() string {
var buf bytes.Buffer

buf.WriteString(r.LeadingComment())
buf.WriteString("ratecounter ")
buf.WriteString(r.Name.String())
buf.WriteString(" " + r.Block.String())
buf.WriteString(r.TrailingComment())
buf.WriteString("\n")

return buf.String()
}
27 changes: 27 additions & 0 deletions ast/ratecounter_declaration_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
package ast

import (
"testing"
)

func TestRatecounterStatement(t *testing.T) {
r := &RatecounterDeclaration{
Meta: New(T, 0, comments("// This is comment"), comments("/* This is comment */")),
Name: &Ident{
Meta: New(T, 0),
Value: "requests_rate",
},
Block: &BlockStatement{
Meta: New(T, 0, comments("/* This is comment */")),
},
}

expect := `// This is comment
ratecounter requests_rate {
} /* This is comment */
`

if r.String() != expect {
t.Errorf("stringer error.\nexpect:\n%s\nactual:\n%s\n", expect, r.String())
}
}
58 changes: 58 additions & 0 deletions context/builtin.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

52 changes: 38 additions & 14 deletions context/context.go
Original file line number Diff line number Diff line change
Expand Up @@ -87,6 +87,8 @@ type Context struct {
Tables map[string]*types.Table
Directors map[string]*types.Director
Subroutines map[string]*types.Subroutine
Penaltyboxes map[string]*types.Penaltybox
Ratecounters map[string]*types.Ratecounter
Identifiers map[string]struct{}
functions Functions
Variables Variables
Expand All @@ -101,6 +103,8 @@ func New() *Context {
Tables: make(map[string]*types.Table),
Directors: make(map[string]*types.Director),
Subroutines: make(map[string]*types.Subroutine),
Penaltyboxes: make(map[string]*types.Penaltybox),
Ratecounters: make(map[string]*types.Ratecounter),
Identifiers: builtinIdentifiers(),
functions: builtinFunctions(),
Variables: predefinedVariables(),
Expand Down Expand Up @@ -167,7 +171,7 @@ func (c *Context) PushRegexVariables(matchN int) error {

// If pushed count is greater than 1, variable is overridden in the subroutine statement
if c.RegexVariables["re.group.0"] > 1 {
return fmt.Errorf("Regex captured variable may override older one.")
return fmt.Errorf("regex captured variable may override older one")
}
return nil
}
Expand All @@ -184,7 +188,7 @@ func (c *Context) GetRegexGroupVariable(name string) (types.Type, error) {
func (c *Context) AddAcl(name string, acl *types.Acl) error {
// check existence
if _, duplicated := c.Acls[name]; duplicated {
return fmt.Errorf(`duplicate deifnition of acl "%s"`, name)
return fmt.Errorf(`duplicate definition of acl "%s"`, name)
}
c.Acls[name] = acl
return nil
Expand All @@ -193,11 +197,11 @@ func (c *Context) AddAcl(name string, acl *types.Acl) error {
func (c *Context) AddBackend(name string, backend *types.Backend) error {
// check existence
if _, duplicated := c.Backends[name]; duplicated {
return fmt.Errorf(`duplicate deifnition of backend "%s"`, name)
return fmt.Errorf(`duplicate definition of backend "%s"`, name)
}
c.Backends[name] = backend

// Additionally, assign some backend name related predefiend variable
// Additionally, assign some backend name related predefined variable
c.Variables["backend"].Items[name] = dynamicBackend()
c.Variables["ratecounter"] = &Object{
Items: map[string]*Object{
Expand All @@ -211,7 +215,7 @@ func (c *Context) AddBackend(name string, backend *types.Backend) error {
func (c *Context) AddTable(name string, table *types.Table) error {
// check existence
if _, duplicated := c.Tables[name]; duplicated {
return fmt.Errorf(`duplicate deifnition of table "%s"`, name)
return fmt.Errorf(`duplicate definition of table "%s"`, name)
}
c.Tables[name] = table
return nil
Expand All @@ -220,7 +224,7 @@ func (c *Context) AddTable(name string, table *types.Table) error {
func (c *Context) AddDirector(name string, director *types.Director) error {
// check existence
if _, duplicated := c.Directors[name]; duplicated {
return fmt.Errorf(`duplicate deifnition of director "%s"`, name)
return fmt.Errorf(`duplicate definition of director "%s"`, name)
}
c.Directors[name] = director

Expand All @@ -235,7 +239,7 @@ func (c *Context) AddDirector(name string, director *types.Director) error {
},
}

// And, director target backend identifilers should be marked as used
// And, director target backend identifiers should be marked as used
for _, d := range director.Decl.Properties {
bo, ok := d.(*ast.DirectorBackendObject)
if !ok {
Expand All @@ -260,14 +264,34 @@ func (c *Context) AddSubroutine(name string, subroutine *types.Subroutine) error
// check existence
if _, duplicated := c.Subroutines[name]; duplicated {
if !IsFastlySubroutine(name) {
return fmt.Errorf(`duplicate deifnition of subroutine "%s"`, name)
return fmt.Errorf(`duplicate definition of subroutine "%s"`, name)
}
} else {
c.Subroutines[name] = subroutine
}
return nil
}

func (c *Context) AddPenaltybox(name string, penaltybox *types.Penaltybox) error {
// check existence
if _, duplicated := c.Penaltyboxes[name]; duplicated {
return fmt.Errorf(`duplicate definition of penaltybox "%s"`, name)
} else {
c.Penaltyboxes[name] = penaltybox
}
return nil
}

func (c *Context) AddRatecounter(name string, ratecounter *types.Ratecounter) error {
// check existence
if _, duplicated := c.Ratecounters[name]; duplicated {
return fmt.Errorf(`duplicate definition of ratecounter "%s"`, name)
} else {
c.Ratecounters[name] = ratecounter
}
return nil
}

func (c *Context) Get(name string) (types.Type, error) {
first, remains := splitName(name)

Expand Down Expand Up @@ -305,7 +329,7 @@ func (c *Context) Get(name string) (types.Type, error) {
if obj == nil || obj.Value == nil {
return types.NullType, fmt.Errorf(`undefined variable "%s"`, name)
}
// Value exists, but uneable to access in current scope
// Value exists, but unable to access in current scope
if obj.Value.Scopes&c.curMode == 0 {
message := fmt.Sprintf(`variable "%s" could not access in scope of %s`, name, ScopeString(c.curMode))
if obj.Value.Reference != "" {
Expand Down Expand Up @@ -365,7 +389,7 @@ func (c *Context) Set(name string) (types.Type, error) {
if obj == nil || obj.Value == nil {
return types.NullType, fmt.Errorf(`undefined variable "%s"`, name)
}
// Value exists, but uneable to access in current scope
// Value exists, but unable to access in current scope
if obj.Value.Scopes&c.curMode == 0 {
message := fmt.Sprintf(`variable "%s" could not access in scope of %s`, name, ScopeString(c.curMode))
if obj.Value.Reference != "" {
Expand Down Expand Up @@ -467,7 +491,7 @@ func (c *Context) Unset(name string) error {
if obj == nil || obj.Value == nil {
return nil
}
// Value exists, but uneable to access in current scope
// Value exists, but unable to access in current scope
if obj.Value.Scopes&c.curMode == 0 {
message := fmt.Sprintf(`variable "%s" could not access in scope of %s`, name, ScopeString(c.curMode))
if obj.Value.Reference != "" {
Expand Down Expand Up @@ -495,12 +519,12 @@ func (c *Context) GetFunction(name string) (*BuiltinFunction, error) {

obj, ok := c.functions[first]
if !ok {
return nil, fmt.Errorf(`Function "%s" is not defined`, name)
return nil, fmt.Errorf(`function "%s" is not defined`, name)
}

for _, key := range remains {
if v, ok := obj.Items[key]; !ok {
return nil, fmt.Errorf(`Function "%s" is not defined`, name)
return nil, fmt.Errorf(`function "%s" is not defined`, name)
} else {
obj = v
}
Expand All @@ -510,7 +534,7 @@ func (c *Context) GetFunction(name string) (*BuiltinFunction, error) {
if obj == nil || obj.Value == nil {
return nil, fmt.Errorf(`"%s" is not a function`, name)
}
// Value exists, but uneable to access in current scope
// Value exists, but unable to access in current scope
if obj.Value.Scopes&c.curMode == 0 {
return nil, fmt.Errorf(
`function "%s" could not call in scope of %s\ndocument: %s`,
Expand Down
4 changes: 2 additions & 2 deletions context/identifilers.go
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
package context

// Fastly predifned identifier list.
// Fastly predefined identifier list.
// we listed as possible as found in Fastly document site,
// but perhapse there are more builtin identifiers.
// but perhaps there are more builtin identifiers.
func builtinIdentifiers() map[string]struct{} {
return map[string]struct{}{
// use for backend.ssl_check_cert
Expand Down
Loading

0 comments on commit e85cced

Please sign in to comment.