345 lines
9.6 KiB
Go
345 lines
9.6 KiB
Go
// Package lua 提供 Lua 脚本嵌入能力。
|
||
//
|
||
// 该文件包含请求级 Lua 协程的实现,包括:
|
||
// - Phase:请求处理阶段常量(对应 nginx 生命周期)
|
||
// - LuaCoroutine:请求级临时协程,每个请求独立创建
|
||
// - 沙箱机制:隔离用户脚本,防止全局污染和危险操作
|
||
// - ngx API 注册:为每个协程注册完整的 ngx.* API
|
||
//
|
||
// 注意事项:
|
||
// - 协程在 ResumeOK 后变成 dead 状态,不能复用
|
||
// - 每个协程拥有独立的 _ENV 沙箱环境
|
||
// - 协程库被安全替换,阻止用户创建嵌套协程
|
||
//
|
||
// 作者:xfy
|
||
package lua
|
||
|
||
import (
|
||
"context"
|
||
"fmt"
|
||
"sync/atomic"
|
||
"time"
|
||
|
||
"github.com/valyala/fasthttp"
|
||
glua "github.com/yuin/gopher-lua"
|
||
)
|
||
|
||
// Phase 处理阶段。
|
||
//
|
||
// 对应 nginx 请求处理生命周期,Lua 脚本可在这些阶段中执行。
|
||
type Phase int
|
||
|
||
// 处理阶段常量,对应 nginx 请求处理生命周期
|
||
const (
|
||
// PhaseInit 初始化阶段
|
||
PhaseInit Phase = iota
|
||
// PhaseRewrite 重写阶段
|
||
PhaseRewrite
|
||
// PhaseAccess 访问控制阶段
|
||
PhaseAccess
|
||
// PhaseContent 内容生成阶段
|
||
PhaseContent
|
||
// PhaseLog 日志记录阶段
|
||
PhaseLog
|
||
// PhaseHeaderFilter 响应头过滤阶段
|
||
PhaseHeaderFilter
|
||
// PhaseBodyFilter 响应体过滤阶段
|
||
PhaseBodyFilter
|
||
)
|
||
|
||
// String 返回处理阶段的字符串表示。
|
||
func (p Phase) String() string {
|
||
switch p {
|
||
case PhaseInit:
|
||
return "init"
|
||
case PhaseRewrite:
|
||
return "rewrite"
|
||
case PhaseAccess:
|
||
return "access"
|
||
case PhaseContent:
|
||
return "content"
|
||
case PhaseLog:
|
||
return "log"
|
||
case PhaseHeaderFilter:
|
||
return "header_filter"
|
||
case PhaseBodyFilter:
|
||
return "body_filter"
|
||
default:
|
||
return "unknown"
|
||
}
|
||
}
|
||
|
||
// LuaCoroutine 请求级临时协程。
|
||
//
|
||
// 每个 HTTP 请求创建一个独立的 LuaCoroutine,负责:
|
||
// - 执行用户 Lua 脚本
|
||
// - 管理 ngx.* API 实例(req、resp、var、log 等)
|
||
// - 处理 yield/resume 循环(支持 sleep、cosocket 等异步操作)
|
||
// - 维护沙箱环境(独立 _ENV,受限 coroutine 库)
|
||
//
|
||
// 注意:协程在 ResumeOK 后变成 dead 状态,不能复用
|
||
//
|
||
// 类型命名说明:虽然 lua.LuaCoroutine 存在 stuttering,但保持此命名以:
|
||
// 1) 与 LuaEngine/LuaContext 保持一致的 API 命名风格
|
||
// 2) 明确区分 Lua 运行时协程与 Go 协程概念
|
||
// 3) 保持向后兼容性
|
||
type LuaCoroutine struct {
|
||
// CreatedAt 协程创建时间
|
||
CreatedAt time.Time
|
||
|
||
// ExecutionContext 执行上下文(含超时控制)
|
||
ExecutionContext context.Context
|
||
|
||
// ngx.req API 实例
|
||
ngxReqAPI *ngxReqAPI
|
||
|
||
// 请求上下文
|
||
RequestCtx *fasthttp.RequestCtx
|
||
|
||
// 底层 Lua 协程(gopher-lua LState)
|
||
Co *glua.LState
|
||
|
||
// ngx.var API 实例
|
||
ngxVarAPI *ngxVarAPI
|
||
|
||
// ngx.resp API 实例
|
||
ngxRespAPI *ngxRespAPI
|
||
|
||
// ngx.log API 实例
|
||
ngxLogAPI *ngxLogAPI
|
||
|
||
// Cancel 协程取消函数
|
||
Cancel context.CancelFunc
|
||
|
||
// executionCancel 执行超时取消函数
|
||
executionCancel context.CancelFunc
|
||
|
||
// 所属引擎
|
||
Engine *LuaEngine
|
||
|
||
// 输出缓冲
|
||
OutputBuffer []byte
|
||
|
||
// 退出标记(ngx.exit 触发)
|
||
Exited bool
|
||
}
|
||
|
||
// SetupSandbox 创建 per-request _ENV 沙箱。
|
||
//
|
||
// 每个请求创建独立的 _ENV 表,通过元表继承全局环境。
|
||
// 安全层:
|
||
// - Layer 1 & 2: 替换 coroutine 库,阻止 create/wrap/resume/running
|
||
// - Layer 3: 注册 ngx.* API(req、resp、var、ctx、log、socket、shared、timer、location)
|
||
//
|
||
// 注意事项:
|
||
// - 阻止写入全局环境(__newindex 返回错误)
|
||
// - 不修改引擎级全局表,避免并发竞态条件
|
||
func (c *LuaCoroutine) SetupSandbox() error {
|
||
// 注意:使用 LState Pool 后,每个协程拥有独立的 LState,
|
||
// 不再共享全局环境,因此无需加锁保护
|
||
|
||
// 创建独立的 _ENV 表
|
||
env := c.Co.NewTable()
|
||
|
||
// 获取全局环境 - 使用当前 LState 的全局表
|
||
globals := c.Co.GetGlobal("_G")
|
||
|
||
// 设置元表,使未找到的变量从全局环境读取
|
||
mt := c.Co.NewTable()
|
||
mt.RawSetString("__index", globals)
|
||
|
||
// 阻止写入全局环境(可选)
|
||
readOnlyFn := c.Co.NewFunction(func(L *glua.LState) int {
|
||
L.RaiseError("attempt to modify global table (read-only)")
|
||
return 0
|
||
})
|
||
mt.RawSetString("__newindex", readOnlyFn)
|
||
|
||
// 设置元表
|
||
c.Co.SetMetatable(env, mt)
|
||
|
||
// 将 _ENV 设置到协程
|
||
c.Co.SetGlobal("_ENV", env)
|
||
|
||
// Layer 1 & 2: 设置安全的协程库(移除危险函数)
|
||
c.setupSecureCoroutineLib()
|
||
|
||
// Layer 3: 设置 ngx API
|
||
c.setupNgxAPI()
|
||
|
||
return nil
|
||
}
|
||
|
||
// setupSecureCoroutineLib 创建安全的协程库替换。
|
||
//
|
||
// 移除原始 coroutine 库中的危险函数(create、wrap、resume、running),
|
||
// 仅保留安全的 yield 和 status 函数。
|
||
// 被拦截的函数返回友好错误消息,而非直接崩溃。
|
||
func (c *LuaCoroutine) setupSecureCoroutineLib() {
|
||
// 创建安全的 coroutine 表
|
||
safeCoroutine := c.Co.NewTable()
|
||
|
||
// 从当前 LState 的 coroutine 库中获取安全的函数
|
||
coroTable := c.Co.GetGlobal("coroutine")
|
||
if coroTable != glua.LNil {
|
||
if tbl, ok := coroTable.(*glua.LTable); ok {
|
||
if yieldFn := tbl.RawGetString("yield"); yieldFn != glua.LNil {
|
||
safeCoroutine.RawSetString("yield", yieldFn)
|
||
}
|
||
if statusFn := tbl.RawGetString("status"); statusFn != glua.LNil {
|
||
safeCoroutine.RawSetString("status", statusFn)
|
||
}
|
||
}
|
||
}
|
||
|
||
// 拦截函数 - 返回友好错误
|
||
blockFn := c.Co.NewFunction(func(L *glua.LState) int {
|
||
L.RaiseError("coroutine creation is blocked in sandbox (use engine-provided coroutine instead)")
|
||
return 0
|
||
})
|
||
safeCoroutine.RawSetString("create", blockFn)
|
||
safeCoroutine.RawSetString("wrap", blockFn)
|
||
safeCoroutine.RawSetString("resume", blockFn)
|
||
safeCoroutine.RawSetString("running", blockFn) // 防止信息泄露
|
||
|
||
// 替换协程的 coroutine 全局变量
|
||
c.Co.SetGlobal("coroutine", safeCoroutine)
|
||
}
|
||
|
||
// setupNgxAPI 创建并注册 ngx API 到协程环境。
|
||
//
|
||
// 注册以下 API 子模块:
|
||
// - ngx.req:请求头/URI/方法/请求体操作
|
||
// - ngx.resp:响应状态码/头操作
|
||
// - ngx.var:nginx 变量访问和自定义变量
|
||
// - ngx.ctx:请求级上下文 table
|
||
// - ngx.log:日志输出
|
||
// - ngx.socket:TCP cosocket
|
||
// - ngx.shared:共享内存字典
|
||
// - ngx.timer:定时器
|
||
// - ngx.location:子请求
|
||
func (c *LuaCoroutine) setupNgxAPI() {
|
||
// 创建 ngx 表
|
||
ngx := c.Co.NewTable()
|
||
|
||
// 先设置到全局,让所有注册函数使用同一个 ngx 表
|
||
c.Co.SetGlobal("ngx", ngx)
|
||
|
||
// 注册 ngx.req API
|
||
if c.RequestCtx != nil {
|
||
reqAPI := newNgxReqAPI(c.RequestCtx)
|
||
c.ngxReqAPI = reqAPI
|
||
RegisterNgxReqAPI(c.Co, reqAPI, ngx)
|
||
|
||
// 注册 ngx.resp API
|
||
respAPI := newNgxRespAPI(c.RequestCtx)
|
||
c.ngxRespAPI = respAPI
|
||
RegisterNgxRespAPI(c.Co, respAPI)
|
||
|
||
// 注册 ngx.log API (logger 为 nil 时禁用日志输出)
|
||
// ngx.say/print/flush 直接写入 RequestCtx
|
||
logAPI := newNgxLogAPI(c.RequestCtx, nil, nil)
|
||
c.ngxLogAPI = logAPI
|
||
RegisterNgxLogAPI(c.Co, logAPI)
|
||
}
|
||
|
||
// 注册 ngx.var API
|
||
varAPI := newNgxVarAPI(c.RequestCtx)
|
||
c.ngxVarAPI = varAPI
|
||
RegisterNgxVarAPI(c.Co, varAPI, ngx)
|
||
|
||
// 注册 ngx.ctx API
|
||
RegisterNgxCtxAPI(c.Co, ngx)
|
||
|
||
// 注册 ngx.socket API
|
||
RegisterTCPSocketAPI(c.Co, c.Engine)
|
||
|
||
// 注册 ngx.shared.DICT API
|
||
RegisterSharedDictAPI(c.Co, c.Engine.SharedDictManager(), ngx)
|
||
|
||
// 注册 ngx.timer API
|
||
RegisterTimerAPI(c.Co, c.Engine.TimerManager(), ngx)
|
||
|
||
// 注册 ngx.location API
|
||
RegisterLocationAPI(c.Co, c.Engine.LocationManager(), ngx)
|
||
}
|
||
|
||
// Execute 在协程中执行 Lua 脚本(支持 Yield/Resume)。
|
||
//
|
||
// 该函数从代码缓存中获取或编译内联脚本,然后执行。
|
||
//
|
||
// 参数:
|
||
// - script: Lua 源代码字符串
|
||
//
|
||
// 返回值:
|
||
// - error: 编译或执行失败时返回错误
|
||
func (c *LuaCoroutine) Execute(script string) error {
|
||
proto, err := c.Engine.codeCache.GetOrCompileInline(script)
|
||
if err != nil {
|
||
return fmt.Errorf("compile script: %w", err)
|
||
}
|
||
return c.executeProto(proto)
|
||
}
|
||
|
||
// ExecuteFile 执行文件中的 Lua 脚本。
|
||
//
|
||
// 参数:
|
||
// - path: Lua 脚本文件路径
|
||
//
|
||
// 返回值:
|
||
// - error: 编译或执行失败时返回错误
|
||
func (c *LuaCoroutine) ExecuteFile(path string) error {
|
||
proto, err := c.Engine.codeCache.GetOrCompileFile(path)
|
||
if err != nil {
|
||
return fmt.Errorf("compile file: %w", err)
|
||
}
|
||
return c.executeProto(proto)
|
||
}
|
||
|
||
// executeProto 执行编译后的字节码,处理 yield/resume 循环。
|
||
//
|
||
// 该函数是协程执行的核心循环:
|
||
// 1. 从 FunctionProto 创建 Lua 函数
|
||
// 2. Resume 执行协程
|
||
// 3. 如果 yield,调用 handleYield 处理并继续 Resume
|
||
// 4. 如果 error,记录统计并返回错误
|
||
// 5. 如果正常结束,更新执行计数
|
||
func (c *LuaCoroutine) executeProto(proto *glua.FunctionProto) error {
|
||
// 在独立的 LState 上直接执行脚本
|
||
fn := c.Co.NewFunctionFromProto(proto)
|
||
c.Co.Push(fn)
|
||
err := c.Co.PCall(0, 0, nil)
|
||
if err != nil {
|
||
atomic.AddUint64(&c.Engine.stats.ScriptsErrors, 1)
|
||
return fmt.Errorf("lua execution error: %w", err)
|
||
}
|
||
|
||
atomic.AddUint64(&c.Engine.stats.ScriptsExecuted, 1)
|
||
return nil
|
||
}
|
||
|
||
// Close 关闭协程
|
||
func (c *LuaCoroutine) Close() {
|
||
c.Engine.releaseCoroutine(c)
|
||
}
|
||
|
||
// GetNgxVarAPI 获取 ngx.var API 实例(用于测试和 Go 层访问)
|
||
func (c *LuaCoroutine) GetNgxVarAPI() *ngxVarAPI {
|
||
return c.ngxVarAPI
|
||
}
|
||
|
||
// GetNgxReqAPI 获取 ngx.req API 实例(用于测试和 Go 层访问)
|
||
func (c *LuaCoroutine) GetNgxReqAPI() *ngxReqAPI {
|
||
return c.ngxReqAPI
|
||
}
|
||
|
||
// GetNgxRespAPI 获取 ngx.resp API 实例(用于测试和 Go 层访问)
|
||
func (c *LuaCoroutine) GetNgxRespAPI() *ngxRespAPI {
|
||
return c.ngxRespAPI
|
||
}
|
||
|
||
// GetNgxLogAPI 获取 ngx.log API 实例(用于测试和 Go 层访问)
|
||
func (c *LuaCoroutine) GetNgxLogAPI() *ngxLogAPI {
|
||
return c.ngxLogAPI
|
||
}
|