diff options
Diffstat (limited to 'modules')
-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 | ||||
-rw-r--r-- | modules/caddytls/certselection.go | 71 | ||||
-rw-r--r-- | modules/caddytls/distributedstek/distributedstek.go | 228 | ||||
-rw-r--r-- | modules/caddytls/pemloader.go | 65 |
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) |