mirror of
https://github.com/aykhans/sarin.git
synced 2026-10-12 20:58:52 +00:00
fasthttp allocates the body from the declared Content-Length before reading it, so a server could exhaust memory by declaring a size it never sends. The new max response body config bounds every response, defaults to 10MiB and accepts sizes like 10MiB or 2MB. Scripts get the same as a maxBodySize option on http().
150 lines
4.1 KiB
Go
150 lines
4.1 KiB
Go
package script
|
|
|
|
import (
|
|
lua "github.com/yuin/gopher-lua"
|
|
"go.aykhans.me/sarin/internal/types"
|
|
)
|
|
|
|
// registerHTTP defines the global http function.
|
|
func (e *LuaEngine) registerHTTP() {
|
|
L := e.state
|
|
L.SetGlobal("http", L.NewFunction(func(L *lua.LState) int {
|
|
return e.httpCall(L, 1)
|
|
}))
|
|
}
|
|
|
|
// httpCall sends the request whose URL is at stack position urlIndex and pushes the response.
|
|
func (e *LuaEngine) httpCall(state *lua.LState, urlIndex int) int {
|
|
req := &HTTPRequest{
|
|
Method: httpDefaultMethod,
|
|
URL: state.CheckString(urlIndex),
|
|
}
|
|
|
|
switch opts := state.Get(urlIndex + 1).(type) {
|
|
case *lua.LNilType:
|
|
case *lua.LTable:
|
|
if err := e.parseHTTPOptions(opts, req); err != nil {
|
|
state.RaiseError("%s", err.Error())
|
|
}
|
|
default:
|
|
state.ArgError(urlIndex+1, "options must be a table")
|
|
}
|
|
|
|
resp, err := e.http.do(req)
|
|
if err != nil {
|
|
state.RaiseError("%s", err.Error())
|
|
}
|
|
|
|
state.Push(httpResponseToTable(state, resp))
|
|
return 1
|
|
}
|
|
|
|
// parseHTTPOptions fills req from a Lua options table.
|
|
// It can return the following errors:
|
|
// - types.ScriptHTTPOptionError
|
|
func (e *LuaEngine) parseHTTPOptions(opts *lua.LTable, req *HTTPRequest) error {
|
|
var err error
|
|
opts.ForEach(func(k, v lua.LValue) {
|
|
if err != nil {
|
|
return
|
|
}
|
|
|
|
key, ok := k.(lua.LString)
|
|
if !ok {
|
|
err = types.NewScriptHTTPOptionError(k.String(), types.ErrScriptHTTPUnknownOption)
|
|
return
|
|
}
|
|
|
|
switch name := string(key); name {
|
|
case httpOptionMethod:
|
|
method, ok := v.(lua.LString)
|
|
if !ok {
|
|
err = types.NewScriptHTTPOptionError(name, types.NewScriptTypeError("string", v.Type().String()))
|
|
return
|
|
}
|
|
req.Method = string(method)
|
|
case httpOptionHeaders, httpOptionParams, httpOptionCookies:
|
|
table, ok := v.(*lua.LTable)
|
|
if !ok {
|
|
err = types.NewScriptHTTPOptionError(name, types.NewScriptTypeError("table", v.Type().String()))
|
|
return
|
|
}
|
|
values := e.tableToStringSliceMap(table)
|
|
switch name {
|
|
case httpOptionHeaders:
|
|
req.Headers = values
|
|
case httpOptionParams:
|
|
req.Params = values
|
|
default:
|
|
req.Cookies = values
|
|
}
|
|
case httpOptionBody:
|
|
body, ok := v.(lua.LString)
|
|
if !ok {
|
|
err = types.NewScriptHTTPOptionError(name, types.NewScriptTypeError("string", v.Type().String()))
|
|
return
|
|
}
|
|
req.Body = string(body)
|
|
case httpOptionTimeout:
|
|
timeout, ok := v.(lua.LString)
|
|
if !ok {
|
|
err = types.NewScriptHTTPOptionError(name, types.NewScriptTypeError("duration string", v.Type().String()))
|
|
return
|
|
}
|
|
req.Timeout, err = parseHTTPTimeout(string(timeout))
|
|
case httpOptionInsecure:
|
|
flag, ok := v.(lua.LBool)
|
|
if !ok {
|
|
err = types.NewScriptHTTPOptionError(name, types.NewScriptTypeError("boolean", v.Type().String()))
|
|
return
|
|
}
|
|
req.Insecure = bool(flag)
|
|
case httpOptionMaxRedirects:
|
|
count, ok := v.(lua.LNumber)
|
|
if !ok {
|
|
err = types.NewScriptHTTPOptionError(name, types.NewScriptTypeError("number", v.Type().String()))
|
|
return
|
|
}
|
|
req.MaxRedirects, err = parseHTTPMaxRedirects(float64(count))
|
|
case httpOptionMaxBodySize:
|
|
size, ok := v.(lua.LNumber)
|
|
if !ok {
|
|
err = types.NewScriptHTTPOptionError(name, types.NewScriptTypeError("number", v.Type().String()))
|
|
return
|
|
}
|
|
req.MaxBodySize, err = parseHTTPMaxBodySize(float64(size))
|
|
default:
|
|
err = types.NewScriptHTTPOptionError(name, types.ErrScriptHTTPUnknownOption)
|
|
}
|
|
})
|
|
return err
|
|
}
|
|
|
|
// httpResponseToTable converts a response to a Lua table.
|
|
func httpResponseToTable(state *lua.LState, resp *HTTPResponse) *lua.LTable {
|
|
t := state.NewTable()
|
|
t.RawSetString("status", lua.LNumber(resp.Status))
|
|
t.RawSetString("headers", stringSliceMapToTable(state, resp.Headers))
|
|
t.RawSetString("body", lua.LString(resp.Body))
|
|
|
|
lookup := func(find func(string) (string, bool)) *lua.LFunction {
|
|
return state.NewFunction(func(L *lua.LState) int {
|
|
// Last argument, so both res:header() and res.header() work.
|
|
top := L.GetTop()
|
|
if top == 0 {
|
|
L.ArgError(1, "name expected")
|
|
}
|
|
if value, ok := find(L.CheckString(top)); ok {
|
|
L.Push(lua.LString(value))
|
|
} else {
|
|
L.Push(lua.LNil)
|
|
}
|
|
return 1
|
|
})
|
|
}
|
|
t.RawSetString("header", lookup(resp.Header))
|
|
t.RawSetString("cookie", lookup(resp.Cookie))
|
|
|
|
return t
|
|
}
|