summaryrefslogtreecommitdiff
path: root/modules
diff options
context:
space:
mode:
authorMatt Holt <mholt@users.noreply.github.com>2019-10-10 11:27:45 -0600
committerGitHub <noreply@github.com>2019-10-10 11:27:45 -0600
commitb38365ff3b6ab8b2c474603fa0603831e7eff2a7 (patch)
tree7a35ea901e4ce8d3fbe028e8fc81bd1f69c6be68 /modules
parent53dd600b4de0c2b169f1eb4450a0f02950912524 (diff)
parent26cc8837084f9cea6057e9908f0b5bde0eb15d3e (diff)
Merge pull request #2799 from caddyserver/v2-enterprise-merge
v2: Merge enterprise code into open source v2 branch
Diffstat (limited to 'modules')
-rw-r--r--modules/caddyhttp/httpcache/httpcache.go205
-rw-r--r--modules/caddyhttp/reverseproxy/circuitbreaker.go152
-rw-r--r--modules/caddyhttp/starlarkmw/example/caddy.json19
-rw-r--r--modules/caddyhttp/starlarkmw/internal/lib/module.go165
-rw-r--r--modules/caddyhttp/starlarkmw/starlarkmw.go172
-rw-r--r--modules/caddyhttp/starlarkmw/tools/gen/example.star40
-rw-r--r--modules/caddytls/certselection.go71
-rw-r--r--modules/caddytls/distributedstek/distributedstek.go228
-rw-r--r--modules/caddytls/pemloader.go65
9 files changed, 1117 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
diff --git a/modules/caddytls/certselection.go b/modules/caddytls/certselection.go
new file mode 100644
index 0000000..b56185a
--- /dev/null
+++ b/modules/caddytls/certselection.go
@@ -0,0 +1,71 @@
+package caddytls
+
+import (
+ "crypto/tls"
+ "crypto/x509"
+ "fmt"
+ "math/big"
+
+ "github.com/caddyserver/caddy/v2"
+ "github.com/mholt/certmagic"
+)
+
+func init() {
+ caddy.RegisterModule(Policy{})
+}
+
+// Policy represents a policy for selecting the certificate used to
+// complete a handshake when there may be multiple options. All fields
+// specified must match the candidate certificate for it to be chosen.
+// This was needed to solve https://github.com/caddyserver/caddy/issues/2588.
+type Policy struct {
+ SerialNumber *big.Int `json:"serial_number,omitempty"`
+ SubjectOrganization string `json:"subject_organization,omitempty"`
+ PublicKeyAlgorithm PublicKeyAlgorithm `json:"public_key_algorithm,omitempty"`
+ Tag string `json:"tag,omitempty"`
+}
+
+// CaddyModule returns the Caddy module information.
+func (Policy) CaddyModule() caddy.ModuleInfo {
+ return caddy.ModuleInfo{
+ Name: "tls.certificate_selection.custom",
+ New: func() caddy.Module { return new(Policy) },
+ }
+}
+
+// SelectCertificate implements certmagic.CertificateSelector.
+func (p Policy) SelectCertificate(_ *tls.ClientHelloInfo, choices []certmagic.Certificate) (certmagic.Certificate, error) {
+ for _, cert := range choices {
+ if p.SerialNumber != nil && cert.SerialNumber.Cmp(p.SerialNumber) != 0 {
+ continue
+ }
+
+ if p.PublicKeyAlgorithm != PublicKeyAlgorithm(x509.UnknownPublicKeyAlgorithm) &&
+ PublicKeyAlgorithm(cert.PublicKeyAlgorithm) != p.PublicKeyAlgorithm {
+ continue
+ }
+
+ if p.SubjectOrganization != "" {
+ var matchOrg bool
+ for _, org := range cert.Subject.Organization {
+ if p.SubjectOrganization == org {
+ matchOrg = true
+ break
+ }
+ }
+ if !matchOrg {
+ continue
+ }
+ }
+
+ if p.Tag != "" && !cert.HasTag(p.Tag) {
+ continue
+ }
+
+ return cert, nil
+ }
+ return certmagic.Certificate{}, fmt.Errorf("no certificates matched custom selection policy")
+}
+
+// Interface guard
+var _ certmagic.CertificateSelector = (*Policy)(nil)
diff --git a/modules/caddytls/distributedstek/distributedstek.go b/modules/caddytls/distributedstek/distributedstek.go
new file mode 100644
index 0000000..a0c4cd2
--- /dev/null
+++ b/modules/caddytls/distributedstek/distributedstek.go
@@ -0,0 +1,228 @@
+// 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 distributedstek provides TLS session ticket ephemeral
+// keys (STEKs) in a distributed fashion by utilizing configured
+// storage for locking and key sharing. This allows a cluster of
+// machines to optimally resume TLS sessions in a load-balanced
+// environment without any hassle. This is similar to what
+// Twitter does, but without needing to rely on SSH, as it is
+// built into the web server this way:
+// https://blog.twitter.com/engineering/en_us/a/2013/forward-secrecy-at-twitter.html
+package distributedstek
+
+import (
+ "bytes"
+ "encoding/gob"
+ "encoding/json"
+ "fmt"
+ "log"
+ "time"
+
+ "github.com/caddyserver/caddy/v2"
+ "github.com/caddyserver/caddy/v2/modules/caddytls"
+ "github.com/mholt/certmagic"
+)
+
+func init() {
+ caddy.RegisterModule(Provider{})
+}
+
+// Provider implements a distributed STEK provider.
+type Provider struct {
+ Storage json.RawMessage `json:"storage,omitempty"`
+
+ storage certmagic.Storage
+ stekConfig *caddytls.SessionTicketService
+ timer *time.Timer
+}
+
+// CaddyModule returns the Caddy module information.
+func (Provider) CaddyModule() caddy.ModuleInfo {
+ return caddy.ModuleInfo{
+ Name: "tls.stek.distributed",
+ New: func() caddy.Module { return new(Provider) },
+ }
+}
+
+// Provision provisions s.
+func (s *Provider) Provision(ctx caddy.Context) error {
+ // unpack the storage module to use, if different from the default
+ if s.Storage != nil {
+ val, err := ctx.LoadModuleInline("module", "caddy.storage", s.Storage)
+ if err != nil {
+ return fmt.Errorf("loading TLS storage module: %s", err)
+ }
+ cmStorage, err := val.(caddy.StorageConverter).CertMagicStorage()
+ if err != nil {
+ return fmt.Errorf("creating TLS storage configuration: %v", err)
+ }
+ s.storage = cmStorage
+ s.Storage = nil // allow GC to deallocate
+ }
+
+ // otherwise, use default storage
+ if s.storage == nil {
+ s.storage = ctx.Storage()
+ }
+
+ return nil
+}
+
+// Initialize sets the configuration for s and returns the starting keys.
+func (s *Provider) Initialize(config *caddytls.SessionTicketService) ([][32]byte, error) {
+ // keep a reference to the config; we'll need it when rotating keys
+ s.stekConfig = config
+
+ dstek, err := s.getSTEK()
+ if err != nil {
+ return nil, err
+ }
+
+ // create timer for the remaining time on the interval;
+ // this timer is cleaned up only when rotate() returns
+ s.timer = time.NewTimer(time.Until(dstek.NextRotation))
+
+ return dstek.Keys, nil
+}
+
+// Next returns a channel which transmits the latest session ticket keys.
+func (s *Provider) Next(doneChan <-chan struct{}) <-chan [][32]byte {
+ keysChan := make(chan [][32]byte)
+ go s.rotate(doneChan, keysChan)
+ return keysChan
+}
+
+func (s *Provider) loadSTEK() (distributedSTEK, error) {
+ var sg distributedSTEK
+ gobBytes, err := s.storage.Load(stekFileName)
+ if err != nil {
+ return sg, err // don't wrap, in case error is certmagic.ErrNotExist
+ }
+ dec := gob.NewDecoder(bytes.NewReader(gobBytes))
+ err = dec.Decode(&sg)
+ if err != nil {
+ return sg, fmt.Errorf("STEK gob corrupted: %v", err)
+ }
+ return sg, nil
+}
+
+func (s *Provider) storeSTEK(dstek distributedSTEK) error {
+ var buf bytes.Buffer
+ err := gob.NewEncoder(&buf).Encode(dstek)
+ if err != nil {
+ return fmt.Errorf("encoding STEK gob: %v", err)
+ }
+ err = s.storage.Store(stekFileName, buf.Bytes())
+ if err != nil {
+ return fmt.Errorf("storing STEK gob: %v", err)
+ }
+ return nil
+}
+
+// getSTEK locks and loads the current STEK from storage. If none
+// currently exists, a new STEK is created and persisted. If the
+// current STEK is outdated (NextRotation time is in the past),
+// then it is rotated and persisted. The resulting STEK is returned.
+func (s *Provider) getSTEK() (distributedSTEK, error) {
+ s.storage.Lock(stekLockName)
+ defer s.storage.Unlock(stekLockName)
+
+ // load the current STEKs from storage
+ dstek, err := s.loadSTEK()
+ if _, isNotExist := err.(certmagic.ErrNotExist); isNotExist {
+ // if there is none, then make some right away
+ dstek, err = s.rotateKeys(dstek)
+ if err != nil {
+ return dstek, fmt.Errorf("creating new STEK: %v", err)
+ }
+ } else if err != nil {
+ // some other error, that's a problem
+ return dstek, fmt.Errorf("loading STEK: %v", err)
+ } else if time.Now().After(dstek.NextRotation) {
+ // if current STEKs are outdated, rotate them
+ dstek, err = s.rotateKeys(dstek)
+ if err != nil {
+ return dstek, fmt.Errorf("rotating keys: %v", err)
+ }
+ }
+
+ return dstek, nil
+}
+
+// rotateKeys rotates the keys of oldSTEK and returns the new distributedSTEK
+// with updated keys and timestamps. It stores the returned STEK in storage,
+// so this function must only be called in a storage-provided lock.
+func (s *Provider) rotateKeys(oldSTEK distributedSTEK) (distributedSTEK, error) {
+ var newSTEK distributedSTEK
+ var err error
+
+ newSTEK.Keys, err = s.stekConfig.RotateSTEKs(oldSTEK.Keys)
+ if err != nil {
+ return newSTEK, err
+ }
+
+ now := time.Now()
+ newSTEK.LastRotation = now
+ newSTEK.NextRotation = now.Add(time.Duration(s.stekConfig.RotationInterval))
+
+ err = s.storeSTEK(newSTEK)
+ if err != nil {
+ return newSTEK, err
+ }
+
+ return newSTEK, nil
+}
+
+// rotate rotates keys on a regular basis, sending each updated set of
+// keys down keysChan, until doneChan is closed.
+func (s *Provider) rotate(doneChan <-chan struct{}, keysChan chan<- [][32]byte) {
+ for {
+ select {
+ case <-s.timer.C:
+ dstek, err := s.getSTEK()
+ if err != nil {
+ // TODO: improve this handling
+ log.Printf("[ERROR] Loading STEK: %v", err)
+ continue
+ }
+
+ // send the updated keys to the service
+ keysChan <- dstek.Keys
+
+ // timer channel is already drained, so reset directly (see godoc)
+ s.timer.Reset(time.Until(dstek.NextRotation))
+
+ case <-doneChan:
+ // again, see godocs for why timer is stopped this way
+ if !s.timer.Stop() {
+ <-s.timer.C
+ }
+ return
+ }
+ }
+}
+
+type distributedSTEK struct {
+ Keys [][32]byte
+ LastRotation, NextRotation time.Time
+}
+
+const (
+ stekLockName = "stek_check"
+ stekFileName = "stek/stek.bin"
+)
+
+// Interface guard
+var _ caddytls.STEKProvider = (*Provider)(nil)
diff --git a/modules/caddytls/pemloader.go b/modules/caddytls/pemloader.go
new file mode 100644
index 0000000..30a491c
--- /dev/null
+++ b/modules/caddytls/pemloader.go
@@ -0,0 +1,65 @@
+// 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 caddytls
+
+import (
+ "crypto/tls"
+ "fmt"
+
+ "github.com/caddyserver/caddy/v2"
+)
+
+func init() {
+ caddy.RegisterModule(PEMLoader{})
+}
+
+// PEMLoader loads certificates and their associated keys by
+// decoding their PEM blocks directly. This has the advantage
+// of not needing to store them on disk at all.
+type PEMLoader []CertKeyPEMPair
+
+// CaddyModule returns the Caddy module information.
+func (PEMLoader) CaddyModule() caddy.ModuleInfo {
+ return caddy.ModuleInfo{
+ Name: "tls.certificates.load_pem",
+ New: func() caddy.Module { return PEMLoader{} },
+ }
+}
+
+// CertKeyPEMPair pairs certificate and key PEM blocks.
+type CertKeyPEMPair struct {
+ CertificatePEM string `json:"certificate"`
+ KeyPEM string `json:"key"`
+ Tags []string `json:"tags,omitempty"`
+}
+
+// LoadCertificates returns the certificates contained in pl.
+func (pl PEMLoader) LoadCertificates() ([]Certificate, error) {
+ var certs []Certificate
+ for i, pair := range pl {
+ cert, err := tls.X509KeyPair([]byte(pair.CertificatePEM), []byte(pair.KeyPEM))
+ if err != nil {
+ return nil, fmt.Errorf("PEM pair %d: %v", i, err)
+ }
+ certs = append(certs, Certificate{
+ Certificate: cert,
+ Tags: pair.Tags,
+ })
+ }
+ return certs, nil
+}
+
+// Interface guard
+var _ CertificateLoader = (PEMLoader)(nil)