diff options
Diffstat (limited to 'modules/caddyhttp')
-rw-r--r-- | modules/caddyhttp/httpcache/httpcache.go | 205 | ||||
-rw-r--r-- | modules/caddyhttp/reverseproxy/circuitbreaker.go | 152 | ||||
-rw-r--r-- | modules/caddyhttp/starlarkmw/example/caddy.json | 19 | ||||
-rw-r--r-- | modules/caddyhttp/starlarkmw/internal/lib/module.go | 165 | ||||
-rw-r--r-- | modules/caddyhttp/starlarkmw/starlarkmw.go | 172 | ||||
-rw-r--r-- | modules/caddyhttp/starlarkmw/tools/gen/example.star | 40 |
6 files changed, 753 insertions, 0 deletions
diff --git a/modules/caddyhttp/httpcache/httpcache.go b/modules/caddyhttp/httpcache/httpcache.go new file mode 100644 index 0000000..1b2cfd2 --- /dev/null +++ b/modules/caddyhttp/httpcache/httpcache.go @@ -0,0 +1,205 @@ +// Copyright 2015 Matthew Holt and The Caddy Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package httpcache + +import ( + "bytes" + "encoding/gob" + "fmt" + "io" + "log" + "net/http" + "sync" + + "github.com/caddyserver/caddy/v2" + "github.com/caddyserver/caddy/v2/modules/caddyhttp" + "github.com/golang/groupcache" +) + +func init() { + caddy.RegisterModule(Cache{}) +} + +// Cache implements a simple distributed cache. +type Cache struct { + group *groupcache.Group +} + +// CaddyModule returns the Caddy module information. +func (Cache) CaddyModule() caddy.ModuleInfo { + return caddy.ModuleInfo{ + Name: "http.handlers.cache", + New: func() caddy.Module { return new(Cache) }, + } +} + +// Provision provisions c. +func (c *Cache) Provision(ctx caddy.Context) error { + // TODO: proper pool configuration + me := "http://localhost:5555" + // TODO: Make max size configurable + maxSize := int64(512 << 20) + poolMu.Lock() + if pool == nil { + pool = groupcache.NewHTTPPool(me) + c.group = groupcache.NewGroup(groupName, maxSize, groupcache.GetterFunc(c.getter)) + } else { + c.group = groupcache.GetGroup(groupName) + } + pool.Set(me) + poolMu.Unlock() + + return nil +} + +// Validate validates c. +func (c *Cache) Validate() error { + // TODO: implement + return nil +} + +func (c *Cache) ServeHTTP(w http.ResponseWriter, r *http.Request, next caddyhttp.Handler) error { + // TODO: proper RFC implementation of cache control headers... + if r.Header.Get("Cache-Control") == "no-cache" || r.Method != "GET" { + return next.ServeHTTP(w, r) + } + + ctx := getterContext{w, r, next} + + // TODO: rigorous performance testing + + // TODO: pretty much everything else to handle the nuances of HTTP caching... + + // TODO: groupcache has no explicit cache eviction, so we need to embed + // all information related to expiring cache entries into the key; right + // now we just use the request URI as a proof-of-concept + key := r.RequestURI + + var cachedBytes []byte + err := c.group.Get(ctx, key, groupcache.AllocatingByteSliceSink(&cachedBytes)) + if err == errUncacheable { + return nil + } + if err != nil { + return err + } + + // the cached bytes consists of two parts: first a + // gob encoding of the status and header, immediately + // followed by the raw bytes of the response body + rdr := bytes.NewReader(cachedBytes) + + // read the header and status first + var hs headerAndStatus + err = gob.NewDecoder(rdr).Decode(&hs) + if err != nil { + return err + } + + // set and write the cached headers + for k, v := range hs.Header { + w.Header()[k] = v + } + w.WriteHeader(hs.Status) + + // write the cached response body + io.Copy(w, rdr) + + return nil +} + +func (c *Cache) getter(ctx groupcache.Context, key string, dest groupcache.Sink) error { + combo := ctx.(getterContext) + + // the buffer will store the gob-encoded header, then the body + buf := bufPool.Get().(*bytes.Buffer) + buf.Reset() + defer bufPool.Put(buf) + + // we need to record the response if we are to cache it; only cache if + // request is successful (TODO: there's probably much more nuance needed here) + var rr caddyhttp.ResponseRecorder + rr = caddyhttp.NewResponseRecorder(combo.rw, buf, func(status int) bool { + shouldBuf := status < 300 + + if shouldBuf { + // store the header before the body, so we can efficiently + // and conveniently use a single buffer for both; gob + // decoder will only read up to end of gob message, and + // the rest will be the body, which will be written + // implicitly for us by the recorder + err := gob.NewEncoder(buf).Encode(headerAndStatus{ + Header: rr.Header(), + Status: status, + }) + if err != nil { + log.Printf("[ERROR] Encoding headers for cache entry: %v; not caching this request", err) + return false + } + } + + return shouldBuf + }) + + // execute next handlers in chain + err := combo.next.ServeHTTP(rr, combo.req) + if err != nil { + return err + } + + // if response body was not buffered, response was + // already written and we are unable to cache + if !rr.Buffered() { + return errUncacheable + } + + // add to cache + dest.SetBytes(buf.Bytes()) + + return nil +} + +type headerAndStatus struct { + Header http.Header + Status int +} + +type getterContext struct { + rw http.ResponseWriter + req *http.Request + next caddyhttp.Handler +} + +var bufPool = sync.Pool{ + New: func() interface{} { + return new(bytes.Buffer) + }, +} + +var ( + pool *groupcache.HTTPPool + poolMu sync.Mutex +) + +var errUncacheable = fmt.Errorf("uncacheable") + +const groupName = "http_requests" + +// Interface guards +var ( + _ caddy.Provisioner = (*Cache)(nil) + _ caddy.Validator = (*Cache)(nil) + _ caddyhttp.MiddlewareHandler = (*Cache)(nil) +) diff --git a/modules/caddyhttp/reverseproxy/circuitbreaker.go b/modules/caddyhttp/reverseproxy/circuitbreaker.go new file mode 100644 index 0000000..de2a6f9 --- /dev/null +++ b/modules/caddyhttp/reverseproxy/circuitbreaker.go @@ -0,0 +1,152 @@ +// Copyright 2015 Matthew Holt and The Caddy Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package reverseproxy + +import ( + "fmt" + "sync/atomic" + "time" + + "github.com/caddyserver/caddy/v2" + "github.com/vulcand/oxy/memmetrics" +) + +func init() { + caddy.RegisterModule(localCircuitBreaker{}) +} + +// localCircuitBreaker implements circuit breaking functionality +// for requests within this process over a sliding time window. +type localCircuitBreaker struct { + tripped int32 + cbType int32 + threshold float64 + metrics *memmetrics.RTMetrics + tripTime time.Duration + Config +} + +// CaddyModule returns the Caddy module information. +func (localCircuitBreaker) CaddyModule() caddy.ModuleInfo { + return caddy.ModuleInfo{ + Name: "http.handlers.reverse_proxy.circuit_breakers.local", + New: func() caddy.Module { return new(localCircuitBreaker) }, + } +} + +// Provision sets up a configured circuit breaker. +func (c *localCircuitBreaker) Provision(ctx caddy.Context) error { + t, ok := typeCB[c.Type] + if !ok { + return fmt.Errorf("type is not defined") + } + + if c.TripTime == "" { + c.TripTime = defaultTripTime + } + + tw, err := time.ParseDuration(c.TripTime) + if err != nil { + return fmt.Errorf("cannot parse trip_time duration, %v", err.Error()) + } + + mt, err := memmetrics.NewRTMetrics() + if err != nil { + return fmt.Errorf("cannot create new metrics: %v", err.Error()) + } + + c.cbType = t + c.tripTime = tw + c.threshold = c.Threshold + c.metrics = mt + c.tripped = 0 + + return nil +} + +// Ok returns whether the circuit breaker is tripped or not. +func (c *localCircuitBreaker) Ok() bool { + tripped := atomic.LoadInt32(&c.tripped) + return tripped == 0 +} + +// RecordMetric records a response status code and execution time of a request. This function should be run in a separate goroutine. +func (c *localCircuitBreaker) RecordMetric(statusCode int, latency time.Duration) { + c.metrics.Record(statusCode, latency) + c.checkAndSet() +} + +// Ok checks our metrics to see if we should trip our circuit breaker, or if the fallback duration has completed. +func (c *localCircuitBreaker) checkAndSet() { + var isTripped bool + + switch c.cbType { + case typeErrorRatio: + // check if amount of network errors exceed threshold over sliding window, threshold for comparison should be < 1.0 i.e. .5 = 50th percentile + if c.metrics.NetworkErrorRatio() > c.threshold { + isTripped = true + } + case typeLatency: + // check if threshold in milliseconds is reached and trip + hist, err := c.metrics.LatencyHistogram() + if err != nil { + return + } + + l := hist.LatencyAtQuantile(c.threshold) + if l.Nanoseconds()/int64(time.Millisecond) > int64(c.threshold) { + isTripped = true + } + case typeStatusCodeRatio: + // check ratio of error status codes of sliding window, threshold for comparison should be < 1.0 i.e. .5 = 50th percentile + if c.metrics.ResponseCodeRatio(500, 600, 0, 600) > c.threshold { + isTripped = true + } + } + + if isTripped { + c.metrics.Reset() + atomic.AddInt32(&c.tripped, 1) + + // wait tripTime amount before allowing operations to resume. + t := time.NewTimer(c.tripTime) + <-t.C + + atomic.AddInt32(&c.tripped, -1) + } +} + +// Config represents the configuration of a circuit breaker. +type Config struct { + Threshold float64 `json:"threshold"` + Type string `json:"type"` + TripTime string `json:"trip_time"` +} + +const ( + typeLatency = iota + 1 + typeErrorRatio + typeStatusCodeRatio + defaultTripTime = "5s" +) + +var ( + // typeCB handles converting a Config Type value to the internal circuit breaker types. + typeCB = map[string]int32{ + "latency": typeLatency, + "error_ratio": typeErrorRatio, + "status_ratio": typeStatusCodeRatio, + } +) diff --git a/modules/caddyhttp/starlarkmw/example/caddy.json b/modules/caddyhttp/starlarkmw/example/caddy.json new file mode 100644 index 0000000..66f9f2c --- /dev/null +++ b/modules/caddyhttp/starlarkmw/example/caddy.json @@ -0,0 +1,19 @@ +{ + "apps": { + "http": { + "servers": { + "MY_SERVER": { + "listen": [":3001"], + "routes": [ + { + "handle": { + "handler": "starlark", + "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" + } + } + ] + } + } + } + } +}
\ No newline at end of file diff --git a/modules/caddyhttp/starlarkmw/internal/lib/module.go b/modules/caddyhttp/starlarkmw/internal/lib/module.go new file mode 100644 index 0000000..a7164cd --- /dev/null +++ b/modules/caddyhttp/starlarkmw/internal/lib/module.go @@ -0,0 +1,165 @@ +package lib + +import ( + "encoding/json" + "fmt" + "strings" + + "github.com/caddyserver/caddy/v2/modules/caddyhttp" + + "github.com/caddyserver/caddy/v2" + "go.starlark.net/starlark" +) + +// ResponderModule represents a module that satisfies the caddyhttp handler. +type ResponderModule struct { + Name string + Cfg json.RawMessage + Instance caddyhttp.Handler +} + +func (r ResponderModule) Freeze() {} +func (r ResponderModule) Hash() (uint32, error) { return 0, fmt.Errorf("unhashable: responder module") } +func (r ResponderModule) String() string { return "responder module" } +func (r ResponderModule) Type() string { return "responder module" } +func (r ResponderModule) Truth() starlark.Bool { return true } + +// Middleware represents a module that satisfies the starlark Value interface. +type Middleware struct { + Name string + Cfg json.RawMessage + Instance caddyhttp.MiddlewareHandler +} + +func (r Middleware) Freeze() {} +func (r Middleware) Hash() (uint32, error) { return 0, fmt.Errorf("unhashable: middleware") } +func (r Middleware) String() string { return "middleware" } +func (r Middleware) Type() string { return "middleware" } +func (r Middleware) Truth() starlark.Bool { return true } + +// LoadMiddleware represents the method exposed to starlark to load a Caddy module. +type LoadMiddleware struct { + Middleware Middleware + Ctx caddy.Context +} + +func (r LoadMiddleware) Freeze() {} +func (r LoadMiddleware) Hash() (uint32, error) { return 0, fmt.Errorf("unhashable: loadMiddleware") } +func (r LoadMiddleware) String() string { return "loadMiddleware" } +func (r LoadMiddleware) Type() string { return "function: loadMiddleware" } +func (r LoadMiddleware) Truth() starlark.Bool { return true } + +// Run is the method bound to the starlark loadMiddleware function. +func (r *LoadMiddleware) Run(thread *starlark.Thread, fn *starlark.Builtin, args starlark.Tuple, kwargs []starlark.Tuple) (starlark.Value, error) { + var name string + var cfg *starlark.Dict + err := starlark.UnpackPositionalArgs(fn.Name(), args, kwargs, 2, &name, &cfg) + if err != nil { + return starlark.None, fmt.Errorf("unpacking arguments: %v", err.Error()) + } + + js := json.RawMessage(cfg.String()) + + if strings.Index(name, "http.handlers.") == -1 { + name = fmt.Sprintf("http.handlers.%s", name) + } + + inst, err := r.Ctx.LoadModule(name, js) + if err != nil { + return starlark.None, err + } + + mid, ok := inst.(caddyhttp.MiddlewareHandler) + if !ok { + return starlark.None, fmt.Errorf("could not assert as middleware handler") + } + + m := Middleware{ + Name: name, + Cfg: js, + Instance: mid, + } + + r.Middleware = m + + return m, nil +} + +// LoadResponder represents the method exposed to starlark to load a Caddy middleware responder. +type LoadResponder struct { + Module ResponderModule + Ctx caddy.Context +} + +func (r LoadResponder) Freeze() {} +func (r LoadResponder) Hash() (uint32, error) { return 0, fmt.Errorf("unhashable: loadModule") } +func (r LoadResponder) String() string { return "loadModule" } +func (r LoadResponder) Type() string { return "function: loadModule" } +func (r LoadResponder) Truth() starlark.Bool { return true } + +// Run is the method bound to the starlark loadResponder function. +func (r *LoadResponder) Run(thread *starlark.Thread, fn *starlark.Builtin, args starlark.Tuple, kwargs []starlark.Tuple) (starlark.Value, error) { + var name string + var cfg *starlark.Dict + err := starlark.UnpackPositionalArgs(fn.Name(), args, kwargs, 2, &name, &cfg) + if err != nil { + return starlark.None, fmt.Errorf("unpacking arguments: %v", err.Error()) + } + + js := json.RawMessage(cfg.String()) + + if strings.Index(name, "http.handlers.") == -1 { + name = fmt.Sprintf("http.handlers.%s", name) + } + + inst, err := r.Ctx.LoadModule(name, js) + if err != nil { + return starlark.None, err + } + + res, ok := inst.(caddyhttp.Handler) + if !ok { + return starlark.None, fmt.Errorf("could not assert as responder") + } + + m := ResponderModule{ + Name: name, + Cfg: js, + Instance: res, + } + + r.Module = m + + return m, nil +} + +// Execute represents the method exposed to starlark to build a middleware chain. +type Execute struct { + Modules []Middleware +} + +func (r Execute) Freeze() {} +func (r Execute) Hash() (uint32, error) { return 0, fmt.Errorf("unhashable: execute") } +func (r Execute) String() string { return "execute" } +func (r Execute) Type() string { return "function: execute" } +func (r Execute) Truth() starlark.Bool { return true } + +// Run is the method bound to the starlark execute function. +func (r *Execute) Run(thread *starlark.Thread, fn *starlark.Builtin, args starlark.Tuple, kwargs []starlark.Tuple) (starlark.Value, error) { + var mids *starlark.List + err := starlark.UnpackPositionalArgs(fn.Name(), args, kwargs, 1, &mids) + if err != nil { + return starlark.None, fmt.Errorf("unpacking arguments: %v", err.Error()) + } + + for i := 0; i < mids.Len(); i++ { + val, ok := mids.Index(i).(Middleware) + if !ok { + return starlark.None, fmt.Errorf("cannot get module from execute") + } + + r.Modules = append(r.Modules, val) + } + + return starlark.None, nil +} diff --git a/modules/caddyhttp/starlarkmw/starlarkmw.go b/modules/caddyhttp/starlarkmw/starlarkmw.go new file mode 100644 index 0000000..007ddb4 --- /dev/null +++ b/modules/caddyhttp/starlarkmw/starlarkmw.go @@ -0,0 +1,172 @@ +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 +} diff --git a/modules/caddyhttp/starlarkmw/tools/gen/example.star b/modules/caddyhttp/starlarkmw/tools/gen/example.star new file mode 100644 index 0000000..6ccab32 --- /dev/null +++ b/modules/caddyhttp/starlarkmw/tools/gen/example.star @@ -0,0 +1,40 @@ +# any module that provisions resources +proxyConfig = { + 'load_balance_type': 'round_robin', + 'upstreams': [ + { + 'host': 'http://localhost:8080', + 'circuit_breaker': { + 'type': 'status_ratio', + 'threshold': 0.5 + } + }, + { + 'host': 'http://localhost:8081' + } + ] +} + +sfConfig = { + 'root': '/Users/dev/Desktop', + 'browse': {}, +} + +proxy = loadResponder('reverse_proxy', proxyConfig) +static_files = loadResponder('file_server', sfConfig) + +def setup(r): + # create some middlewares specific to this request + mid = [] + + if r.query.get('log') == 'true': + logMid = loadMiddleware('log', {'file': 'access.log'}) + mid.append(logMid) + + execute(mid) + +def serveHTTP(w, r): + if r.url.find('static') > 0: + return static_files + + return proxy |