package starlarkmw import ( "context" "fmt" "net/http" "github.com/caddyserver/caddy/v2" "github.com/caddyserver/caddy/v2/modules/caddyhttp" caddyscript "github.com/caddyserver/caddy/v2/pkg/caddyscript/lib" "github.com/caddyserver/caddy/v2/modules/caddyhttp/starlarkmw/internal/lib" "github.com/starlight-go/starlight/convert" "go.starlark.net/starlark" ) func init() { caddy.RegisterModule(StarlarkMW{}) } // StarlarkMW represents a middleware responder written in starlark type StarlarkMW struct { Script string `json:"script"` serveHTTP *starlark.Function setup *starlark.Function thread *starlark.Thread loadMiddleware *lib.LoadMiddleware execute *lib.Execute globals *starlark.StringDict gctx caddy.Context rctx caddy.Context rcancel context.CancelFunc } // CaddyModule returns the Caddy module information. func (StarlarkMW) CaddyModule() caddy.ModuleInfo { return caddy.ModuleInfo{ Name: "http.handlers.starlark", New: func() caddy.Module { return new(StarlarkMW) }, } } // ServeHTTP responds to an http request with starlark. func (s *StarlarkMW) ServeHTTP(w http.ResponseWriter, r *http.Request) error { var mwcancel context.CancelFunc var mwctx caddy.Context // call setup() to prepare the middleware chain if it is defined if s.setup != nil { mwctx, mwcancel = caddy.NewContext(s.gctx) defer mwcancel() s.loadMiddleware.Ctx = mwctx args := starlark.Tuple{caddyscript.HTTPRequest{Req: r}} _, err := starlark.Call(new(starlark.Thread), s.setup, args, nil) if err != nil { return fmt.Errorf("starlark setup(), %v", err) } } // dynamically build middleware chain for each request stack := caddyhttp.HandlerFunc(func(w http.ResponseWriter, r *http.Request) error { wr, err := convert.ToValue(w) if err != nil { return fmt.Errorf("cannot convert response writer to starlark value") } args := starlark.Tuple{wr, caddyscript.HTTPRequest{Req: r}} v, err := starlark.Call(new(starlark.Thread), s.serveHTTP, args, nil) if err != nil { return fmt.Errorf("starlark serveHTTP(), %v", err) } // if a responder type was returned from starlark we should run it otherwise it // is expected to handle the request if resp, ok := v.(lib.ResponderModule); ok { return resp.Instance.ServeHTTP(w, r) } return nil }) // TODO :- make middlewareResponseWriter exported and wrap w with that var mid []caddyhttp.Middleware for _, m := range s.execute.Modules { mid = append(mid, func(next caddyhttp.HandlerFunc) caddyhttp.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) error { return m.Instance.ServeHTTP(w, r, next) } }) } for i := len(mid) - 1; i >= 0; i-- { stack = mid[i](stack) } s.execute.Modules = nil return stack(w, r) } // Cleanup cleans up any modules loaded during the creation of a starlark route. func (s *StarlarkMW) Cleanup() error { s.rcancel() return nil } // Provision sets up the starlark env. func (s *StarlarkMW) Provision(ctx caddy.Context) error { // store global context s.gctx = ctx // setup context for cleaning up any modules loaded during starlark script parsing phase rctx, cancel := caddy.NewContext(ctx) s.rcancel = cancel // setup starlark global env env := make(starlark.StringDict) loadMiddleware := lib.LoadMiddleware{} loadResponder := lib.LoadResponder{ Ctx: rctx, } execute := lib.Execute{} lr := starlark.NewBuiltin("loadResponder", loadResponder.Run) lr = lr.BindReceiver(&loadResponder) env["loadResponder"] = lr lm := starlark.NewBuiltin("loadMiddleware", loadMiddleware.Run) lm = lm.BindReceiver(&loadMiddleware) env["loadMiddleware"] = lm ex := starlark.NewBuiltin("execute", execute.Run) ex = ex.BindReceiver(&execute) env["execute"] = ex // import caddyscript lib env["time"] = caddyscript.Time{} env["regexp"] = caddyscript.Regexp{} // configure starlark thread := new(starlark.Thread) s.thread = thread // run starlark script globals, err := starlark.ExecFile(thread, "", s.Script, env) if err != nil { return fmt.Errorf("starlark exec file: %v", err.Error()) } // extract defined methods to setup middleware chain and responder, setup is not required var setup *starlark.Function if _, ok := globals["setup"]; ok { setup, ok = globals["setup"].(*starlark.Function) if !ok { return fmt.Errorf("setup function not defined in starlark script") } } serveHTTP, ok := globals["serveHTTP"].(*starlark.Function) if !ok { return fmt.Errorf("serveHTTP function not defined in starlark script") } s.setup = setup s.serveHTTP = serveHTTP s.loadMiddleware = &loadMiddleware s.execute = &execute s.globals = &globals return nil }