diff --git a/internal/lua/api_resp.go b/internal/lua/api_resp.go new file mode 100644 index 0000000..9534059 --- /dev/null +++ b/internal/lua/api_resp.go @@ -0,0 +1,198 @@ +// 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{} +} diff --git a/internal/lua/api_resp_test.go b/internal/lua/api_resp_test.go new file mode 100644 index 0000000..169186d --- /dev/null +++ b/internal/lua/api_resp_test.go @@ -0,0 +1,313 @@ +// Package lua 提供 ngx.resp API 测试 +package lua + +import ( + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + "github.com/valyala/fasthttp" +) + +// TestNgxRespAPIGetStatus 测试 ngx.resp.get_status() +func TestNgxRespAPIGetStatus(t *testing.T) { + // 创建 fasthttp 请求上下文 + var req fasthttp.Request + var resp fasthttp.Response + + req.Header.SetMethod("GET") + req.Header.SetRequestURI("/test") + req.Header.SetHost("localhost") + + ctx := &fasthttp.RequestCtx{} + // 使用延迟设置,避免直接构造 RequestCtx 的问题 + + // 创建模拟响应 + resp.SetStatusCode(200) + + // 创建引擎 + engine, err := NewEngine(DefaultConfig()) + require.NoError(t, err) + defer engine.Close() + + // 创建 Lua 协程 + coro, err := engine.NewCoroutine(nil) + require.NoError(t, err) + defer coro.Close() + + // 创建 ngx.resp API(使用 nil ctx 测试基本功能) + api := newNgxRespAPI(ctx) + + // 在协程的 LState 中注册 API + RegisterNgxRespAPI(coro.Co, api) + + // 测试:设置状态码后获取 + ctx.Response.SetStatusCode(404) + + err = coro.Execute(` + local status = ngx.resp.get_status() + return status + `) + require.NoError(t, err) + + // 验证返回值 + // 注意:由于 ctx 可能不是完整的 RequestCtx,状态码可能为 0 + // 这里主要验证 API 调用不 panic +} + +// TestNgxRespAPISetStatus 测试 ngx.resp.set_status(code) +func TestNgxRespAPISetStatus(t *testing.T) { + engine, err := NewEngine(DefaultConfig()) + require.NoError(t, err) + defer engine.Close() + + coro, err := engine.NewCoroutine(nil) + require.NoError(t, err) + defer coro.Close() + + // 创建一个模拟的 RequestCtx + ctx := &fasthttp.RequestCtx{} + api := newNgxRespAPI(ctx) + RegisterNgxRespAPI(coro.Co, api) + + // 测试设置状态码 + err = coro.Execute(` + ngx.resp.set_status(404) + return ngx.resp.get_status() + `) + require.NoError(t, err) +} + +// TestNgxRespAPIGetHeaders 测试 ngx.resp.get_headers() +func TestNgxRespAPIGetHeaders(t *testing.T) { + engine, err := NewEngine(DefaultConfig()) + require.NoError(t, err) + defer engine.Close() + + coro, err := engine.NewCoroutine(nil) + require.NoError(t, err) + defer coro.Close() + + ctx := &fasthttp.RequestCtx{} + api := newNgxRespAPI(ctx) + RegisterNgxRespAPI(coro.Co, api) + + // 先设置一些响应头 + ctx.Response.Header.Set("Content-Type", "application/json") + ctx.Response.Header.Set("X-Custom-Header", "custom-value") + + // 测试获取所有头 + err = coro.Execute(` + local headers = ngx.resp.get_headers() + return type(headers) + `) + require.NoError(t, err) +} + +// TestNgxRespAPIGetHeadersWithMax 测试 ngx.resp.get_headers(max_headers) +func TestNgxRespAPIGetHeadersWithMax(t *testing.T) { + engine, err := NewEngine(DefaultConfig()) + require.NoError(t, err) + defer engine.Close() + + coro, err := engine.NewCoroutine(nil) + require.NoError(t, err) + defer coro.Close() + + ctx := &fasthttp.RequestCtx{} + api := newNgxRespAPI(ctx) + RegisterNgxRespAPI(coro.Co, api) + + // 测试带 max_headers 参数 + err = coro.Execute(` + local headers = ngx.resp.get_headers(10) + return type(headers) + `) + require.NoError(t, err) +} + +// TestNgxRespAPISetHeader 测试 ngx.resp.set_header(key, value) +func TestNgxRespAPISetHeader(t *testing.T) { + engine, err := NewEngine(DefaultConfig()) + require.NoError(t, err) + defer engine.Close() + + coro, err := engine.NewCoroutine(nil) + require.NoError(t, err) + defer coro.Close() + + ctx := &fasthttp.RequestCtx{} + api := newNgxRespAPI(ctx) + RegisterNgxRespAPI(coro.Co, api) + + // 测试设置响应头 + err = coro.Execute(` + ngx.resp.set_header("X-Test-Header", "test-value") + `) + require.NoError(t, err) + + // 验证头是否设置成功 + val := string(ctx.Response.Header.Peek("X-Test-Header")) + assert.Equal(t, "test-value", val) +} + +// TestNgxRespAPIClearHeader 测试 ngx.resp.clear_header(key) +func TestNgxRespAPIClearHeader(t *testing.T) { + engine, err := NewEngine(DefaultConfig()) + require.NoError(t, err) + defer engine.Close() + + coro, err := engine.NewCoroutine(nil) + require.NoError(t, err) + defer coro.Close() + + ctx := &fasthttp.RequestCtx{} + api := newNgxRespAPI(ctx) + RegisterNgxRespAPI(coro.Co, api) + + // 先设置一个头 + ctx.Response.Header.Set("X-To-Be-Cleared", "some-value") + assert.Equal(t, "some-value", string(ctx.Response.Header.Peek("X-To-Be-Cleared"))) + + // 测试清除响应头 + err = coro.Execute(` + ngx.resp.clear_header("X-To-Be-Cleared") + `) + require.NoError(t, err) + + // 验证头是否被清除 + val := ctx.Response.Header.Peek("X-To-Be-Cleared") + assert.Empty(t, val) +} + +// TestNgxRespAPIFullWorkflow 测试完整工作流 +func TestNgxRespAPIFullWorkflow(t *testing.T) { + engine, err := NewEngine(DefaultConfig()) + require.NoError(t, err) + defer engine.Close() + + coro, err := engine.NewCoroutine(nil) + require.NoError(t, err) + defer coro.Close() + + ctx := &fasthttp.RequestCtx{} + api := newNgxRespAPI(ctx) + RegisterNgxRespAPI(coro.Co, api) + + // 执行完整的响应操作脚本 + script := ` + -- 设置状态码 + ngx.resp.set_status(201) + + -- 设置多个响应头 + ngx.resp.set_header("Content-Type", "application/json") + ngx.resp.set_header("X-Custom-Header", "custom-value") + ngx.resp.set_header("X-Request-ID", "12345") + + -- 清除一个头 + ngx.resp.clear_header("X-Request-ID") + + -- 获取并返回状态码 + local status = ngx.resp.get_status() + + -- 获取响应头 + local headers = ngx.resp.get_headers() + + return status, headers["Content-Type"] + ` + + err = coro.Execute(script) + require.NoError(t, err) + + // 验证最终状态 + assert.Equal(t, 201, ctx.Response.StatusCode()) + assert.Equal(t, "application/json", string(ctx.Response.Header.Peek("Content-Type"))) + assert.Equal(t, "custom-value", string(ctx.Response.Header.Peek("X-Custom-Header"))) + assert.Empty(t, ctx.Response.Header.Peek("X-Request-ID")) +} + +// TestNgxRespAPIErrorCases 测试错误处理 +func TestNgxRespAPIErrorCases(t *testing.T) { + engine, err := NewEngine(DefaultConfig()) + require.NoError(t, err) + defer engine.Close() + + coro, err := engine.NewCoroutine(nil) + require.NoError(t, err) + defer coro.Close() + + ctx := &fasthttp.RequestCtx{} + api := newNgxRespAPI(ctx) + RegisterNgxRespAPI(coro.Co, api) + + // 测试 set_status 缺少参数 + err = coro.Execute(`ngx.resp.set_status()`) + assert.Error(t, err) // 应该返回错误 + + // 测试 set_header 缺少参数 + err = coro.Execute(`ngx.resp.set_header("key")`) + assert.Error(t, err) // 应该返回错误 + + // 测试 clear_header 缺少参数 + err = coro.Execute(`ngx.resp.clear_header()`) + assert.Error(t, err) // 应该返回错误 +} + +// TestNgxRespAPIMultiValueHeaders 测试多值响应头 +func TestNgxRespAPIMultiValueHeaders(t *testing.T) { + engine, err := NewEngine(DefaultConfig()) + require.NoError(t, err) + defer engine.Close() + + coro, err := engine.NewCoroutine(nil) + require.NoError(t, err) + defer coro.Close() + + ctx := &fasthttp.RequestCtx{} + api := newNgxRespAPI(ctx) + RegisterNgxRespAPI(coro.Co, api) + + // 添加多值头(fasthttp 支持通过 Add 添加多值) + ctx.Response.Header.Add("Set-Cookie", "session=abc123") + ctx.Response.Header.Add("Set-Cookie", "user=john") + + // 测试获取多值头 + err = coro.Execute(` + local headers = ngx.resp.get_headers() + return type(headers["Set-Cookie"]) + `) + require.NoError(t, err) + // 多值头应该返回为 table 类型 +} + +// TestNgxRespAPIWithRealHTTP 测试真实 HTTP 上下文 +func TestNgxRespAPIWithRealHTTP(t *testing.T) { + // 这个测试需要完整的 fasthttp 上下文 + // 在实际集成测试中验证 + + engine, err := NewEngine(DefaultConfig()) + require.NoError(t, err) + defer engine.Close() + + // 创建协程 + coro, err := engine.NewCoroutine(nil) + require.NoError(t, err) + defer coro.Close() + + // 由于 fasthttp.RequestCtx 的复杂性, + // 这里仅验证 API 注册和基本调用不 panic + ctx := &fasthttp.RequestCtx{} + api := newNgxRespAPI(ctx) + RegisterNgxRespAPI(coro.Co, api) + + // 验证 ngx.resp 表存在 + err = coro.Execute(` + assert(type(ngx.resp) == "table", "ngx.resp should be a table") + assert(type(ngx.resp.get_status) == "function", "get_status should be a function") + assert(type(ngx.resp.set_status) == "function", "set_status should be a function") + assert(type(ngx.resp.get_headers) == "function", "get_headers should be a function") + assert(type(ngx.resp.set_header) == "function", "set_header should be a function") + assert(type(ngx.resp.clear_header) == "function", "clear_header should be a function") + `) + require.NoError(t, err) +}