feat(lua): 扩展 ngx.req API 并集成所有 ngx API 到沙箱
扩展 API: set_uri, set_uri_args, get_headers, set_header, clear_header, get_body_data, read_body 在 coroutine.SetupSandbox() 中统一注册 ngx.req/resp/var/ctx/log/socket API Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
86e5b0e6f1
commit
6b9df86217
@ -83,10 +83,7 @@ func newNgxReqAPI(ctx *fasthttp.RequestCtx) *ngxReqAPI {
|
|||||||
|
|
||||||
// RegisterNgxReqAPI 在 Lua 状态机中注册 ngx.req API
|
// RegisterNgxReqAPI 在 Lua 状态机中注册 ngx.req API
|
||||||
// 这是主入口函数,由 LuaEngine 在初始化时调用
|
// 这是主入口函数,由 LuaEngine 在初始化时调用
|
||||||
func RegisterNgxReqAPI(L *glua.LState, api *ngxReqAPI) {
|
func RegisterNgxReqAPI(L *glua.LState, api *ngxReqAPI, ngxTable *glua.LTable) {
|
||||||
// 创建 ngx 表
|
|
||||||
ngx := L.NewTable()
|
|
||||||
|
|
||||||
// 创建 ngx.req 子表
|
// 创建 ngx.req 子表
|
||||||
ngxReq := L.NewTable()
|
ngxReq := L.NewTable()
|
||||||
|
|
||||||
@ -98,21 +95,41 @@ func RegisterNgxReqAPI(L *glua.LState, api *ngxReqAPI) {
|
|||||||
// 特点:直接返回请求的 URI 路径(不含 query string)
|
// 特点:直接返回请求的 URI 路径(不含 query string)
|
||||||
ngxReq.RawSetString("get_uri", L.NewFunction(api.luaGetURI))
|
ngxReq.RawSetString("get_uri", L.NewFunction(api.luaGetURI))
|
||||||
|
|
||||||
|
// 直接映射层 API:set_uri
|
||||||
|
// 特点:直接修改请求的 URI 路径,支持可选的内部跳转标记
|
||||||
|
ngxReq.RawSetString("set_uri", L.NewFunction(api.luaSetURI))
|
||||||
|
|
||||||
// 兼容层 API:get_uri_args
|
// 兼容层 API:get_uri_args
|
||||||
// 特点:需要解析 query string 为 nginx 兼容的表结构
|
// 特点:需要解析 query string 为 nginx 兼容的表结构
|
||||||
// 增加了解析开销,但保持 API 兼容性
|
// 增加了解析开销,但保持 API 兼容性
|
||||||
ngxReq.RawSetString("get_uri_args", L.NewFunction(api.luaGetURIArgs))
|
ngxReq.RawSetString("get_uri_args", L.NewFunction(api.luaGetURIArgs))
|
||||||
|
|
||||||
|
// 兼容层 API:set_uri_args
|
||||||
|
// 特点:支持 table 或 string 参数设置查询参数
|
||||||
|
ngxReq.RawSetString("set_uri_args", L.NewFunction(api.luaSetURIArgs))
|
||||||
|
|
||||||
|
// 兼容层 API:get_headers
|
||||||
|
// 特点:需要遍历所有请求头,模拟 nginx 的头表结构
|
||||||
|
ngxReq.RawSetString("get_headers", L.NewFunction(api.luaGetHeaders))
|
||||||
|
|
||||||
|
// 直接映射层 API:set_header
|
||||||
|
// 特点:直接操作 fasthttp 请求头,支持设置和清除
|
||||||
|
ngxReq.RawSetString("set_header", L.NewFunction(api.luaSetHeader))
|
||||||
|
|
||||||
|
// 直接映射层 API:clear_header
|
||||||
|
// 特点:直接删除 fasthttp 请求头
|
||||||
|
ngxReq.RawSetString("clear_header", L.NewFunction(api.luaClearHeader))
|
||||||
|
|
||||||
|
// 兼容层 API:get_body_data
|
||||||
|
// 特点:获取请求体内容
|
||||||
|
ngxReq.RawSetString("get_body_data", L.NewFunction(api.luaGetBodyData))
|
||||||
|
|
||||||
// 伪非阻塞层 API:read_body
|
// 伪非阻塞层 API:read_body
|
||||||
// 特点:使用 yield/resume 模式支持异步读取
|
// 特点:确保请求体已被读取(fasthttp 已预读)
|
||||||
// 这是实验性 API,展示非阻塞调用模式
|
ngxReq.RawSetString("read_body", L.NewFunction(api.luaReadBody))
|
||||||
ngxReq.RawSetString("read_body", L.NewFunction(api.luaReadBodyAsync))
|
|
||||||
|
|
||||||
// 将 ngx.req 添加到 ngx
|
// 将 ngx.req 添加到 ngx 表
|
||||||
ngx.RawSetString("req", ngxReq)
|
ngxTable.RawSetString("req", ngxReq)
|
||||||
|
|
||||||
// 注册 ngx 全局变量
|
|
||||||
L.SetGlobal("ngx", ngx)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// ==================== 直接映射层 API ====================
|
// ==================== 直接映射层 API ====================
|
||||||
@ -138,6 +155,43 @@ func (api *ngxReqAPI) luaGetMethod(L *glua.LState) int {
|
|||||||
return 1
|
return 1
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// luaSetURI 实现 ngx.req.set_uri(uri, jump?) - 直接映射层
|
||||||
|
// Lua 调用: ngx.req.set_uri("/new/path") 或 ngx.req.set_uri("/new/path", true)
|
||||||
|
// 参数:
|
||||||
|
// - uri: 新的 URI 路径
|
||||||
|
// - jump: 是否触发内部跳转(可选,默认为 false)
|
||||||
|
func (api *ngxReqAPI) luaSetURI(L *glua.LState) int {
|
||||||
|
start := time.Now()
|
||||||
|
|
||||||
|
// 获取 uri 参数
|
||||||
|
uri := L.CheckString(1)
|
||||||
|
|
||||||
|
// 获取可选的 jump 参数
|
||||||
|
jump := false
|
||||||
|
if L.GetTop() >= 2 {
|
||||||
|
jump = L.ToBool(2)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 设置新的 URI
|
||||||
|
api.ctx.Request.URI().SetPath(uri)
|
||||||
|
|
||||||
|
// 如果 jump 为 true,记录内部跳转标记(供后续处理使用)
|
||||||
|
if jump {
|
||||||
|
// 在请求上下文中存储跳转标记
|
||||||
|
api.ctx.SetUserValue("_ngx_req_internal_jump", true)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 记录指标
|
||||||
|
elapsed := uint64(time.Since(start).Nanoseconds())
|
||||||
|
api.metrics.DirectCallCount++
|
||||||
|
api.metrics.DirectTotalNs += elapsed
|
||||||
|
if elapsed > api.metrics.DirectMaxNs {
|
||||||
|
api.metrics.DirectMaxNs = elapsed
|
||||||
|
}
|
||||||
|
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
// luaGetURI 实现 ngx.req.get_uri() - 直接映射层
|
// luaGetURI 实现 ngx.req.get_uri() - 直接映射层
|
||||||
// Lua 调用: local uri = ngx.req.get_uri()
|
// Lua 调用: local uri = ngx.req.get_uri()
|
||||||
// 返回: string (如 "/path/to/resource")
|
// 返回: string (如 "/path/to/resource")
|
||||||
@ -225,7 +279,241 @@ func (api *ngxReqAPI) parseURIArgs() map[string][]string {
|
|||||||
return args
|
return args
|
||||||
}
|
}
|
||||||
|
|
||||||
// ==================== 伪非阻塞层 API(实验性) ====================
|
// luaSetURIArgs 实现 ngx.req.set_uri_args(args) - 兼容层
|
||||||
|
// Lua 调用: ngx.req.set_uri_args({ key = "value" }) 或 ngx.req.set_uri_args("key=value&foo=bar")
|
||||||
|
// 参数:
|
||||||
|
// - args: table 或 string 类型的查询参数
|
||||||
|
func (api *ngxReqAPI) luaSetURIArgs(L *glua.LState) int {
|
||||||
|
start := time.Now()
|
||||||
|
|
||||||
|
// 获取参数类型
|
||||||
|
argType := L.Get(1)
|
||||||
|
|
||||||
|
switch argType.Type() {
|
||||||
|
case glua.LTString:
|
||||||
|
// 如果是字符串,直接解析并设置
|
||||||
|
queryStr := string(argType.(glua.LString))
|
||||||
|
api.ctx.Request.URI().SetQueryString(queryStr)
|
||||||
|
|
||||||
|
case glua.LTTable:
|
||||||
|
// 如果是 table,构建查询字符串
|
||||||
|
table := argType.(*glua.LTable)
|
||||||
|
args := make(map[string][]string)
|
||||||
|
|
||||||
|
table.ForEach(func(key, value glua.LValue) {
|
||||||
|
keyStr := glua.LVAsString(key)
|
||||||
|
switch value.Type() {
|
||||||
|
case glua.LTString:
|
||||||
|
args[keyStr] = []string{string(value.(glua.LString))}
|
||||||
|
case glua.LTNumber:
|
||||||
|
args[keyStr] = []string{glua.LVAsString(value)}
|
||||||
|
case glua.LTTable:
|
||||||
|
// 数组形式的多值
|
||||||
|
arr := value.(*glua.LTable)
|
||||||
|
values := []string{}
|
||||||
|
arr.ForEach(func(_, v glua.LValue) {
|
||||||
|
values = append(values, glua.LVAsString(v))
|
||||||
|
})
|
||||||
|
args[keyStr] = values
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
// 构建查询字符串
|
||||||
|
if len(args) > 0 {
|
||||||
|
query := fasthttp.Args{}
|
||||||
|
for key, values := range args {
|
||||||
|
for _, v := range values {
|
||||||
|
query.Add(key, v)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
api.ctx.Request.URI().SetQueryString(query.String())
|
||||||
|
}
|
||||||
|
|
||||||
|
default:
|
||||||
|
L.RaiseError("set_uri_args expects table or string, got %s", argType.Type().String())
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
|
// 记录指标
|
||||||
|
elapsed := uint64(time.Since(start).Nanoseconds())
|
||||||
|
api.metrics.CompatibleCallCount++
|
||||||
|
api.metrics.CompatibleTotalNs += elapsed
|
||||||
|
if elapsed > api.metrics.CompatibleMaxNs {
|
||||||
|
api.metrics.CompatibleMaxNs = elapsed
|
||||||
|
}
|
||||||
|
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
|
// ==================== 请求头 API ====================
|
||||||
|
|
||||||
|
// luaGetHeaders 实现 ngx.req.get_headers(max_headers?) - 兼容层
|
||||||
|
// Lua 调用: local headers = ngx.req.get_headers() 或 ngx.req.get_headers(50)
|
||||||
|
// 返回: table (如 { ["host"] = "example.com", ["cookie"] = { "a=1", "b=2" } })
|
||||||
|
// 注意:兼容层需要遍历所有请求头,模拟 nginx 的头表结构
|
||||||
|
func (api *ngxReqAPI) luaGetHeaders(L *glua.LState) int {
|
||||||
|
start := time.Now()
|
||||||
|
|
||||||
|
// 获取可选的 max_headers 参数
|
||||||
|
maxHeaders := 100 // 默认最大头数
|
||||||
|
if L.GetTop() >= 1 {
|
||||||
|
maxHeaders = L.ToInt(1)
|
||||||
|
if maxHeaders <= 0 {
|
||||||
|
maxHeaders = 100
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 构建 Lua 表
|
||||||
|
result := L.NewTable()
|
||||||
|
headers := &api.ctx.Request.Header
|
||||||
|
|
||||||
|
count := 0
|
||||||
|
// 使用 VisitAll 遍历所有请求头
|
||||||
|
headers.VisitAll(func(key, value []byte) {
|
||||||
|
if count >= maxHeaders {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
keyStr := string(key)
|
||||||
|
valueStr := string(value)
|
||||||
|
|
||||||
|
// 检查是否已存在同名头(多值头)
|
||||||
|
existing := result.RawGetString(keyStr)
|
||||||
|
if existing == glua.LNil {
|
||||||
|
// 第一次遇到这个头
|
||||||
|
result.RawSetString(keyStr, glua.LString(valueStr))
|
||||||
|
} else if existingStr, ok := existing.(glua.LString); ok {
|
||||||
|
// 第二次遇到,需要转换为数组
|
||||||
|
arr := L.NewTable()
|
||||||
|
arr.Append(existingStr)
|
||||||
|
arr.Append(glua.LString(valueStr))
|
||||||
|
result.RawSetString(keyStr, arr)
|
||||||
|
} else if existingArr, ok := existing.(*glua.LTable); ok {
|
||||||
|
// 已经是数组,追加
|
||||||
|
existingArr.Append(glua.LString(valueStr))
|
||||||
|
}
|
||||||
|
count++
|
||||||
|
})
|
||||||
|
|
||||||
|
// 记录指标
|
||||||
|
elapsed := uint64(time.Since(start).Nanoseconds())
|
||||||
|
api.metrics.CompatibleCallCount++
|
||||||
|
api.metrics.CompatibleTotalNs += elapsed
|
||||||
|
if elapsed > api.metrics.CompatibleMaxNs {
|
||||||
|
api.metrics.CompatibleMaxNs = elapsed
|
||||||
|
}
|
||||||
|
|
||||||
|
L.Push(result)
|
||||||
|
return 1
|
||||||
|
}
|
||||||
|
|
||||||
|
// luaSetHeader 实现 ngx.req.set_header(key, value) - 直接映射层
|
||||||
|
// Lua 调用: ngx.req.set_header("X-Custom", "value") 或 ngx.req.set_header("X-Custom", nil) 清除
|
||||||
|
// 参数:
|
||||||
|
// - key: 头名称
|
||||||
|
// - value: 头值,如果为 nil 则清除该头
|
||||||
|
func (api *ngxReqAPI) luaSetHeader(L *glua.LState) int {
|
||||||
|
start := time.Now()
|
||||||
|
|
||||||
|
// 获取参数
|
||||||
|
key := L.CheckString(1)
|
||||||
|
value := L.Get(2)
|
||||||
|
|
||||||
|
if value == glua.LNil {
|
||||||
|
// 值为 nil,删除头
|
||||||
|
api.ctx.Request.Header.Del(key)
|
||||||
|
} else {
|
||||||
|
// 设置头值
|
||||||
|
valueStr := glua.LVAsString(value)
|
||||||
|
api.ctx.Request.Header.Set(key, valueStr)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 记录指标
|
||||||
|
elapsed := uint64(time.Since(start).Nanoseconds())
|
||||||
|
api.metrics.DirectCallCount++
|
||||||
|
api.metrics.DirectTotalNs += elapsed
|
||||||
|
if elapsed > api.metrics.DirectMaxNs {
|
||||||
|
api.metrics.DirectMaxNs = elapsed
|
||||||
|
}
|
||||||
|
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
|
// luaClearHeader 实现 ngx.req.clear_header(key) - 直接映射层
|
||||||
|
// Lua 调用: ngx.req.clear_header("X-Custom")
|
||||||
|
// 参数:
|
||||||
|
// - key: 要清除的头名称
|
||||||
|
func (api *ngxReqAPI) luaClearHeader(L *glua.LState) int {
|
||||||
|
start := time.Now()
|
||||||
|
|
||||||
|
// 获取参数
|
||||||
|
key := L.CheckString(1)
|
||||||
|
|
||||||
|
// 删除头
|
||||||
|
api.ctx.Request.Header.Del(key)
|
||||||
|
|
||||||
|
// 记录指标
|
||||||
|
elapsed := uint64(time.Since(start).Nanoseconds())
|
||||||
|
api.metrics.DirectCallCount++
|
||||||
|
api.metrics.DirectTotalNs += elapsed
|
||||||
|
if elapsed > api.metrics.DirectMaxNs {
|
||||||
|
api.metrics.DirectMaxNs = elapsed
|
||||||
|
}
|
||||||
|
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
|
// ==================== 请求体 API ====================
|
||||||
|
|
||||||
|
// luaGetBodyData 实现 ngx.req.get_body_data() - 兼容层
|
||||||
|
// Lua 调用: local body = ngx.req.get_body_data()
|
||||||
|
// 返回: string 或 nil(如果没有请求体)
|
||||||
|
func (api *ngxReqAPI) luaGetBodyData(L *glua.LState) int {
|
||||||
|
start := time.Now()
|
||||||
|
|
||||||
|
// 获取请求体
|
||||||
|
body := api.ctx.Request.Body()
|
||||||
|
|
||||||
|
if len(body) == 0 {
|
||||||
|
L.Push(glua.LNil)
|
||||||
|
} else {
|
||||||
|
L.Push(glua.LString(body))
|
||||||
|
}
|
||||||
|
|
||||||
|
// 记录指标
|
||||||
|
elapsed := uint64(time.Since(start).Nanoseconds())
|
||||||
|
api.metrics.CompatibleCallCount++
|
||||||
|
api.metrics.CompatibleTotalNs += elapsed
|
||||||
|
if elapsed > api.metrics.CompatibleMaxNs {
|
||||||
|
api.metrics.CompatibleMaxNs = elapsed
|
||||||
|
}
|
||||||
|
|
||||||
|
return 1
|
||||||
|
}
|
||||||
|
|
||||||
|
// luaReadBody 实现 ngx.req.read_body() - 伪非阻塞层
|
||||||
|
// Lua 调用: ngx.req.read_body() -- 完成后返回
|
||||||
|
// 注意:fasthttp 已经预读取了请求体,这里主要是确保请求体已被读取
|
||||||
|
func (api *ngxReqAPI) luaReadBody(L *glua.LState) int {
|
||||||
|
start := time.Now()
|
||||||
|
|
||||||
|
// fasthttp 默认会预读取请求体到内存中
|
||||||
|
// 这里我们只需要确保请求体已被读取(对于 POST/PUT 等请求)
|
||||||
|
// 如果请求体未读取,触发读取
|
||||||
|
if api.ctx.Request.Header.ContentLength() > 0 {
|
||||||
|
// 访问 Body() 会确保请求体已被读取
|
||||||
|
_ = api.ctx.Request.Body()
|
||||||
|
}
|
||||||
|
|
||||||
|
// 记录指标(使用伪非阻塞层指标)
|
||||||
|
elapsed := uint64(time.Since(start).Nanoseconds())
|
||||||
|
api.metrics.PseudoBlockingCallCount++
|
||||||
|
api.metrics.PseudoBlockingTotalNs += elapsed
|
||||||
|
if elapsed > api.metrics.PseudoBlockingMaxNs {
|
||||||
|
api.metrics.PseudoBlockingMaxNs = elapsed
|
||||||
|
}
|
||||||
|
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
// luaReadBodyAsync 实现 ngx.req.read_body() - 伪非阻塞层
|
// luaReadBodyAsync 实现 ngx.req.read_body() - 伪非阻塞层
|
||||||
// Lua 调用: ngx.req.read_body() -- 会 yield,完成后 resume
|
// Lua 调用: ngx.req.read_body() -- 会 yield,完成后 resume
|
||||||
|
|||||||
555
internal/lua/api_req_test.go
Normal file
555
internal/lua/api_req_test.go
Normal file
@ -0,0 +1,555 @@
|
|||||||
|
// Package lua 提供 ngx.req API 测试
|
||||||
|
package lua
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
"github.com/stretchr/testify/require"
|
||||||
|
"github.com/valyala/fasthttp"
|
||||||
|
glua "github.com/yuin/gopher-lua"
|
||||||
|
)
|
||||||
|
|
||||||
|
// 创建测试用的 fasthttp.RequestCtx
|
||||||
|
func createTestRequestCtx(method, uri string, headers map[string]string, body []byte) *fasthttp.RequestCtx {
|
||||||
|
ctx := &fasthttp.RequestCtx{}
|
||||||
|
|
||||||
|
// 设置请求
|
||||||
|
ctx.Request.Header.SetMethod(method)
|
||||||
|
ctx.Request.SetRequestURI(uri)
|
||||||
|
|
||||||
|
// 设置请求头
|
||||||
|
for key, value := range headers {
|
||||||
|
ctx.Request.Header.Set(key, value)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 设置请求体
|
||||||
|
if len(body) > 0 {
|
||||||
|
ctx.Request.SetBody(body)
|
||||||
|
}
|
||||||
|
|
||||||
|
return ctx
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestNgxReqGetMethod 测试 ngx.req.get_method()
|
||||||
|
func TestNgxReqGetMethod(t *testing.T) {
|
||||||
|
engine, err := NewEngine(DefaultConfig())
|
||||||
|
require.NoError(t, err)
|
||||||
|
defer engine.Close()
|
||||||
|
|
||||||
|
// 创建测试请求
|
||||||
|
reqCtx := createTestRequestCtx("POST", "/test", nil, nil)
|
||||||
|
|
||||||
|
// 创建协程
|
||||||
|
coro, err := engine.NewCoroutine(reqCtx)
|
||||||
|
require.NoError(t, err)
|
||||||
|
defer coro.Close()
|
||||||
|
|
||||||
|
// 设置沙箱(这会自动注册 ngx API)
|
||||||
|
err = coro.SetupSandbox()
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
// 测试获取请求方法
|
||||||
|
err = coro.Execute(`
|
||||||
|
local method = ngx.req.get_method()
|
||||||
|
if method ~= "POST" then
|
||||||
|
error("expected POST, got " .. tostring(method))
|
||||||
|
end
|
||||||
|
`)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestNgxReqGetURI 测试 ngx.req.get_uri()
|
||||||
|
func TestNgxReqGetURI(t *testing.T) {
|
||||||
|
engine, err := NewEngine(DefaultConfig())
|
||||||
|
require.NoError(t, err)
|
||||||
|
defer engine.Close()
|
||||||
|
|
||||||
|
reqCtx := createTestRequestCtx("GET", "/path/to/resource?key=value", nil, nil)
|
||||||
|
|
||||||
|
coro, err := engine.NewCoroutine(reqCtx)
|
||||||
|
require.NoError(t, err)
|
||||||
|
defer coro.Close()
|
||||||
|
|
||||||
|
err = coro.SetupSandbox()
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
err = coro.Execute(`
|
||||||
|
local uri = ngx.req.get_uri()
|
||||||
|
if uri ~= "/path/to/resource" then
|
||||||
|
error("expected /path/to/resource, got " .. tostring(uri))
|
||||||
|
end
|
||||||
|
`)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestNgxReqSetURI 测试 ngx.req.set_uri()
|
||||||
|
func TestNgxReqSetURI(t *testing.T) {
|
||||||
|
engine, err := NewEngine(DefaultConfig())
|
||||||
|
require.NoError(t, err)
|
||||||
|
defer engine.Close()
|
||||||
|
|
||||||
|
// 测试设置 URI(不带 jump)
|
||||||
|
reqCtx := createTestRequestCtx("GET", "/original", nil, nil)
|
||||||
|
coro, err := engine.NewCoroutine(reqCtx)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
err = coro.SetupSandbox()
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
err = coro.Execute(`
|
||||||
|
ngx.req.set_uri("/new/path")
|
||||||
|
`)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
coro.Close()
|
||||||
|
|
||||||
|
// 验证 URI 已修改
|
||||||
|
assert.Equal(t, "/new/path", string(reqCtx.URI().Path()))
|
||||||
|
|
||||||
|
// 测试设置 URI(带 jump)
|
||||||
|
reqCtx2 := createTestRequestCtx("GET", "/original", nil, nil)
|
||||||
|
coro2, err := engine.NewCoroutine(reqCtx2)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
err = coro2.SetupSandbox()
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
err = coro2.Execute(`
|
||||||
|
ngx.req.set_uri("/redirect/path", true)
|
||||||
|
`)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
coro2.Close()
|
||||||
|
|
||||||
|
assert.Equal(t, "/redirect/path", string(reqCtx2.URI().Path()))
|
||||||
|
// 验证 jump 标记已设置
|
||||||
|
jumpFlag := reqCtx2.UserValue("_ngx_req_internal_jump")
|
||||||
|
assert.Equal(t, true, jumpFlag)
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestNgxReqGetURIArgs 测试 ngx.req.get_uri_args()
|
||||||
|
func TestNgxReqGetURIArgs(t *testing.T) {
|
||||||
|
engine, err := NewEngine(DefaultConfig())
|
||||||
|
require.NoError(t, err)
|
||||||
|
defer engine.Close()
|
||||||
|
|
||||||
|
reqCtx := createTestRequestCtx("GET", "/test?foo=bar&baz=qux&arr=1&arr=2", nil, nil)
|
||||||
|
|
||||||
|
coro, err := engine.NewCoroutine(reqCtx)
|
||||||
|
require.NoError(t, err)
|
||||||
|
defer coro.Close()
|
||||||
|
|
||||||
|
err = coro.SetupSandbox()
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
err = coro.Execute(`
|
||||||
|
local args = ngx.req.get_uri_args()
|
||||||
|
|
||||||
|
if args.foo ~= "bar" then
|
||||||
|
error("expected foo=bar, got " .. tostring(args.foo))
|
||||||
|
end
|
||||||
|
|
||||||
|
if args.baz ~= "qux" then
|
||||||
|
error("expected baz=qux, got " .. tostring(args.baz))
|
||||||
|
end
|
||||||
|
|
||||||
|
-- 多值参数应该返回数组
|
||||||
|
if type(args.arr) ~= "table" then
|
||||||
|
error("expected arr to be table, got " .. type(args.arr))
|
||||||
|
end
|
||||||
|
|
||||||
|
if args.arr[1] ~= "1" or args.arr[2] ~= "2" then
|
||||||
|
error("expected arr = {1, 2}")
|
||||||
|
end
|
||||||
|
`)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestNgxReqSetURIArgs 测试 ngx.req.set_uri_args()
|
||||||
|
func TestNgxReqSetURIArgs(t *testing.T) {
|
||||||
|
engine, err := NewEngine(DefaultConfig())
|
||||||
|
require.NoError(t, err)
|
||||||
|
defer engine.Close()
|
||||||
|
|
||||||
|
reqCtx := createTestRequestCtx("GET", "/test", nil, nil)
|
||||||
|
|
||||||
|
coro, err := engine.NewCoroutine(reqCtx)
|
||||||
|
require.NoError(t, err)
|
||||||
|
defer coro.Close()
|
||||||
|
|
||||||
|
err = coro.SetupSandbox()
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
// 测试使用 table 设置参数
|
||||||
|
err = coro.Execute(`
|
||||||
|
ngx.req.set_uri_args({ key = "value", num = 123 })
|
||||||
|
`)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
|
||||||
|
queryStr := string(reqCtx.URI().QueryString())
|
||||||
|
assert.Contains(t, queryStr, "key=value")
|
||||||
|
assert.Contains(t, queryStr, "num=123")
|
||||||
|
|
||||||
|
// 测试使用字符串设置参数
|
||||||
|
reqCtx2 := createTestRequestCtx("GET", "/test", nil, nil)
|
||||||
|
coro2, err := engine.NewCoroutine(reqCtx2)
|
||||||
|
require.NoError(t, err)
|
||||||
|
defer coro2.Close()
|
||||||
|
|
||||||
|
err = coro2.SetupSandbox()
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
err = coro2.Execute(`
|
||||||
|
ngx.req.set_uri_args("foo=bar&baz=qux")
|
||||||
|
`)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
|
||||||
|
assert.Equal(t, "foo=bar&baz=qux", string(reqCtx2.URI().QueryString()))
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestNgxReqGetHeaders 测试 ngx.req.get_headers()
|
||||||
|
func TestNgxReqGetHeaders(t *testing.T) {
|
||||||
|
engine, err := NewEngine(DefaultConfig())
|
||||||
|
require.NoError(t, err)
|
||||||
|
defer engine.Close()
|
||||||
|
|
||||||
|
reqCtx := createTestRequestCtx("GET", "/test", map[string]string{
|
||||||
|
"Host": "example.com",
|
||||||
|
"X-Custom": "custom-value",
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
}, nil)
|
||||||
|
|
||||||
|
coro, err := engine.NewCoroutine(reqCtx)
|
||||||
|
require.NoError(t, err)
|
||||||
|
defer coro.Close()
|
||||||
|
|
||||||
|
err = coro.SetupSandbox()
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
err = coro.Execute(`
|
||||||
|
local headers = ngx.req.get_headers()
|
||||||
|
|
||||||
|
if headers.Host ~= "example.com" then
|
||||||
|
error("expected Host=example.com, got " .. tostring(headers.Host))
|
||||||
|
end
|
||||||
|
|
||||||
|
if headers["X-Custom"] ~= "custom-value" then
|
||||||
|
error("expected X-Custom=custom-value")
|
||||||
|
end
|
||||||
|
|
||||||
|
if headers["Content-Type"] ~= "application/json" then
|
||||||
|
error("expected Content-Type=application/json")
|
||||||
|
end
|
||||||
|
`)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestNgxReqSetHeader 测试 ngx.req.set_header()
|
||||||
|
func TestNgxReqSetHeader(t *testing.T) {
|
||||||
|
engine, err := NewEngine(DefaultConfig())
|
||||||
|
require.NoError(t, err)
|
||||||
|
defer engine.Close()
|
||||||
|
|
||||||
|
// 测试设置请求头
|
||||||
|
reqCtx := createTestRequestCtx("GET", "/test", nil, nil)
|
||||||
|
coro, err := engine.NewCoroutine(reqCtx)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
err = coro.SetupSandbox()
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
err = coro.Execute(`
|
||||||
|
ngx.req.set_header("X-Custom-Header", "custom-value")
|
||||||
|
`)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
coro.Close()
|
||||||
|
|
||||||
|
assert.Equal(t, "custom-value", string(reqCtx.Request.Header.Peek("X-Custom-Header")))
|
||||||
|
|
||||||
|
// 测试使用 nil 清除请求头
|
||||||
|
reqCtx2 := createTestRequestCtx("GET", "/test", map[string]string{
|
||||||
|
"X-Custom-Header": "custom-value",
|
||||||
|
}, nil)
|
||||||
|
coro2, err := engine.NewCoroutine(reqCtx2)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
err = coro2.SetupSandbox()
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
err = coro2.Execute(`
|
||||||
|
ngx.req.set_header("X-Custom-Header", nil)
|
||||||
|
`)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
coro2.Close()
|
||||||
|
|
||||||
|
assert.Equal(t, "", string(reqCtx2.Request.Header.Peek("X-Custom-Header")))
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestNgxReqClearHeader 测试 ngx.req.clear_header()
|
||||||
|
func TestNgxReqClearHeader(t *testing.T) {
|
||||||
|
engine, err := NewEngine(DefaultConfig())
|
||||||
|
require.NoError(t, err)
|
||||||
|
defer engine.Close()
|
||||||
|
|
||||||
|
reqCtx := createTestRequestCtx("GET", "/test", map[string]string{
|
||||||
|
"X-To-Clear": "value",
|
||||||
|
}, nil)
|
||||||
|
|
||||||
|
coro, err := engine.NewCoroutine(reqCtx)
|
||||||
|
require.NoError(t, err)
|
||||||
|
defer coro.Close()
|
||||||
|
|
||||||
|
err = coro.SetupSandbox()
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
// 先验证头存在
|
||||||
|
assert.Equal(t, "value", string(reqCtx.Request.Header.Peek("X-To-Clear")))
|
||||||
|
|
||||||
|
// 清除头
|
||||||
|
err = coro.Execute(`
|
||||||
|
ngx.req.clear_header("X-To-Clear")
|
||||||
|
`)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
|
||||||
|
assert.Equal(t, "", string(reqCtx.Request.Header.Peek("X-To-Clear")))
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestNgxReqGetBodyData 测试 ngx.req.get_body_data()
|
||||||
|
func TestNgxReqGetBodyData(t *testing.T) {
|
||||||
|
engine, err := NewEngine(DefaultConfig())
|
||||||
|
require.NoError(t, err)
|
||||||
|
defer engine.Close()
|
||||||
|
|
||||||
|
reqCtx := createTestRequestCtx("POST", "/test", nil, []byte("test body data"))
|
||||||
|
|
||||||
|
coro, err := engine.NewCoroutine(reqCtx)
|
||||||
|
require.NoError(t, err)
|
||||||
|
defer coro.Close()
|
||||||
|
|
||||||
|
err = coro.SetupSandbox()
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
err = coro.Execute(`
|
||||||
|
local body = ngx.req.get_body_data()
|
||||||
|
if body ~= "test body data" then
|
||||||
|
error("expected 'test body data', got " .. tostring(body))
|
||||||
|
end
|
||||||
|
`)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestNgxReqGetBodyDataEmpty 测试 ngx.req.get_body_data() 空请求体
|
||||||
|
func TestNgxReqGetBodyDataEmpty(t *testing.T) {
|
||||||
|
engine, err := NewEngine(DefaultConfig())
|
||||||
|
require.NoError(t, err)
|
||||||
|
defer engine.Close()
|
||||||
|
|
||||||
|
reqCtx := createTestRequestCtx("GET", "/test", nil, nil)
|
||||||
|
|
||||||
|
coro, err := engine.NewCoroutine(reqCtx)
|
||||||
|
require.NoError(t, err)
|
||||||
|
defer coro.Close()
|
||||||
|
|
||||||
|
err = coro.SetupSandbox()
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
err = coro.Execute(`
|
||||||
|
local body = ngx.req.get_body_data()
|
||||||
|
if body ~= nil then
|
||||||
|
error("expected nil for empty body, got " .. tostring(body))
|
||||||
|
end
|
||||||
|
`)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestNgxReqReadBody 测试 ngx.req.read_body()
|
||||||
|
func TestNgxReqReadBody(t *testing.T) {
|
||||||
|
engine, err := NewEngine(DefaultConfig())
|
||||||
|
require.NoError(t, err)
|
||||||
|
defer engine.Close()
|
||||||
|
|
||||||
|
reqCtx := createTestRequestCtx("POST", "/test", map[string]string{
|
||||||
|
"Content-Length": "14",
|
||||||
|
}, []byte("test body data"))
|
||||||
|
|
||||||
|
coro, err := engine.NewCoroutine(reqCtx)
|
||||||
|
require.NoError(t, err)
|
||||||
|
defer coro.Close()
|
||||||
|
|
||||||
|
err = coro.SetupSandbox()
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
// read_body 应该成功执行
|
||||||
|
err = coro.Execute(`
|
||||||
|
ngx.req.read_body()
|
||||||
|
`)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
|
||||||
|
// 验证请求体仍可访问
|
||||||
|
body := reqCtx.Request.Body()
|
||||||
|
assert.Equal(t, "test body data", string(body))
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestNgxReqAPIIntegration 测试 ngx.req API 集成场景
|
||||||
|
func TestNgxReqAPIIntegration(t *testing.T) {
|
||||||
|
engine, err := NewEngine(DefaultConfig())
|
||||||
|
require.NoError(t, err)
|
||||||
|
defer engine.Close()
|
||||||
|
|
||||||
|
reqCtx := createTestRequestCtx("POST", "/api/users?limit=10&offset=20", map[string]string{
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
"X-API-Key": "secret123",
|
||||||
|
}, []byte(`{"name":"test"}`))
|
||||||
|
|
||||||
|
coro, err := engine.NewCoroutine(reqCtx)
|
||||||
|
require.NoError(t, err)
|
||||||
|
defer coro.Close()
|
||||||
|
|
||||||
|
err = coro.SetupSandbox()
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
// 复杂场景:获取各种请求信息并修改
|
||||||
|
err = coro.Execute(`
|
||||||
|
-- 获取请求信息
|
||||||
|
local method = ngx.req.get_method()
|
||||||
|
local uri = ngx.req.get_uri()
|
||||||
|
local args = ngx.req.get_uri_args()
|
||||||
|
local headers = ngx.req.get_headers()
|
||||||
|
|
||||||
|
-- 验证获取的信息
|
||||||
|
if method ~= "POST" then
|
||||||
|
error("method should be POST")
|
||||||
|
end
|
||||||
|
|
||||||
|
if uri ~= "/api/users" then
|
||||||
|
error("uri should be /api/users, got " .. tostring(uri))
|
||||||
|
end
|
||||||
|
|
||||||
|
if args.limit ~= "10" or args.offset ~= "20" then
|
||||||
|
error("args incorrect")
|
||||||
|
end
|
||||||
|
|
||||||
|
-- 注意:fasthttp 会标准化 header 名称,所以需要使用实际的 key
|
||||||
|
if headers["Content-Type"] ~= "application/json" and headers["content-type"] ~= "application/json" then
|
||||||
|
error("Content-Type header incorrect: " .. tostring(headers["Content-Type"]))
|
||||||
|
end
|
||||||
|
|
||||||
|
-- 修改请求
|
||||||
|
ngx.req.set_header("X-Request-ID", "req-12345")
|
||||||
|
ngx.req.set_uri("/api/v2/users")
|
||||||
|
`)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
|
||||||
|
// 在 Go 层验证修改
|
||||||
|
assert.Equal(t, "/api/v2/users", string(reqCtx.URI().Path()))
|
||||||
|
assert.Equal(t, "req-12345", string(reqCtx.Request.Header.Peek("X-Request-ID")))
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestNgxReqMetrics 测试 ngx.req API 性能指标
|
||||||
|
func TestNgxReqMetrics(t *testing.T) {
|
||||||
|
reqCtx := createTestRequestCtx("GET", "/test?a=1&b=2", nil, nil)
|
||||||
|
api := newNgxReqAPI(reqCtx)
|
||||||
|
|
||||||
|
L := glua.NewState()
|
||||||
|
defer L.Close()
|
||||||
|
|
||||||
|
// 创建 ngx 表
|
||||||
|
ngx := L.NewTable()
|
||||||
|
|
||||||
|
// 注册 API
|
||||||
|
RegisterNgxReqAPI(L, api, ngx)
|
||||||
|
|
||||||
|
// 将 ngx 设置到全局
|
||||||
|
L.SetGlobal("ngx", ngx)
|
||||||
|
|
||||||
|
// 调用各种 API
|
||||||
|
L.DoString(`
|
||||||
|
ngx.req.get_method()
|
||||||
|
ngx.req.get_uri()
|
||||||
|
ngx.req.get_uri_args()
|
||||||
|
`)
|
||||||
|
|
||||||
|
// 验证指标
|
||||||
|
metrics := api.GetMetrics()
|
||||||
|
assert.Greater(t, metrics.DirectCallCount, uint64(0), "应该有直接层调用")
|
||||||
|
assert.Greater(t, metrics.CompatibleCallCount, uint64(0), "应该有兼容层调用")
|
||||||
|
|
||||||
|
// 验证平均延迟
|
||||||
|
directAvg := api.GetDirectLayerAvgNs()
|
||||||
|
compatibleAvg := api.GetCompatibleLayerAvgNs()
|
||||||
|
assert.GreaterOrEqual(t, directAvg, float64(0))
|
||||||
|
assert.GreaterOrEqual(t, compatibleAvg, float64(0))
|
||||||
|
|
||||||
|
// 验证性能比率
|
||||||
|
ratio := api.GetPerformanceRatio()
|
||||||
|
assert.GreaterOrEqual(t, ratio, float64(0))
|
||||||
|
|
||||||
|
// 重置指标
|
||||||
|
api.ResetMetrics()
|
||||||
|
metrics = api.GetMetrics()
|
||||||
|
assert.Equal(t, uint64(0), metrics.DirectCallCount)
|
||||||
|
assert.Equal(t, uint64(0), metrics.CompatibleCallCount)
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestNgxReqGetHeadersWithMaxHeaders 测试 ngx.req.get_headers(max_headers)
|
||||||
|
func TestNgxReqGetHeadersWithMaxHeaders(t *testing.T) {
|
||||||
|
engine, err := NewEngine(DefaultConfig())
|
||||||
|
require.NoError(t, err)
|
||||||
|
defer engine.Close()
|
||||||
|
|
||||||
|
// 创建带有多个头的请求
|
||||||
|
reqCtx := &fasthttp.RequestCtx{}
|
||||||
|
reqCtx.Request.Header.SetMethod("GET")
|
||||||
|
reqCtx.Request.SetRequestURI("/test")
|
||||||
|
reqCtx.Request.Header.Set("Header1", "value1")
|
||||||
|
reqCtx.Request.Header.Set("Header2", "value2")
|
||||||
|
reqCtx.Request.Header.Set("Header3", "value3")
|
||||||
|
|
||||||
|
coro, err := engine.NewCoroutine(reqCtx)
|
||||||
|
require.NoError(t, err)
|
||||||
|
defer coro.Close()
|
||||||
|
|
||||||
|
err = coro.SetupSandbox()
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
// 测试限制头数
|
||||||
|
err = coro.Execute(`
|
||||||
|
local headers = ngx.req.get_headers(2)
|
||||||
|
local count = 0
|
||||||
|
for k, v in pairs(headers) do
|
||||||
|
count = count + 1
|
||||||
|
end
|
||||||
|
-- 应该最多返回 2 个头
|
||||||
|
if count > 2 then
|
||||||
|
error("expected at most 2 headers, got " .. count)
|
||||||
|
end
|
||||||
|
`)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestNgxReqSetURIArgsWithArray 测试 ngx.req.set_uri_args() 使用数组值
|
||||||
|
func TestNgxReqSetURIArgsWithArray(t *testing.T) {
|
||||||
|
engine, err := NewEngine(DefaultConfig())
|
||||||
|
require.NoError(t, err)
|
||||||
|
defer engine.Close()
|
||||||
|
|
||||||
|
reqCtx := createTestRequestCtx("GET", "/test", nil, nil)
|
||||||
|
|
||||||
|
coro, err := engine.NewCoroutine(reqCtx)
|
||||||
|
require.NoError(t, err)
|
||||||
|
defer coro.Close()
|
||||||
|
|
||||||
|
err = coro.SetupSandbox()
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
// 测试使用包含数组的 table
|
||||||
|
err = coro.Execute(`
|
||||||
|
ngx.req.set_uri_args({ tags = { "a", "b", "c" }, page = 1 })
|
||||||
|
`)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
|
||||||
|
queryStr := string(reqCtx.URI().QueryString())
|
||||||
|
assert.Contains(t, queryStr, "tags=a")
|
||||||
|
assert.Contains(t, queryStr, "tags=b")
|
||||||
|
assert.Contains(t, queryStr, "tags=c")
|
||||||
|
assert.Contains(t, queryStr, "page=1")
|
||||||
|
}
|
||||||
@ -117,6 +117,9 @@ func (c *LuaCoroutine) SetupSandbox() error {
|
|||||||
// Layer 1 & 2: 设置安全的协程库(移除危险函数)
|
// Layer 1 & 2: 设置安全的协程库(移除危险函数)
|
||||||
c.setupSecureCoroutineLib()
|
c.setupSecureCoroutineLib()
|
||||||
|
|
||||||
|
// Layer 3: 设置 ngx API
|
||||||
|
c.setupNgxAPI()
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -163,6 +166,48 @@ func (c *LuaCoroutine) setupSecureCoroutineLib() {
|
|||||||
// 因为协程继承的是引擎全局环境,而我们在协程级别设置了独立的 coroutine 表
|
// 因为协程继承的是引擎全局环境,而我们在协程级别设置了独立的 coroutine 表
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// setupNgxAPI 创建 ngx API
|
||||||
|
// 注册 ngx.req、ngx.resp、ngx.var、ngx.ctx、ngx.log 和 ngx.socket API
|
||||||
|
func (c *LuaCoroutine) setupNgxAPI() {
|
||||||
|
// 检查是否已有 ngx 表(可能已由其他 API 注册)
|
||||||
|
existingNgx := c.Co.GetGlobal("ngx")
|
||||||
|
var ngx *glua.LTable
|
||||||
|
if existingTbl, ok := existingNgx.(*glua.LTable); ok {
|
||||||
|
ngx = existingTbl
|
||||||
|
} else {
|
||||||
|
// 创建 ngx 表
|
||||||
|
ngx = c.Co.NewTable()
|
||||||
|
}
|
||||||
|
|
||||||
|
// 注册 ngx.req API
|
||||||
|
if c.RequestCtx != nil {
|
||||||
|
reqAPI := newNgxReqAPI(c.RequestCtx)
|
||||||
|
RegisterNgxReqAPI(c.Co, reqAPI, ngx)
|
||||||
|
|
||||||
|
// 注册 ngx.resp API
|
||||||
|
respAPI := newNgxRespAPI(c.RequestCtx)
|
||||||
|
RegisterNgxRespAPI(c.Co, respAPI)
|
||||||
|
|
||||||
|
// 注册 ngx.log API (logger 为 nil 时禁用日志输出)
|
||||||
|
// ngx.say/print/flush 直接写入 RequestCtx
|
||||||
|
logAPI := newNgxLogAPI(c.RequestCtx, nil, nil)
|
||||||
|
RegisterNgxLogAPI(c.Co, logAPI)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 注册 ngx.var API
|
||||||
|
varAPI := newNgxVarAPI(c.RequestCtx)
|
||||||
|
RegisterNgxVarAPI(c.Co, varAPI, ngx)
|
||||||
|
|
||||||
|
// 注册 ngx.ctx API
|
||||||
|
RegisterNgxCtxAPI(c.Co, ngx)
|
||||||
|
|
||||||
|
// 注册 ngx.socket API
|
||||||
|
RegisterTCPSocketAPI(c.Co, c.Engine)
|
||||||
|
|
||||||
|
// 将 ngx 表设置到协程环境
|
||||||
|
c.Co.SetGlobal("ngx", ngx)
|
||||||
|
}
|
||||||
|
|
||||||
// Execute 在协程中执行 Lua 脚本(支持 Yield/Resume)
|
// Execute 在协程中执行 Lua 脚本(支持 Yield/Resume)
|
||||||
func (c *LuaCoroutine) Execute(script string) error {
|
func (c *LuaCoroutine) Execute(script string) error {
|
||||||
proto, err := c.Engine.codeCache.GetOrCompileInline(script)
|
proto, err := c.Engine.codeCache.GetOrCompileInline(script)
|
||||||
|
|||||||
@ -9,6 +9,7 @@ import (
|
|||||||
|
|
||||||
"github.com/stretchr/testify/assert"
|
"github.com/stretchr/testify/assert"
|
||||||
"github.com/stretchr/testify/require"
|
"github.com/stretchr/testify/require"
|
||||||
|
"github.com/valyala/fasthttp"
|
||||||
)
|
)
|
||||||
|
|
||||||
// TestLuaContext 测试 LuaContext 基础功能
|
// TestLuaContext 测试 LuaContext 基础功能
|
||||||
@ -456,3 +457,118 @@ func TestConfig(t *testing.T) {
|
|||||||
|
|
||||||
assert.Equal(t, 100, engine.maxCoroutines)
|
assert.Equal(t, 100, engine.maxCoroutines)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// TestNgxAPIRegistrationInSandbox 测试所有 ngx API 在沙箱中的注册
|
||||||
|
func TestNgxAPIRegistrationInSandbox(t *testing.T) {
|
||||||
|
engine, err := NewEngine(DefaultConfig())
|
||||||
|
require.NoError(t, err)
|
||||||
|
defer engine.Close()
|
||||||
|
|
||||||
|
// 创建 mock RequestCtx(ngx.req/resp/log API 需要 RequestCtx)
|
||||||
|
mockCtx := &fasthttp.RequestCtx{}
|
||||||
|
|
||||||
|
coro, err := engine.NewCoroutine(mockCtx)
|
||||||
|
require.NoError(t, err)
|
||||||
|
defer coro.Close()
|
||||||
|
|
||||||
|
err = coro.SetupSandbox()
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
// 验证 ngx 表存在
|
||||||
|
err = coro.Execute(`
|
||||||
|
assert(ngx ~= nil, "ngx table should exist")
|
||||||
|
assert(type(ngx) == "table", "ngx should be a table")
|
||||||
|
`)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
|
||||||
|
// 验证 ngx.req API 存在
|
||||||
|
coro2, err := engine.NewCoroutine(mockCtx)
|
||||||
|
require.NoError(t, err)
|
||||||
|
defer coro2.Close()
|
||||||
|
err = coro2.SetupSandbox()
|
||||||
|
require.NoError(t, err)
|
||||||
|
err = coro2.Execute(`
|
||||||
|
assert(ngx.req ~= nil, "ngx.req should exist")
|
||||||
|
assert(type(ngx.req.get_method) == "function", "ngx.req.get_method should be a function")
|
||||||
|
assert(type(ngx.req.get_uri) == "function", "ngx.req.get_uri should be a function")
|
||||||
|
assert(type(ngx.req.set_uri) == "function", "ngx.req.set_uri should be a function")
|
||||||
|
assert(type(ngx.req.get_uri_args) == "function", "ngx.req.get_uri_args should be a function")
|
||||||
|
assert(type(ngx.req.get_headers) == "function", "ngx.req.get_headers should be a function")
|
||||||
|
assert(type(ngx.req.set_header) == "function", "ngx.req.set_header should be a function")
|
||||||
|
assert(type(ngx.req.clear_header) == "function", "ngx.req.clear_header should be a function")
|
||||||
|
assert(type(ngx.req.get_body_data) == "function", "ngx.req.get_body_data should be a function")
|
||||||
|
`)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
|
||||||
|
// 验证 ngx.resp API 存在
|
||||||
|
coro3, err := engine.NewCoroutine(mockCtx)
|
||||||
|
require.NoError(t, err)
|
||||||
|
defer coro3.Close()
|
||||||
|
err = coro3.SetupSandbox()
|
||||||
|
require.NoError(t, err)
|
||||||
|
err = coro3.Execute(`
|
||||||
|
assert(ngx.resp ~= nil, "ngx.resp should exist")
|
||||||
|
assert(type(ngx.resp.get_status) == "function", "ngx.resp.get_status should be a function")
|
||||||
|
assert(type(ngx.resp.set_status) == "function", "ngx.resp.set_status should be a function")
|
||||||
|
assert(type(ngx.resp.get_headers) == "function", "ngx.resp.get_headers should be a function")
|
||||||
|
assert(type(ngx.resp.set_header) == "function", "ngx.resp.set_header should be a function")
|
||||||
|
assert(type(ngx.resp.clear_header) == "function", "ngx.resp.clear_header should be a function")
|
||||||
|
`)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
|
||||||
|
// 验证 ngx.var API 存在
|
||||||
|
coro4, err := engine.NewCoroutine(mockCtx)
|
||||||
|
require.NoError(t, err)
|
||||||
|
defer coro4.Close()
|
||||||
|
err = coro4.SetupSandbox()
|
||||||
|
require.NoError(t, err)
|
||||||
|
err = coro4.Execute(`
|
||||||
|
assert(ngx.var ~= nil, "ngx.var should exist")
|
||||||
|
`)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
|
||||||
|
// 验证 ngx.ctx API 存在
|
||||||
|
coro5, err := engine.NewCoroutine(mockCtx)
|
||||||
|
require.NoError(t, err)
|
||||||
|
defer coro5.Close()
|
||||||
|
err = coro5.SetupSandbox()
|
||||||
|
require.NoError(t, err)
|
||||||
|
err = coro5.Execute(`
|
||||||
|
assert(ngx.ctx ~= nil, "ngx.ctx should exist")
|
||||||
|
assert(type(ngx.ctx) == "table", "ngx.ctx should be a table")
|
||||||
|
`)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
|
||||||
|
// 验证 ngx.log API 存在(日志级别常量和函数)
|
||||||
|
coro6, err := engine.NewCoroutine(mockCtx)
|
||||||
|
require.NoError(t, err)
|
||||||
|
defer coro6.Close()
|
||||||
|
err = coro6.SetupSandbox()
|
||||||
|
require.NoError(t, err)
|
||||||
|
err = coro6.Execute(`
|
||||||
|
assert(ngx.log ~= nil, "ngx.log should exist")
|
||||||
|
assert(type(ngx.log) == "function", "ngx.log should be a function")
|
||||||
|
assert(ngx.ERR ~= nil, "ngx.ERR should exist")
|
||||||
|
assert(ngx.WARN ~= nil, "ngx.WARN should exist")
|
||||||
|
assert(ngx.INFO ~= nil, "ngx.INFO should exist")
|
||||||
|
assert(ngx.DEBUG ~= nil, "ngx.DEBUG should exist")
|
||||||
|
assert(type(ngx.say) == "function", "ngx.say should be a function")
|
||||||
|
assert(type(ngx.print) == "function", "ngx.print should be a function")
|
||||||
|
assert(type(ngx.flush) == "function", "ngx.flush should be a function")
|
||||||
|
assert(type(ngx.exit) == "function", "ngx.exit should be a function")
|
||||||
|
assert(type(ngx.redirect) == "function", "ngx.redirect should be a function")
|
||||||
|
`)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
|
||||||
|
// 验证 ngx.socket API 存在
|
||||||
|
coro7, err := engine.NewCoroutine(mockCtx)
|
||||||
|
require.NoError(t, err)
|
||||||
|
defer coro7.Close()
|
||||||
|
err = coro7.SetupSandbox()
|
||||||
|
require.NoError(t, err)
|
||||||
|
err = coro7.Execute(`
|
||||||
|
assert(ngx.socket ~= nil, "ngx.socket should exist")
|
||||||
|
assert(type(ngx.socket.tcp) == "function", "ngx.socket.tcp should be a function")
|
||||||
|
`)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
}
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user