lolly/internal/lua/api_resp.go
xfy 26f18055ce feat(lua): 添加 Scheduler 模式下的 API 安全检查
为定时器回调上下文注册不安全的 ngx API 时返回错误:
- ngx.ctx: 请求上下文不可用
- ngx.req: 请求 API 不可用
- ngx.resp: 响应 API 不可用
- ngx.var: 变量 API 不可用
- ngx.log: 提供安全版本(不依赖 RequestCtx)

防止定时器回调中误用请求相关 API 导致并发问题。

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-04-13 09:24:43 +08:00

219 lines
6.1 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

// Package lua 提供 ngx.resp API 实现
// 本文件实现 nginx 风格的响应 API用于操作 HTTP 响应
package lua
import (
"sync"
"github.com/valyala/fasthttp"
glua "github.com/yuin/gopher-lua"
)
// ngxRespAPI ngx.resp API 实现
type ngxRespAPI struct {
// 请求上下文(包含 fasthttp.Response
ctx *fasthttp.RequestCtx
// 缓存:响应头表
headersCache map[string][]string
headersCacheOnce sync.Once
}
// newNgxRespAPI 创建 ngx.resp API 实例
func newNgxRespAPI(ctx *fasthttp.RequestCtx) *ngxRespAPI {
return &ngxRespAPI{
ctx: ctx,
headersCache: nil, // 延迟初始化
}
}
// RegisterNgxRespAPI 在 Lua 状态机中注册 ngx.resp API
// 这是主入口函数,由 LuaEngine 在初始化时调用
func RegisterNgxRespAPI(L *glua.LState, api *ngxRespAPI) {
// 获取已存在的 ngx 表(必须已设置全局)
ngx := L.GetGlobal("ngx")
if ngx == nil || ngx.Type() != glua.LTTable {
// 如果不存在,创建新表并设置全局
ngx = L.NewTable()
L.SetGlobal("ngx", ngx)
}
// 创建 ngx.resp 子表
ngxResp := L.NewTable()
// ngx.resp.get_status() - 获取响应状态码
ngxResp.RawSetString("get_status", L.NewFunction(api.luaGetStatus))
// ngx.resp.set_status(code) - 设置响应状态码
ngxResp.RawSetString("set_status", L.NewFunction(api.luaSetStatus))
// ngx.resp.get_headers(max_headers?) - 获取响应头表
ngxResp.RawSetString("get_headers", L.NewFunction(api.luaGetHeaders))
// ngx.resp.set_header(key, value) - 设置响应头
ngxResp.RawSetString("set_header", L.NewFunction(api.luaSetHeader))
// ngx.resp.clear_header(key) - 清除响应头
ngxResp.RawSetString("clear_header", L.NewFunction(api.luaClearHeader))
// 将 ngx.resp 添加到 ngx
ngx.(*glua.LTable).RawSetString("resp", ngxResp)
}
// ==================== API 实现 ====================
// luaGetStatus 实现 ngx.resp.get_status()
// Lua 调用: local status = ngx.resp.get_status()
// 返回: number (HTTP 状态码,如 200, 404, 500 等)
func (api *ngxRespAPI) luaGetStatus(L *glua.LState) int {
status := api.ctx.Response.StatusCode()
L.Push(glua.LNumber(status))
return 1
}
// luaSetStatus 实现 ngx.resp.set_status(code)
// Lua 调用: ngx.resp.set_status(404)
// 参数: code (number) - HTTP 状态码
// 返回: 无
func (api *ngxRespAPI) luaSetStatus(L *glua.LState) int {
code := L.CheckInt(1)
api.ctx.Response.SetStatusCode(code)
return 0
}
// luaGetHeaders 实现 ngx.resp.get_headers(max_headers?)
// Lua 调用: local headers = ngx.resp.get_headers() 或 ngx.resp.get_headers(100)
// 参数: max_headers (number, 可选) - 最大返回头数量,默认为 100
// 返回: table (响应头表,如 { ["Content-Type"] = "text/html", ... })
func (api *ngxRespAPI) luaGetHeaders(L *glua.LState) int {
// 获取可选的 max_headers 参数
maxHeaders := 100
if L.GetTop() >= 1 {
maxHeaders = L.CheckInt(1)
if maxHeaders <= 0 {
maxHeaders = 100
}
}
// 延迟初始化缓存
api.headersCacheOnce.Do(func() {
api.headersCache = api.parseHeaders()
})
// 构建 Lua 表
result := L.NewTable()
count := 0
for key, values := range api.headersCache {
if count >= maxHeaders {
break
}
if len(values) == 1 {
// 单值:直接存储为字符串
result.RawSetString(key, glua.LString(values[0]))
} else if len(values) > 1 {
// 多值存储为数组table
arr := L.NewTable()
for i, v := range values {
arr.RawSetInt(i+1, glua.LString(v)) // Lua 数组从 1 开始
}
result.RawSetString(key, arr)
}
count++
}
L.Push(result)
return 1
}
// luaSetHeader 实现 ngx.resp.set_header(key, value)
// Lua 调用: ngx.resp.set_header("Content-Type", "application/json")
// 参数:
// - key (string) - 头名称
// - value (string) - 头值
//
// 返回: 无
func (api *ngxRespAPI) luaSetHeader(L *glua.LState) int {
key := L.CheckString(1)
value := L.CheckString(2)
api.ctx.Response.Header.Set(key, value)
// 清除缓存,下次 get_headers 会重新解析
api.headersCache = nil
api.headersCacheOnce = sync.Once{}
return 0
}
// luaClearHeader 实现 ngx.resp.clear_header(key)
// Lua 调用: ngx.resp.clear_header("X-Custom-Header")
// 参数: key (string) - 要清除的头名称
// 返回: 无
func (api *ngxRespAPI) luaClearHeader(L *glua.LState) int {
key := L.CheckString(1)
api.ctx.Response.Header.Del(key)
// 清除缓存,下次 get_headers 会重新解析
api.headersCache = nil
api.headersCacheOnce = sync.Once{}
return 0
}
// ==================== 辅助函数 ====================
// parseHeaders 解析响应头为 map
func (api *ngxRespAPI) parseHeaders() map[string][]string {
result := make(map[string][]string)
// 遍历所有响应头
api.ctx.Response.Header.VisitAll(func(key, value []byte) {
keyStr := string(key)
valueStr := string(value)
if existing, ok := result[keyStr]; ok {
result[keyStr] = append(existing, valueStr)
} else {
result[keyStr] = []string{valueStr}
}
})
return result
}
// GetHeader 获取单个响应头值(辅助函数,供外部调用)
func (api *ngxRespAPI) GetHeader(name string) string {
return string(api.ctx.Response.Header.Peek(name))
}
// SetHeader 设置单个响应头(辅助函数,供外部调用)
func (api *ngxRespAPI) SetHeader(name, value string) {
api.ctx.Response.Header.Set(name, value)
// 清除缓存
api.headersCache = nil
api.headersCacheOnce = sync.Once{}
}
// RegisterSchedulerUnsafeRespAPI 为 Scheduler LState 注册不安全的 ngx.resp API
func RegisterSchedulerUnsafeRespAPI(L *glua.LState, ngx *glua.LTable) {
ngxResp := L.NewTable()
ngxResp.RawSetString("get_status", L.NewFunction(luaSchedulerUnsafeResp))
ngxResp.RawSetString("set_status", L.NewFunction(luaSchedulerUnsafeResp))
ngxResp.RawSetString("get_headers", L.NewFunction(luaSchedulerUnsafeResp))
ngxResp.RawSetString("set_header", L.NewFunction(luaSchedulerUnsafeResp))
ngxResp.RawSetString("clear_header", L.NewFunction(luaSchedulerUnsafeResp))
ngx.RawSetString("resp", ngxResp)
}
// luaSchedulerUnsafeResp 在 scheduler 模式下调用 ngx.resp API 时返回错误
func luaSchedulerUnsafeResp(L *glua.LState) int {
L.RaiseError("API ngx.resp not available in timer callback context")
return 0
}