Fast and extensible multi-platform HTTP/1-2-3 web server with automatic HTTPS
Mirrored from https://github.com/Slazee/caddy.git
Synced Nov 14, 05:15 UTC
http: Add Starlark handler
This migrates a feature that was previously reserved for enterprise users, according to #2786. The Starlark integration needs to be updated since this was made before some significant changes in the v2 code base. When functional, it makes it possible to have very dynamic HTTP handlers. This will be a long-term ongoing project. Credit to Danny Navarro
Matthew Holt committed
Oct 10, 2019 at 11:02 UTC
26cc8837084f9cea6057e9908f0b5bde0eb15d3e
5 files changed
+397
cmd/caddy/main.go
+1
@@ -34,6 +34,7 @@ import (
34
_ "github.com/caddyserver/caddy/v2/modules/caddyhttp/reverseproxy"
35
_ "github.com/caddyserver/caddy/v2/modules/caddyhttp/reverseproxy/fastcgi"
36
_ "github.com/caddyserver/caddy/v2/modules/caddyhttp/rewrite"
37
+ _ "github.com/caddyserver/caddy/v2/modules/caddyhttp/starlarkmw"
38
_ "github.com/caddyserver/caddy/v2/modules/caddyhttp/templates"
39
_ "github.com/caddyserver/caddy/v2/modules/caddytls"
40
_ "github.com/caddyserver/caddy/v2/modules/caddytls/standardstek"
modules/caddyhttp/starlarkmw/example/caddy.json
new
+19
@@ -0,0 +1,19 @@
1
+{
2
+ "apps": {
3
+ "http": {
4
+ "servers": {
5
+ "MY_SERVER": {
6
+ "listen": [":3001"],
7
+ "routes": [
8
+ {
9
+ "handle": {
10
+ "handler": "starlark",
11
+ "script": "def setup(r):\n\t# create some middlewares specific to this request\n\ttemplates = loadModule('http.handlers.templates', {'include_root': './includes'})\n\tmidChain = execute([templates])\n\ndef serveHTTP (rw, r):\n\trw.Write('Hello world, from Starlark!')\n"
12
+ }
13
+ }
14
+ ]
15
+ }
16
+ }
17
+ }
18
+ }
19
+}
\ No newline at end of file
modules/caddyhttp/starlarkmw/internal/lib/module.go
new
+165
@@ -0,0 +1,165 @@
1
+package lib
2
+
3
+import (
4
+ "encoding/json"
5
+ "fmt"
6
+ "strings"
7
+
8
+ "github.com/caddyserver/caddy/v2/modules/caddyhttp"
9
+
10
+ "github.com/caddyserver/caddy/v2"
11
+ "go.starlark.net/starlark"
12
+)
13
+
14
+// ResponderModule represents a module that satisfies the caddyhttp handler.
15
+type ResponderModule struct {
16
+ Name string
17
+ Cfg json.RawMessage
18
+ Instance caddyhttp.Handler
19
+}
20
+
21
+func (r ResponderModule) Freeze() {}
22
+func (r ResponderModule) Hash() (uint32, error) { return 0, fmt.Errorf("unhashable: responder module") }
23
+func (r ResponderModule) String() string { return "responder module" }
24
+func (r ResponderModule) Type() string { return "responder module" }
25
+func (r ResponderModule) Truth() starlark.Bool { return true }
26
+
27
+// Middleware represents a module that satisfies the starlark Value interface.
28
+type Middleware struct {
29
+ Name string
30
+ Cfg json.RawMessage
31
+ Instance caddyhttp.MiddlewareHandler
32
+}
33
+
34
+func (r Middleware) Freeze() {}
35
+func (r Middleware) Hash() (uint32, error) { return 0, fmt.Errorf("unhashable: middleware") }
36
+func (r Middleware) String() string { return "middleware" }
37
+func (r Middleware) Type() string { return "middleware" }
38
+func (r Middleware) Truth() starlark.Bool { return true }
39
+
40
+// LoadMiddleware represents the method exposed to starlark to load a Caddy module.
41
+type LoadMiddleware struct {
42
+ Middleware Middleware
43
+ Ctx caddy.Context
44
+}
45
+
46
+func (r LoadMiddleware) Freeze() {}
47
+func (r LoadMiddleware) Hash() (uint32, error) { return 0, fmt.Errorf("unhashable: loadMiddleware") }
48
+func (r LoadMiddleware) String() string { return "loadMiddleware" }
49
+func (r LoadMiddleware) Type() string { return "function: loadMiddleware" }
50
+func (r LoadMiddleware) Truth() starlark.Bool { return true }
51
+
52
+// Run is the method bound to the starlark loadMiddleware function.
53
+func (r *LoadMiddleware) Run(thread *starlark.Thread, fn *starlark.Builtin, args starlark.Tuple, kwargs []starlark.Tuple) (starlark.Value, error) {
54
+ var name string
55
+ var cfg *starlark.Dict
56
+ err := starlark.UnpackPositionalArgs(fn.Name(), args, kwargs, 2, &name, &cfg)
57
+ if err != nil {
58
+ return starlark.None, fmt.Errorf("unpacking arguments: %v", err.Error())
59
+ }
60
+
61
+ js := json.RawMessage(cfg.String())
62
+
63
+ if strings.Index(name, "http.handlers.") == -1 {
64
+ name = fmt.Sprintf("http.handlers.%s", name)
65
+ }
66
+
67
+ inst, err := r.Ctx.LoadModule(name, js)
68
+ if err != nil {
69
+ return starlark.None, err
70
+ }
71
+
72
+ mid, ok := inst.(caddyhttp.MiddlewareHandler)
73
+ if !ok {
74
+ return starlark.None, fmt.Errorf("could not assert as middleware handler")
75
+ }
76
+
77
+ m := Middleware{
78
+ Name: name,
79
+ Cfg: js,
80
+ Instance: mid,
81
+ }
82
+
83
+ r.Middleware = m
84
+
85
+ return m, nil
86
+}
87
+
88
+// LoadResponder represents the method exposed to starlark to load a Caddy middleware responder.
89
+type LoadResponder struct {
90
+ Module ResponderModule
91
+ Ctx caddy.Context
92
+}
93
+
94
+func (r LoadResponder) Freeze() {}
95
+func (r LoadResponder) Hash() (uint32, error) { return 0, fmt.Errorf("unhashable: loadModule") }
96
+func (r LoadResponder) String() string { return "loadModule" }
97
+func (r LoadResponder) Type() string { return "function: loadModule" }
98
+func (r LoadResponder) Truth() starlark.Bool { return true }
99
+
100
+// Run is the method bound to the starlark loadResponder function.
101
+func (r *LoadResponder) Run(thread *starlark.Thread, fn *starlark.Builtin, args starlark.Tuple, kwargs []starlark.Tuple) (starlark.Value, error) {
102
+ var name string
103
+ var cfg *starlark.Dict
104
+ err := starlark.UnpackPositionalArgs(fn.Name(), args, kwargs, 2, &name, &cfg)
105
+ if err != nil {
106
+ return starlark.None, fmt.Errorf("unpacking arguments: %v", err.Error())
107
+ }
108
+
109
+ js := json.RawMessage(cfg.String())
110
+
111
+ if strings.Index(name, "http.handlers.") == -1 {
112
+ name = fmt.Sprintf("http.handlers.%s", name)
113
+ }
114
+
115
+ inst, err := r.Ctx.LoadModule(name, js)
116
+ if err != nil {
117
+ return starlark.None, err
118
+ }
119
+
120
+ res, ok := inst.(caddyhttp.Handler)
121
+ if !ok {
122
+ return starlark.None, fmt.Errorf("could not assert as responder")
123
+ }
124
+
125
+ m := ResponderModule{
126
+ Name: name,
127
+ Cfg: js,
128
+ Instance: res,
129
+ }
130
+
131
+ r.Module = m
132
+
133
+ return m, nil
134
+}
135
+
136
+// Execute represents the method exposed to starlark to build a middleware chain.
137
+type Execute struct {
138
+ Modules []Middleware
139
+}
140
+
141
+func (r Execute) Freeze() {}
142
+func (r Execute) Hash() (uint32, error) { return 0, fmt.Errorf("unhashable: execute") }
143
+func (r Execute) String() string { return "execute" }
144
+func (r Execute) Type() string { return "function: execute" }
145
+func (r Execute) Truth() starlark.Bool { return true }
146
+
147
+// Run is the method bound to the starlark execute function.
148
+func (r *Execute) Run(thread *starlark.Thread, fn *starlark.Builtin, args starlark.Tuple, kwargs []starlark.Tuple) (starlark.Value, error) {
149
+ var mids *starlark.List
150
+ err := starlark.UnpackPositionalArgs(fn.Name(), args, kwargs, 1, &mids)
151
+ if err != nil {
152
+ return starlark.None, fmt.Errorf("unpacking arguments: %v", err.Error())
153
+ }
154
+
155
+ for i := 0; i < mids.Len(); i++ {
156
+ val, ok := mids.Index(i).(Middleware)
157
+ if !ok {
158
+ return starlark.None, fmt.Errorf("cannot get module from execute")
159
+ }
160
+
161
+ r.Modules = append(r.Modules, val)
162
+ }
163
+
164
+ return starlark.None, nil
165
+}
modules/caddyhttp/starlarkmw/starlarkmw.go
new
+172
@@ -0,0 +1,172 @@
1
+package starlarkmw
2
+
3
+import (
4
+ "context"
5
+ "fmt"
6
+ "net/http"
7
+
8
+ "github.com/caddyserver/caddy/v2"
9
+ "github.com/caddyserver/caddy/v2/modules/caddyhttp"
10
+ caddyscript "github.com/caddyserver/caddy/v2/pkg/caddyscript/lib"
11
+ "github.com/caddyserver/caddy/v2/modules/caddyhttp/starlarkmw/internal/lib"
12
+ "github.com/starlight-go/starlight/convert"
13
+ "go.starlark.net/starlark"
14
+)
15
+
16
+func init() {
17
+ caddy.RegisterModule(StarlarkMW{})
18
+}
19
+
20
+// StarlarkMW represents a middleware responder written in starlark
21
+type StarlarkMW struct {
22
+ Script string `json:"script"`
23
+ serveHTTP *starlark.Function
24
+ setup *starlark.Function
25
+ thread *starlark.Thread
26
+ loadMiddleware *lib.LoadMiddleware
27
+ execute *lib.Execute
28
+ globals *starlark.StringDict
29
+ gctx caddy.Context
30
+ rctx caddy.Context
31
+ rcancel context.CancelFunc
32
+}
33
+
34
+// CaddyModule returns the Caddy module information.
35
+func (StarlarkMW) CaddyModule() caddy.ModuleInfo {
36
+ return caddy.ModuleInfo{
37
+ Name: "http.handlers.starlark",
38
+ New: func() caddy.Module { return new(StarlarkMW) },
39
+ }
40
+}
41
+
42
+// ServeHTTP responds to an http request with starlark.
43
+func (s *StarlarkMW) ServeHTTP(w http.ResponseWriter, r *http.Request) error {
44
+ var mwcancel context.CancelFunc
45
+ var mwctx caddy.Context
46
+
47
+ // call setup() to prepare the middleware chain if it is defined
48
+ if s.setup != nil {
49
+ mwctx, mwcancel = caddy.NewContext(s.gctx)
50
+ defer mwcancel()
51
+
52
+ s.loadMiddleware.Ctx = mwctx
53
+ args := starlark.Tuple{caddyscript.HTTPRequest{Req: r}}
54
+
55
+ _, err := starlark.Call(new(starlark.Thread), s.setup, args, nil)
56
+ if err != nil {
57
+ return fmt.Errorf("starlark setup(), %v", err)
58
+ }
59
+ }
60
+
61
+ // dynamically build middleware chain for each request
62
+ stack := caddyhttp.HandlerFunc(func(w http.ResponseWriter, r *http.Request) error {
63
+ wr, err := convert.ToValue(w)
64
+ if err != nil {
65
+ return fmt.Errorf("cannot convert response writer to starlark value")
66
+ }
67
+
68
+ args := starlark.Tuple{wr, caddyscript.HTTPRequest{Req: r}}
69
+ v, err := starlark.Call(new(starlark.Thread), s.serveHTTP, args, nil)
70
+ if err != nil {
71
+ return fmt.Errorf("starlark serveHTTP(), %v", err)
72
+ }
73
+
74
+ // if a responder type was returned from starlark we should run it otherwise it
75
+ // is expected to handle the request
76
+ if resp, ok := v.(lib.ResponderModule); ok {
77
+ return resp.Instance.ServeHTTP(w, r)
78
+ }
79
+
80
+ return nil
81
+ })
82
+
83
+ // TODO :- make middlewareResponseWriter exported and wrap w with that
84
+ var mid []caddyhttp.Middleware
85
+ for _, m := range s.execute.Modules {
86
+ mid = append(mid, func(next caddyhttp.HandlerFunc) caddyhttp.HandlerFunc {
87
+ return func(w http.ResponseWriter, r *http.Request) error {
88
+ return m.Instance.ServeHTTP(w, r, next)
89
+ }
90
+ })
91
+ }
92
+
93
+ for i := len(mid) - 1; i >= 0; i-- {
94
+ stack = mid[i](stack)
95
+ }
96
+
97
+ s.execute.Modules = nil
98
+
99
+ return stack(w, r)
100
+}
101
+
102
+// Cleanup cleans up any modules loaded during the creation of a starlark route.
103
+func (s *StarlarkMW) Cleanup() error {
104
+ s.rcancel()
105
+ return nil
106
+}
107
+
108
+// Provision sets up the starlark env.
109
+func (s *StarlarkMW) Provision(ctx caddy.Context) error {
110
+ // store global context
111
+ s.gctx = ctx
112
+
113
+ // setup context for cleaning up any modules loaded during starlark script parsing phase
114
+ rctx, cancel := caddy.NewContext(ctx)
115
+ s.rcancel = cancel
116
+
117
+ // setup starlark global env
118
+ env := make(starlark.StringDict)
119
+ loadMiddleware := lib.LoadMiddleware{}
120
+ loadResponder := lib.LoadResponder{
121
+ Ctx: rctx,
122
+ }
123
+ execute := lib.Execute{}
124
+
125
+ lr := starlark.NewBuiltin("loadResponder", loadResponder.Run)
126
+ lr = lr.BindReceiver(&loadResponder)
127
+ env["loadResponder"] = lr
128
+
129
+ lm := starlark.NewBuiltin("loadMiddleware", loadMiddleware.Run)
130
+ lm = lm.BindReceiver(&loadMiddleware)
131
+ env["loadMiddleware"] = lm
132
+
133
+ ex := starlark.NewBuiltin("execute", execute.Run)
134
+ ex = ex.BindReceiver(&execute)
135
+ env["execute"] = ex
136
+
137
+ // import caddyscript lib
138
+ env["time"] = caddyscript.Time{}
139
+ env["regexp"] = caddyscript.Regexp{}
140
+
141
+ // configure starlark
142
+ thread := new(starlark.Thread)
143
+ s.thread = thread
144
+
145
+ // run starlark script
146
+ globals, err := starlark.ExecFile(thread, "", s.Script, env)
147
+ if err != nil {
148
+ return fmt.Errorf("starlark exec file: %v", err.Error())
149
+ }
150
+
151
+ // extract defined methods to setup middleware chain and responder, setup is not required
152
+ var setup *starlark.Function
153
+ if _, ok := globals["setup"]; ok {
154
+ setup, ok = globals["setup"].(*starlark.Function)
155
+ if !ok {
156
+ return fmt.Errorf("setup function not defined in starlark script")
157
+ }
158
+ }
159
+
160
+ serveHTTP, ok := globals["serveHTTP"].(*starlark.Function)
161
+ if !ok {
162
+ return fmt.Errorf("serveHTTP function not defined in starlark script")
163
+ }
164
+
165
+ s.setup = setup
166
+ s.serveHTTP = serveHTTP
167
+ s.loadMiddleware = &loadMiddleware
168
+ s.execute = &execute
169
+ s.globals = &globals
170
+
171
+ return nil
172
+}
modules/caddyhttp/starlarkmw/tools/gen/example.star
new
+40
@@ -0,0 +1,40 @@
1
+# any module that provisions resources
2
+proxyConfig = {
3
+ 'load_balance_type': 'round_robin',
4
+ 'upstreams': [
5
+ {
6
+ 'host': 'http://localhost:8080',
7
+ 'circuit_breaker': {
8
+ 'type': 'status_ratio',
9
+ 'threshold': 0.5
10
+ }
11
+ },
12
+ {
13
+ 'host': 'http://localhost:8081'
14
+ }
15
+ ]
16
+}
17
+
18
+sfConfig = {
19
+ 'root': '/Users/dev/Desktop',
20
+ 'browse': {},
21
+}
22
+
23
+proxy = loadResponder('reverse_proxy', proxyConfig)
24
+static_files = loadResponder('file_server', sfConfig)
25
+
26
+def setup(r):
27
+ # create some middlewares specific to this request
28
+ mid = []
29
+
30
+ if r.query.get('log') == 'true':
31
+ logMid = loadMiddleware('log', {'file': 'access.log'})
32
+ mid.append(logMid)
33
+
34
+ execute(mid)
35
+
36
+def serveHTTP(w, r):
37
+ if r.url.find('static') > 0:
38
+ return static_files
39
+
40
+ return proxy