Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 7 additions & 2 deletions api/v1alpha1/envoyproxy_types.go
Original file line number Diff line number Diff line change
Expand Up @@ -173,9 +173,14 @@ const (
// For supported APIs, see: https://www.envoyproxy.io/docs/envoy/latest/configuration/http/http_filters/lua_filter#stream-handle-api
LuaValidationStrict LuaValidation = "Strict"

// LuaValidationDisabled disables all validation of Lua scripts.
// LuaValidationSyntax checks for syntax errors in the Lua script.
// Note that this is not a full runtime validation and does not check for issues during script execution.
// This is recommended if your scripts use external libraries that are not supported by Lua runtime validation.
LuaValidationSyntax LuaValidation = "Syntax"

// LuaValidationDisabled disables all validations of Lua scripts.
// Scripts will be accepted and executed without any validation checks.
// This is not recommended unless your scripts import libraries that are not supported by Lua runtime validation.
// This is not recommended unless both runtime and syntax validations are failing unexpectedly.
LuaValidationDisabled LuaValidation = "Disabled"
)

Expand Down
13 changes: 6 additions & 7 deletions internal/gatewayapi/envoyextensionpolicy.go
Original file line number Diff line number Diff line change
Expand Up @@ -443,6 +443,7 @@ func (t *Translator) buildLua(
envoyProxy *egv1a1.EnvoyProxy,
) (*ir.Lua, error) {
var luaCode *string
var luaValidation egv1a1.LuaValidation
var err error
if lua.Type == egv1a1.LuaValueTypeValueRef {
luaCode, err = getLuaBodyFromLocalObjectReference(lua.ValueRef, resources, policy.Namespace)
Expand All @@ -452,14 +453,12 @@ func (t *Translator) buildLua(
if err != nil {
return nil, err
}
if envoyProxy != nil && envoyProxy.Spec.LuaValidation != nil &&
*envoyProxy.Spec.LuaValidation == egv1a1.LuaValidationDisabled {
return &ir.Lua{
Name: name,
Code: luaCode,
}, nil
if envoyProxy != nil && envoyProxy.Spec.LuaValidation != nil {
luaValidation = *envoyProxy.Spec.LuaValidation
} else {
luaValidation = egv1a1.LuaValidationStrict
}
if err = luavalidator.NewLuaValidator(*luaCode).Validate(); err != nil {
if err = luavalidator.NewLuaValidator(*luaCode, luaValidation).Validate(); err != nil {
return nil, fmt.Errorf("validation failed for lua body in policy with name %v: %w", name, err)
}
return &ir.Lua{
Expand Down
55 changes: 44 additions & 11 deletions internal/gatewayapi/luavalidator/lua_validator.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,13 @@ import (
"strings"

lua "github.com/yuin/gopher-lua"

egv1a1 "github.com/envoyproxy/gateway/api/v1alpha1"
)

const (
envoyOnRequestFunctionName = "envoy_on_request"
envoyOnResponseFunctionName = "envoy_on_response"
)

// mockData contains mocks of Envoy supported APIs for Lua filters.
Expand All @@ -20,35 +27,50 @@ import (
var mockData []byte

// LuaValidator validates user provided Lua for compatibility with Envoy supported Lua HTTP filter
// Validation strictness is controlled by the validation field
type LuaValidator struct {
code string
code string
validation egv1a1.LuaValidation
}

// NewLuaValidator returns a LuaValidator for user provided Lua code
func NewLuaValidator(code string) *LuaValidator {
func NewLuaValidator(code string, validation egv1a1.LuaValidation) *LuaValidator {
return &LuaValidator{
code: code,
code: code,
validation: validation,
}
}

// Validate runs all validations for the LuaValidator
func (l *LuaValidator) Validate() error {
if !strings.Contains(l.code, "envoy_on_request") && !strings.Contains(l.code, "envoy_on_response") {
return fmt.Errorf("expected one of envoy_on_request() or envoy_on_response() to be defined")
if !strings.Contains(l.code, envoyOnRequestFunctionName) && !strings.Contains(l.code, envoyOnResponseFunctionName) {
return fmt.Errorf("expected one of %s() or %s() to be defined", envoyOnRequestFunctionName, envoyOnResponseFunctionName)
}
if strings.Contains(l.code, "envoy_on_request") {
if err := l.runLua(string(mockData) + "\n" + l.code + "\nenvoy_on_request(StreamHandle)"); err != nil {
return fmt.Errorf("failed to mock run envoy_on_request: %w", err)
if strings.Contains(l.code, envoyOnRequestFunctionName) {
if err := l.validate(string(mockData) + "\n" + l.code + "\n" + envoyOnRequestFunctionName + "(StreamHandle)"); err != nil {
return fmt.Errorf("failed to validate with %s: %w", envoyOnRequestFunctionName, err)
}
}
if strings.Contains(l.code, "envoy_on_response") {
if err := l.runLua(string(mockData) + "\n" + l.code + "\nenvoy_on_response(StreamHandle)"); err != nil {
return fmt.Errorf("failed to mock run envoy_on_response: %w", err)
if strings.Contains(l.code, envoyOnResponseFunctionName) {
if err := l.validate(string(mockData) + "\n" + l.code + "\n" + envoyOnResponseFunctionName + "(StreamHandle)"); err != nil {
return fmt.Errorf("failed to validate with %s: %w", envoyOnResponseFunctionName, err)
}
}
return nil
}

// validate runs the validation on given code
func (l *LuaValidator) validate(code string) error {
switch l.validation {
case egv1a1.LuaValidationSyntax:
return l.loadLua(code)
case egv1a1.LuaValidationDisabled:
return nil
default:
return l.runLua(code)
}
}

// runLua interprets and runs the provided Lua code in runtime using gopher-lua
// Refer: https://github.com/yuin/gopher-lua?tab=readme-ov-file#differences-between-lua-and-gopherlua
func (l *LuaValidator) runLua(code string) error {
Expand All @@ -59,3 +81,14 @@ func (l *LuaValidator) runLua(code string) error {
}
return nil
}

// loadLua loads the Lua code into the Lua state, does not run it
// This is used to check for syntax errors in the Lua code
func (l *LuaValidator) loadLua(code string) error {
L := lua.NewState()
defer L.Close()
if _, err := L.LoadString(code); err != nil {
return err
}
return nil
}
47 changes: 46 additions & 1 deletion internal/gatewayapi/luavalidator/lua_validator_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,12 +8,15 @@ package luavalidator
import (
"strings"
"testing"

egv1a1 "github.com/envoyproxy/gateway/api/v1alpha1"
)

func Test_Validate(t *testing.T) {
type args struct {
name string
code string
validation egv1a1.LuaValidation
expectedErrSubstring string
}
tests := []args{
Expand Down Expand Up @@ -130,10 +133,52 @@ func Test_Validate(t *testing.T) {
end`,
expectedErrSubstring: "attempt to call a non-function object",
},
{
name: "unsupported api",
code: `function envoy_on_request(request_handle)
request_handle:unknownApi()
end`,
validation: egv1a1.LuaValidationSyntax,
expectedErrSubstring: "",
},
{
name: "unsupported api",
code: `function envoy_on_response(response_handle)
-- Sets the content-type.
response_handle:headers():replace("content-type", "text/html")
local last
for chunk in response_handle:bodyChunks() do
-- Clears each received chunk.
chunk:setBytes("")
last = chunk
-- invalid syntax as there is no end for the for loop

last:setBytes("<html><b>Not Found<b></html>")
end`,
validation: egv1a1.LuaValidationSyntax,
expectedErrSubstring: "<string> at EOF: syntax error",
},
{
name: "unsupported api",
code: `function envoy_on_response(response_handle)
-- Sets the content-type.
response_handle:headers():replace("content-type", "text/html")
local last
for chunk in response_handle:bodyChunks() do
-- Clears each received chunk.
chunk:setBytes("")
last = chunk
-- invalid syntax as there is no end for the for loop

last:setBytes("<html><b>Not Found<b></html>")
end`,
validation: egv1a1.LuaValidationDisabled,
expectedErrSubstring: "",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
l := NewLuaValidator(tt.code)
l := NewLuaValidator(tt.code, tt.validation)
if err := l.Validate(); err != nil && tt.expectedErrSubstring == "" {
t.Errorf("Unexpected error: %v", err)
} else if err != nil && !strings.Contains(err.Error(), tt.expectedErrSubstring) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -46,14 +46,18 @@ envoyextensionpolicies:
kind: EnvoyExtensionPolicy
metadata:
namespace: default
name: policy-for-http-route # Invalid Lua but still gets accepted
name: policy-for-http-route
spec:
targetRef:
group: gateway.networking.k8s.io
kind: HTTPRoute
name: httproute-1
lua:
- type: Inline
inline: "function envoy_on_response(response_handle)
response_handle:UnknownApi()
end"
- type: Inline # Invalid Lua syntax (missing then keyword in if statement) but should be accepted
inline: |
function envoy_on_response(response_handle)
local value = 10
if value > 5
print("Value is greater than 5")
end
end
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,12 @@ envoyExtensionPolicies:
namespace: default
spec:
lua:
- inline: function envoy_on_response(response_handle) response_handle:UnknownApi()
- inline: |
function envoy_on_response(response_handle)
local value = 10
if value > 5
print("Value is greater than 5")
end
end
type: Inline
targetRef:
Expand Down Expand Up @@ -181,7 +186,12 @@ xdsIR:
weight: 1
envoyExtensions:
luas:
- Code: function envoy_on_response(response_handle) response_handle:UnknownApi()
- Code: |
function envoy_on_response(response_handle)
local value = 10
if value > 5
print("Value is greater than 5")
end
end
Name: envoyextensionpolicy/default/policy-for-http-route/lua/0
hostname: www.example.com
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,113 @@
envoyProxyForGatewayClass:
apiVersion: gateway.envoyproxy.io/v1alpha1
kind: EnvoyProxy
metadata:
namespace: envoy-gateway-system
name: test
spec:
luaValidation: Syntax
gateways:
- apiVersion: gateway.networking.k8s.io/v1
kind: Gateway
metadata:
namespace: envoy-gateway
name: gateway-1
spec:
gatewayClassName: envoy-gateway-class
listeners:
- name: http
protocol: HTTP
port: 80
allowedRoutes:
namespaces:
from: All
httpRoutes:
- apiVersion: gateway.networking.k8s.io/v1
kind: HTTPRoute
metadata:
namespace: default
name: httproute-1
spec:
hostnames:
- www.example.com
parentRefs:
- namespace: envoy-gateway
name: gateway-1
sectionName: http
rules:
- matches:
- path:
value: "/foo"
backendRefs:
- name: service-1
port: 8080
- apiVersion: gateway.networking.k8s.io/v1
kind: HTTPRoute
metadata:
namespace: default
name: httproute-2
spec:
hostnames:
- www.example.com
parentRefs:
- namespace: envoy-gateway
name: gateway-1
sectionName: http
rules:
- matches:
- path:
value: "/foo"
backendRefs:
- name: service-1
port: 8080
envoyextensionpolicies:
- apiVersion: gateway.envoyproxy.io/v1alpha1
kind: EnvoyExtensionPolicy
metadata:
namespace: default
name: policy-for-http-route
spec:
targetRef:
group: gateway.networking.k8s.io
kind: HTTPRoute
name: httproute-1
lua:
- type: Inline # Lua with external library and UnknownApi() call but correct syntax so should be accepted
inline: |
local json = require("json")
function envoy_on_response(response_handle)
local content_type = response_handle:headers():get("content-type")
if content_type and string.find(content_type, "application/json", 1, true) then
response_handle:body():setBytes(0, response_handle:body():length())
response_handle:UnknownApi()
local response_body = response_handle:body():getBytes(0, response_handle:body():length())
if response_body and #response_body > 0 then
local parsed_json = json.decode(response_body)
if type(parsed_json) == "table" then
response_handle:logInfo("Successfully parsed JSON response.")
else
response_handle:logWarn("Parsed JSON is not a table, or unexpected format.")
end
end
end
return envoy.lua.ResponseStatus.Continue
end
- apiVersion: gateway.envoyproxy.io/v1alpha1
kind: EnvoyExtensionPolicy
metadata:
namespace: default
name: policy-for-http-route-2
spec:
targetRef:
group: gateway.networking.k8s.io
kind: HTTPRoute
name: httproute-2
lua:
- type: Inline # Invalid Lua syntax (missing then keyword in if statement) so should be rejected
inline: |
function envoy_on_response(response_handle)
local value = 10
if value > 5
print("Value is greater than 5")
end
end
Loading