lolly/internal/lua/api_location.go

260 lines
7.3 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.location API 实现。
//
// 该文件实现 ngx.location.capture 子请求功能,兼容 OpenResty/ngx_lua 语义。
// 支持:
// - 注册 location handler 映射
// - 执行子请求并捕获响应(状态码、响应头、响应体)
// - 选项控制:方法、请求体、自定义头、查询参数
//
// 注意事项:
// - 子请求复用父请求的数据(深拷贝请求体)
// - Scheduler 模式下 ngx.location.capture 不可用
//
// 作者xfy
package lua
import (
"strings"
"sync"
"github.com/valyala/fasthttp"
glua "github.com/yuin/gopher-lua"
)
// LocationCaptureResult 子请求捕获结果。
//
// 包含子请求的响应状态码、响应头和响应体数据。
type LocationCaptureResult struct {
// Headers 响应头映射
Headers map[string]string
// Body 响应体数据
Body []byte
// Status HTTP 状态码
Status int
}
// LocationManager location 管理器,用于注册和管理子请求 handler。
//
// 维护 location 路径到 fasthttp.RequestHandler 的映射,
// 支持并发安全的注册和捕获操作。
type LocationManager struct {
// handlers 路径到 handler 的映射
handlers map[string]fasthttp.RequestHandler
// mu 读写锁
mu sync.Mutex
}
// NewLocationManager 创建 location 管理器实例。
//
// 返回值:
// - *LocationManager: 初始化的管理器实例
func NewLocationManager() *LocationManager {
return &LocationManager{
handlers: make(map[string]fasthttp.RequestHandler),
}
}
// Register 注册 location handler。
//
// 参数:
// - location: 路径标识(如 "/api/internal"
// - handler: fasthttp 请求处理器
func (m *LocationManager) Register(location string, handler fasthttp.RequestHandler) {
m.mu.Lock()
defer m.mu.Unlock()
m.handlers[location] = handler
}
// Capture 执行子请求。
//
// 查找指定 location 的 handler创建子请求上下文复制父请求数据
// 应用选项(方法、请求体、头、查询参数),执行 handler 并收集响应。
//
// 参数:
// - parentCtx: 父请求上下文,用于数据复制
// - location: 目标 location 路径
// - opts: 可选参数映射method、body、headers、args
//
// 返回值:
// - *LocationCaptureResult: 子请求响应结果
// - error: 当前实现始终返回 nil
func (m *LocationManager) Capture(parentCtx *fasthttp.RequestCtx, location string, opts map[string]any) (*LocationCaptureResult, error) {
m.mu.Lock()
handler, ok := m.handlers[location]
m.mu.Unlock()
if !ok {
// location 不存在,返回 404
return &LocationCaptureResult{
Status: 404,
Body: []byte("location not found"),
Headers: map[string]string{},
}, nil
}
// 创建子请求上下文(不设置 Conn
subCtx := &fasthttp.RequestCtx{}
// 复制父请求作为基础(深拷贝)
parentCtx.Request.CopyTo(&subCtx.Request)
// 设置子请求的路径,保留父请求的查询参数
// 解析 location分离路径和查询参数
uri := subCtx.URI()
uri.SetPath(location)
// 应用选项
if opts != nil {
if method, ok := opts["method"].(string); ok {
subCtx.Request.Header.SetMethod(method)
}
if body, ok := opts["body"].(string); ok {
subCtx.Request.SetBodyString(body)
}
if headers, ok := opts["headers"].(map[string]string); ok {
for k, v := range headers {
subCtx.Request.Header.Set(k, v)
}
}
// 如果选项中显式指定了 args则覆盖父请求的查询参数
if args, ok := opts["args"].(map[string]string); ok {
uri.QueryArgs().Reset()
for k, v := range args {
uri.QueryArgs().Add(k, v)
}
}
}
// 执行 handler
handler(subCtx)
// 收集结果
result := &LocationCaptureResult{
Status: subCtx.Response.StatusCode(),
Body: subCtx.Response.Body(),
Headers: make(map[string]string),
}
// 收集响应头(使用 All 替代已弃用的 VisitAll
for key, value := range subCtx.Response.Header.All() {
result.Headers[string(key)] = string(value)
}
return result, nil
}
// getRequestCtx 从当前 Lua 协程的 UserData 中获取 RequestCtx
// 通过协程关联的 RequestCtx 实现子请求对父请求数据的访问
func getRequestCtx(L *glua.LState) *fasthttp.RequestCtx {
// 获取当前协程的上下文(在创建协程时通过 SetContext 设置)
if ctx := L.Context(); ctx != nil {
if reqCtx, ok := ctx.(*fasthttp.RequestCtx); ok {
return reqCtx
}
}
return nil
}
// RegisterLocationAPI 注册 ngx.location API 到 Lua 状态机。
//
// 在 ngx 表下创建 location 子表,注册 capture 方法用于执行子请求。
//
// 参数:
// - L: Lua 状态
// - manager: location 管理器实例
// - ngx: ngx 全局表
func RegisterLocationAPI(L *glua.LState, manager *LocationManager, ngx *glua.LTable) {
// 创建 ngx.location 表
location := L.NewTable()
// ngx.location.capture(uri, options?)
L.SetField(location, "capture", L.NewFunction(func(L *glua.LState) int {
uri := L.CheckString(1)
// 解析选项
opts := make(map[string]any)
if L.GetTop() >= 2 {
optionsTable := L.CheckTable(2)
optionsTable.ForEach(func(key, value glua.LValue) {
keyStr := glua.LVAsString(key)
//nolint:exhaustive // 只处理特定类型
switch value.Type() {
case glua.LTString:
opts[keyStr] = glua.LVAsString(value)
case glua.LTNumber:
opts[keyStr] = float64(glua.LVAsNumber(value))
case glua.LTTable:
// 处理 headers 表
if keyStr == "headers" {
headers := make(map[string]string)
// ForEach 不返回错误,但在类型断言前需要检查
tbl, ok := value.(*glua.LTable)
if !ok {
return // 跳过当前 ForEach 回调
}
tbl.ForEach(func(hKey, hValue glua.LValue) {
headers[glua.LVAsString(hKey)] = glua.LVAsString(hValue)
})
opts[keyStr] = headers
}
default:
// 其他类型不处理
}
})
}
// 创建结果表
result := L.NewTable()
if manager == nil {
// manager 未初始化
L.SetField(result, "status", glua.LNumber(500))
L.SetField(result, "body", glua.LString("location manager not initialized"))
L.Push(result)
return 1
}
// 获取父请求上下文(从当前协程)
parentCtx := getRequestCtx(L)
if parentCtx == nil {
// 没有父请求上下文,使用模拟上下文
mockCtx := &fasthttp.RequestCtx{}
mockCtx.Request.SetRequestURI(uri)
parentCtx = mockCtx
}
// 执行子请求,传递父请求上下文用于数据复制
captureResult, err := manager.Capture(parentCtx, uri, opts)
if err == nil && captureResult != nil {
L.SetField(result, "status", glua.LNumber(captureResult.Status))
L.SetField(result, "body", glua.LString(string(captureResult.Body)))
// 设置 headers
headersTable := headersToLuaTable(L, captureResult.Headers)
L.SetField(result, "headers", headersTable)
} else {
// 执行失败
L.SetField(result, "status", glua.LNumber(500))
L.SetField(result, "body", glua.LString("subrequest failed: "+err.Error()))
}
L.Push(result)
return 1
}))
L.SetField(ngx, "location", location)
}
// headersToLuaTable 将 headers 转为 Lua 表
func headersToLuaTable(L *glua.LState, headers map[string]string) *glua.LTable {
table := L.NewTable()
for k, v := range headers {
// 转换为小写键名nginx 风格)
table.RawSetString(strings.ToLower(k), glua.LString(v))
}
return table
}