diff options
author | Matt Holt <mholt@users.noreply.github.com> | 2019-08-21 11:28:03 -0600 |
---|---|---|
committer | GitHub <noreply@github.com> | 2019-08-21 11:28:03 -0600 |
commit | 0544f0266a11874efb305a46db762248e64bc62d (patch) | |
tree | 3e75e6f59047bb967c52915e1e75aee6ad67c0ea /modules | |
parent | 42f75a4ca94ef3fb5e15a74e5dc9ef8b4f1f0b39 (diff) | |
parent | b2aa679c33f63ebec5bc1a21bca01f345dffebdd (diff) |
Merge pull request #2699 from caddyserver/cfadapter
v2: Implement config adapters and WIP Caddyfile adapter
Diffstat (limited to 'modules')
40 files changed, 1300 insertions, 372 deletions
diff --git a/modules/caddyhttp/caddyhttp.go b/modules/caddyhttp/caddyhttp.go index 467b40f..b4b1ec6 100644 --- a/modules/caddyhttp/caddyhttp.go +++ b/modules/caddyhttp/caddyhttp.go @@ -15,9 +15,12 @@ package caddyhttp import ( + "bytes" "context" "crypto/tls" + "encoding/json" "fmt" + "io" "log" weakrand "math/rand" "net" @@ -34,10 +37,7 @@ import ( func init() { weakrand.Seed(time.Now().UnixNano()) - err := caddy.RegisterModule(caddy.Module{ - Name: "http", - New: func() interface{} { return new(App) }, - }) + err := caddy.RegisterModule(App{}) if err != nil { log.Fatal(err) } @@ -55,6 +55,14 @@ type App struct { ctx caddy.Context } +// CaddyModule returns the Caddy module information. +func (App) CaddyModule() caddy.ModuleInfo { + return caddy.ModuleInfo{ + Name: "http", + New: func() caddy.Module { return new(App) }, + } +} + // Provision sets up the app. func (app *App) Provision(ctx caddy.Context) error { app.ctx = ctx @@ -224,7 +232,7 @@ func (app *App) automaticHTTPS() error { // find all qualifying domain names, de-duplicated domainSet := make(map[string]struct{}) for _, route := range srv.Routes { - for _, matcherSet := range route.matcherSets { + for _, matcherSet := range route.MatcherSets { for _, m := range matcherSet { if hm, ok := m.(*MatchHost); ok { for _, d := range *hm { @@ -244,6 +252,14 @@ func (app *App) automaticHTTPS() error { for d := range domainSet { domains = append(domains, d) if !srv.AutoHTTPS.Skipped(d, srv.AutoHTTPS.SkipCerts) { + // if a certificate for this name is already loaded, + // don't obtain another one for it, unless we are + // supposed to ignore loaded certificates + if !srv.AutoHTTPS.IgnoreLoadedCerts && + len(tlsApp.CertificatesWithSAN(d)) > 0 { + log.Printf("[INFO][%s] Skipping automatic certificate management because a certificate with that SAN is already loaded", d) + continue + } domainsForCerts = append(domainsForCerts, d) } } @@ -319,16 +335,16 @@ func (app *App) automaticHTTPS() error { } redirTo += "{http.request.uri}" - redirRoutes = append(redirRoutes, ServerRoute{ - matcherSets: []MatcherSet{ + redirRoutes = append(redirRoutes, Route{ + MatcherSets: []MatcherSet{ { MatchProtocol("http"), MatchHost(domains), }, }, - handlers: []MiddlewareHandler{ + Handlers: []MiddlewareHandler{ StaticResponse{ - StatusCode: weakString(strconv.Itoa(http.StatusTemporaryRedirect)), // TODO: use permanent redirect instead + StatusCode: WeakString(strconv.Itoa(http.StatusTemporaryRedirect)), // TODO: use permanent redirect instead Headers: http.Header{ "Location": []string{redirTo}, "Connection": []string{"close"}, @@ -431,6 +447,77 @@ type MiddlewareHandler interface { // emptyHandler is used as a no-op handler. var emptyHandler HandlerFunc = func(http.ResponseWriter, *http.Request) error { return nil } +// WeakString is a type that unmarshals any JSON value +// as a string literal, with the following exceptions: +// 1) actual string values are decoded as strings, and +// 2) null is decoded as empty string +// and provides methods for getting the value as various +// primitive types. However, using this type removes any +// type safety as far as deserializing JSON is concerned. +type WeakString string + +// UnmarshalJSON satisfies json.Unmarshaler according to +// this type's documentation. +func (ws *WeakString) UnmarshalJSON(b []byte) error { + if len(b) == 0 { + return io.EOF + } + if b[0] == byte('"') && b[len(b)-1] == byte('"') { + var s string + err := json.Unmarshal(b, &s) + if err != nil { + return err + } + *ws = WeakString(s) + return nil + } + if bytes.Equal(b, []byte("null")) { + return nil + } + *ws = WeakString(b) + return nil +} + +// MarshalJSON marshals was a boolean if true or false, +// a number if an integer, or a string otherwise. +func (ws WeakString) MarshalJSON() ([]byte, error) { + if ws == "true" { + return []byte("true"), nil + } + if ws == "false" { + return []byte("false"), nil + } + if num, err := strconv.Atoi(string(ws)); err == nil { + return json.Marshal(num) + } + return json.Marshal(string(ws)) +} + +// Int returns ws as an integer. If ws is not an +// integer, 0 is returned. +func (ws WeakString) Int() int { + num, _ := strconv.Atoi(string(ws)) + return num +} + +// Float64 returns ws as a float64. If ws is not a +// float value, the zero value is returned. +func (ws WeakString) Float64() float64 { + num, _ := strconv.ParseFloat(string(ws), 64) + return num +} + +// Bool returns ws as a boolean. If ws is not a +// boolean, false is returned. +func (ws WeakString) Bool() bool { + return string(ws) == "true" +} + +// String returns ws as a string. +func (ws WeakString) String() string { + return string(ws) +} + const ( // DefaultHTTPPort is the default port for HTTP. DefaultHTTPPort = 80 diff --git a/modules/caddyhttp/caddylog/log.go b/modules/caddyhttp/caddylog/log.go index 902f60f..3f636d1 100644 --- a/modules/caddyhttp/caddylog/log.go +++ b/modules/caddyhttp/caddylog/log.go @@ -24,10 +24,7 @@ import ( ) func init() { - caddy.RegisterModule(caddy.Module{ - Name: "http.handlers.log", - New: func() interface{} { return new(Log) }, - }) + caddy.RegisterModule(Log{}) } // Log implements a simple logging middleware. @@ -36,6 +33,14 @@ type Log struct { counter int } +// CaddyModule returns the Caddy module information. +func (Log) CaddyModule() caddy.ModuleInfo { + return caddy.ModuleInfo{ + Name: "http.handlers.log", + New: func() caddy.Module { return new(Log) }, + } +} + func (l *Log) ServeHTTP(w http.ResponseWriter, r *http.Request, next caddyhttp.Handler) error { start := time.Now() diff --git a/modules/caddyhttp/encode/brotli/brotli.go b/modules/caddyhttp/encode/brotli/brotli.go index 0890d43..cf055aa 100644 --- a/modules/caddyhttp/encode/brotli/brotli.go +++ b/modules/caddyhttp/encode/brotli/brotli.go @@ -16,17 +16,16 @@ package caddybrotli import ( "fmt" + "strconv" "github.com/andybalholm/brotli" "github.com/caddyserver/caddy/v2" + "github.com/caddyserver/caddy/v2/caddyconfig/caddyfile" "github.com/caddyserver/caddy/v2/modules/caddyhttp/encode" ) func init() { - caddy.RegisterModule(caddy.Module{ - Name: "http.encoders.brotli", - New: func() interface{} { return new(Brotli) }, - }) + caddy.RegisterModule(Brotli{}) } // Brotli can create brotli encoders. Note that brotli @@ -35,6 +34,30 @@ type Brotli struct { Quality *int `json:"quality,omitempty"` } +// CaddyModule returns the Caddy module information. +func (Brotli) CaddyModule() caddy.ModuleInfo { + return caddy.ModuleInfo{ + Name: "http.encoders.brotli", + New: func() caddy.Module { return new(Brotli) }, + } +} + +// UnmarshalCaddyfile sets up the handler from Caddyfile tokens. +func (b *Brotli) UnmarshalCaddyfile(d *caddyfile.Dispenser) error { + for d.Next() { + if !d.NextArg() { + continue + } + qualityStr := d.Val() + quality, err := strconv.Atoi(qualityStr) + if err != nil { + return err + } + b.Quality = &quality + } + return nil +} + // Validate validates b's configuration. func (b Brotli) Validate() error { if b.Quality != nil { @@ -64,6 +87,7 @@ func (b Brotli) NewEncoder() encode.Encoder { // Interface guards var ( - _ encode.Encoding = (*Brotli)(nil) - _ caddy.Validator = (*Brotli)(nil) + _ encode.Encoding = (*Brotli)(nil) + _ caddy.Validator = (*Brotli)(nil) + _ caddyfile.Unmarshaler = (*Brotli)(nil) ) diff --git a/modules/caddyhttp/encode/caddyfile.go b/modules/caddyhttp/encode/caddyfile.go new file mode 100644 index 0000000..5762bd3 --- /dev/null +++ b/modules/caddyhttp/encode/caddyfile.go @@ -0,0 +1,99 @@ +// 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 encode + +import ( + "encoding/json" + "fmt" + + "github.com/caddyserver/caddy/v2" + "github.com/caddyserver/caddy/v2/caddyconfig" + "github.com/caddyserver/caddy/v2/caddyconfig/caddyfile" + "github.com/caddyserver/caddy/v2/caddyconfig/httpcaddyfile" + "github.com/caddyserver/caddy/v2/modules/caddyhttp" +) + +func init() { + httpcaddyfile.RegisterHandlerDirective("encode", parseCaddyfile) +} + +// TODO: This is a good example of why UnmarshalCaddyfile is still a good idea... hmm. +func parseCaddyfile(h httpcaddyfile.Helper) (caddyhttp.MiddlewareHandler, error) { + enc := new(Encode) + err := enc.UnmarshalCaddyfile(h.Dispenser) + if err != nil { + return nil, err + } + return enc, nil +} + +// TODO: Keep UnmarshalCaddyfile pattern? + +// UnmarshalCaddyfile sets up the handler from Caddyfile tokens. Syntax: +// +// encode [<matcher>] <formats...> { +// gzip [<level>] +// zstd +// brotli [<quality>] +// } +// +// Specifying the formats on the first line will use those formats' defaults. +func (enc *Encode) UnmarshalCaddyfile(d *caddyfile.Dispenser) error { + for d.Next() { + for _, arg := range d.RemainingArgs() { + mod, err := caddy.GetModule("http.encoders." + arg) + if err != nil { + return fmt.Errorf("finding encoder module '%s': %v", mod.Name, err) + } + encoding, ok := mod.New().(Encoding) + if !ok { + return fmt.Errorf("module %s is not an HTTP encoding", mod.Name) + } + if enc.EncodingsRaw == nil { + enc.EncodingsRaw = make(map[string]json.RawMessage) + } + enc.EncodingsRaw[arg] = caddyconfig.JSON(encoding, nil) + } + + for d.NextBlock() { + name := d.Val() + mod, err := caddy.GetModule("http.encoders." + name) + if err != nil { + return fmt.Errorf("getting encoder module '%s': %v", mod.Name, err) + } + unm, ok := mod.New().(caddyfile.Unmarshaler) + if !ok { + return fmt.Errorf("encoder module '%s' is not a Caddyfile unmarshaler", mod.Name) + } + err = unm.UnmarshalCaddyfile(d.NewFromNextTokens()) + if err != nil { + return err + } + encoding, ok := unm.(Encoding) + if !ok { + return fmt.Errorf("module %s is not an HTTP encoding", mod.Name) + } + if enc.EncodingsRaw == nil { + enc.EncodingsRaw = make(map[string]json.RawMessage) + } + enc.EncodingsRaw[name] = caddyconfig.JSON(encoding, nil) + } + } + + return nil +} + +// Interface guard +var _ caddyfile.Unmarshaler = (*Encode)(nil) diff --git a/modules/caddyhttp/encode/encode.go b/modules/caddyhttp/encode/encode.go index b2c1327..723b988 100644 --- a/modules/caddyhttp/encode/encode.go +++ b/modules/caddyhttp/encode/encode.go @@ -35,10 +35,7 @@ import ( ) func init() { - caddy.RegisterModule(caddy.Module{ - Name: "http.handlers.encode", - New: func() interface{} { return new(Encode) }, - }) + caddy.RegisterModule(Encode{}) } // Encode is a middleware which can encode responses. @@ -50,21 +47,25 @@ type Encode struct { writerPools map[string]*sync.Pool // TODO: these pools do not get reused through config reloads... } +// CaddyModule returns the Caddy module information. +func (Encode) CaddyModule() caddy.ModuleInfo { + return caddy.ModuleInfo{ + Name: "http.handlers.encode", + New: func() caddy.Module { return new(Encode) }, + } +} + // Provision provisions enc. func (enc *Encode) Provision(ctx caddy.Context) error { - enc.writerPools = make(map[string]*sync.Pool) - for modName, rawMsg := range enc.EncodingsRaw { val, err := ctx.LoadModule("http.encoders."+modName, rawMsg) if err != nil { return fmt.Errorf("loading encoder module '%s': %v", modName, err) } - encoder := val.(Encoding) - - enc.writerPools[encoder.AcceptEncoding()] = &sync.Pool{ - New: func() interface{} { - return encoder.NewEncoder() - }, + encoding := val.(Encoding) + err = enc.addEncoding(encoding) + if err != nil { + return err } } enc.EncodingsRaw = nil // allow GC to deallocate - TODO: Does this help? @@ -85,10 +86,28 @@ func (enc *Encode) ServeHTTP(w http.ResponseWriter, r *http.Request, next caddyh defer w.(*responseWriter).Close() break } - return next.ServeHTTP(w, r) } +func (enc *Encode) addEncoding(e Encoding) error { + ae := e.AcceptEncoding() + if ae == "" { + return fmt.Errorf("encoder does not specify an Accept-Encoding value") + } + if _, ok := enc.writerPools[ae]; ok { + return fmt.Errorf("encoder already added: %s", ae) + } + if enc.writerPools == nil { + enc.writerPools = make(map[string]*sync.Pool) + } + enc.writerPools[ae] = &sync.Pool{ + New: func() interface{} { + return e.NewEncoder() + }, + } + return nil +} + // openResponseWriter creates a new response writer that may (or may not) // encode the response with encodingName. The returned response writer MUST // be closed after the handler completes. diff --git a/modules/caddyhttp/encode/gzip/gzip.go b/modules/caddyhttp/encode/gzip/gzip.go index 45c5f54..d6d67f7 100644 --- a/modules/caddyhttp/encode/gzip/gzip.go +++ b/modules/caddyhttp/encode/gzip/gzip.go @@ -18,16 +18,15 @@ import ( "compress/flate" "compress/gzip" // TODO: consider using https://github.com/klauspost/compress/gzip "fmt" + "strconv" "github.com/caddyserver/caddy/v2" + "github.com/caddyserver/caddy/v2/caddyconfig/caddyfile" "github.com/caddyserver/caddy/v2/modules/caddyhttp/encode" ) func init() { - caddy.RegisterModule(caddy.Module{ - Name: "http.encoders.gzip", - New: func() interface{} { return new(Gzip) }, - }) + caddy.RegisterModule(Gzip{}) } // Gzip can create gzip encoders. @@ -35,6 +34,30 @@ type Gzip struct { Level int `json:"level,omitempty"` } +// CaddyModule returns the Caddy module information. +func (Gzip) CaddyModule() caddy.ModuleInfo { + return caddy.ModuleInfo{ + Name: "http.encoders.gzip", + New: func() caddy.Module { return new(Gzip) }, + } +} + +// UnmarshalCaddyfile sets up the handler from Caddyfile tokens. +func (g *Gzip) UnmarshalCaddyfile(d *caddyfile.Dispenser) error { + for d.Next() { + if !d.NextArg() { + continue + } + levelStr := d.Val() + level, err := strconv.Atoi(levelStr) + if err != nil { + return err + } + g.Level = level + } + return nil +} + // Provision provisions g's configuration. func (g *Gzip) Provision(ctx caddy.Context) error { if g.Level == 0 { @@ -69,7 +92,8 @@ var defaultGzipLevel = 5 // Interface guards var ( - _ encode.Encoding = (*Gzip)(nil) - _ caddy.Provisioner = (*Gzip)(nil) - _ caddy.Validator = (*Gzip)(nil) + _ encode.Encoding = (*Gzip)(nil) + _ caddy.Provisioner = (*Gzip)(nil) + _ caddy.Validator = (*Gzip)(nil) + _ caddyfile.Unmarshaler = (*Gzip)(nil) ) diff --git a/modules/caddyhttp/encode/zstd/zstd.go b/modules/caddyhttp/encode/zstd/zstd.go index acebff5..f2b4e85 100644 --- a/modules/caddyhttp/encode/zstd/zstd.go +++ b/modules/caddyhttp/encode/zstd/zstd.go @@ -16,20 +16,31 @@ package caddyzstd import ( "github.com/caddyserver/caddy/v2" + "github.com/caddyserver/caddy/v2/caddyconfig/caddyfile" "github.com/caddyserver/caddy/v2/modules/caddyhttp/encode" "github.com/klauspost/compress/zstd" ) func init() { - caddy.RegisterModule(caddy.Module{ - Name: "http.encoders.zstd", - New: func() interface{} { return new(Zstd) }, - }) + caddy.RegisterModule(Zstd{}) } // Zstd can create Zstandard encoders. type Zstd struct{} +// CaddyModule returns the Caddy module information. +func (Zstd) CaddyModule() caddy.ModuleInfo { + return caddy.ModuleInfo{ + Name: "http.encoders.zstd", + New: func() caddy.Module { return new(Zstd) }, + } +} + +// UnmarshalCaddyfile sets up the handler from Caddyfile tokens. +func (z *Zstd) UnmarshalCaddyfile(d *caddyfile.Dispenser) error { + return nil +} + // AcceptEncoding returns the name of the encoding as // used in the Accept-Encoding request headers. func (Zstd) AcceptEncoding() string { return "zstd" } @@ -40,5 +51,8 @@ func (z Zstd) NewEncoder() encode.Encoder { return writer } -// Interface guard -var _ encode.Encoding = (*Zstd)(nil) +// Interface guards +var ( + _ encode.Encoding = (*Zstd)(nil) + _ caddyfile.Unmarshaler = (*Zstd)(nil) +) diff --git a/modules/caddyhttp/fileserver/caddyfile.go b/modules/caddyhttp/fileserver/caddyfile.go new file mode 100644 index 0000000..6fa94e7 --- /dev/null +++ b/modules/caddyhttp/fileserver/caddyfile.go @@ -0,0 +1,104 @@ +// 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 fileserver + +import ( + "encoding/json" + + "github.com/caddyserver/caddy/modules/caddyhttp/rewrite" + "github.com/caddyserver/caddy/v2/caddyconfig/httpcaddyfile" + "github.com/caddyserver/caddy/v2/modules/caddyhttp" +) + +func init() { + httpcaddyfile.RegisterHandlerDirective("file_server", parseCaddyfile) + httpcaddyfile.RegisterDirective("try_files", parseTryFiles) +} + +func parseCaddyfile(h httpcaddyfile.Helper) (caddyhttp.MiddlewareHandler, error) { + var fsrv FileServer + + for h.Next() { + args := h.RemainingArgs() + switch len(args) { + case 0: + case 1: + if args[0] != "browse" { + return nil, h.ArgErr() + } + fsrv.Browse = new(Browse) + default: + return nil, h.ArgErr() + } + + for h.NextBlock() { + switch h.Val() { + case "hide": + fsrv.Hide = h.RemainingArgs() + if len(fsrv.Hide) == 0 { + return nil, h.ArgErr() + } + case "index": + fsrv.IndexNames = h.RemainingArgs() + if len(fsrv.Hide) == 0 { + return nil, h.ArgErr() + } + case "root": + if !h.Args(&fsrv.Root) { + return nil, h.ArgErr() + } + case "browse": + if fsrv.Browse != nil { + return nil, h.Err("browsing is already configured") + } + fsrv.Browse = new(Browse) + h.Args(&fsrv.Browse.TemplateFile) + default: + return nil, h.Errf("unknown subdirective '%s'", h.Val()) + } + } + } + + // if no root was configured explicitly, use site root + if fsrv.Root == "" { + fsrv.Root = "{http.var.root}" + } + + return &fsrv, nil +} + +func parseTryFiles(h httpcaddyfile.Helper) ([]httpcaddyfile.ConfigValue, error) { + if !h.Next() { + return nil, h.ArgErr() + } + + try := h.RemainingArgs() + if len(try) == 0 { + return nil, h.ArgErr() + } + + handler := rewrite.Rewrite{ + URI: "{http.matchers.file.relative}{http.request.uri.query}", + } + + matcherSet := map[string]json.RawMessage{ + "file": h.JSON(MatchFile{ + Root: "{http.var.root}", + TryFiles: try, + }, nil), + } + + return h.NewRoute(matcherSet, handler), nil +} diff --git a/modules/caddyhttp/fileserver/matcher.go b/modules/caddyhttp/fileserver/matcher.go index 17d5c11..b091250 100644 --- a/modules/caddyhttp/fileserver/matcher.go +++ b/modules/caddyhttp/fileserver/matcher.go @@ -21,14 +21,12 @@ import ( "time" "github.com/caddyserver/caddy/v2" + "github.com/caddyserver/caddy/v2/caddyconfig/caddyfile" "github.com/caddyserver/caddy/v2/modules/caddyhttp" ) func init() { - caddy.RegisterModule(caddy.Module{ - Name: "http.matchers.file", - New: func() interface{} { return new(MatchFile) }, - }) + caddy.RegisterModule(MatchFile{}) } // MatchFile is an HTTP request matcher that can match @@ -51,6 +49,50 @@ type MatchFile struct { TryPolicy string `json:"try_policy,omitempty"` } +// CaddyModule returns the Caddy module information. +func (MatchFile) CaddyModule() caddy.ModuleInfo { + return caddy.ModuleInfo{ + Name: "http.matchers.file", + New: func() caddy.Module { return new(MatchFile) }, + } +} + +// UnmarshalCaddyfile sets up the matcher from Caddyfile tokens. Syntax: +// +// file { +// root <path> +// try_files <files...> +// try_policy first_exist|smallest_size|largest_size|most_recent_modified +// } +// +func (m *MatchFile) UnmarshalCaddyfile(d *caddyfile.Dispenser) error { + for d.Next() { + for d.NextBlock() { + switch d.Val() { + case "root": + if !d.NextArg() { + return d.ArgErr() + } + m.Root = d.Val() + case "try_files": + m.TryFiles = d.RemainingArgs() + if len(m.TryFiles) == 0 { + return d.ArgErr() + } + case "try_policy": + if !d.NextArg() { + return d.ArgErr() + } + m.TryPolicy = d.Val() + } + } + } + if m.Root == "" { + m.Root = "{http.var.root}" + } + return nil +} + // Validate ensures m has a valid configuration. func (m MatchFile) Validate() error { switch m.TryPolicy { @@ -87,7 +129,7 @@ func (m MatchFile) Match(r *http.Request) bool { func (m MatchFile) selectFile(r *http.Request) (rel, abs string, matched bool) { repl := r.Context().Value(caddy.ReplacerCtxKey).(caddy.Replacer) - root := repl.ReplaceAll(m.Root, "") + root := repl.ReplaceAll(m.Root, ".") // if list of files to try was omitted entirely, // assume URL path diff --git a/modules/caddyhttp/fileserver/staticfiles.go b/modules/caddyhttp/fileserver/staticfiles.go index a66b753..cdac453 100644 --- a/modules/caddyhttp/fileserver/staticfiles.go +++ b/modules/caddyhttp/fileserver/staticfiles.go @@ -36,10 +36,7 @@ import ( func init() { weakrand.Seed(time.Now().UnixNano()) - caddy.RegisterModule(caddy.Module{ - Name: "http.handlers.file_server", - New: func() interface{} { return new(FileServer) }, - }) + caddy.RegisterModule(FileServer{}) } // FileServer implements a static file server responder for Caddy. @@ -48,8 +45,14 @@ type FileServer struct { Hide []string `json:"hide,omitempty"` IndexNames []string `json:"index_names,omitempty"` Browse *Browse `json:"browse,omitempty"` +} - // TODO: Content negotiation +// CaddyModule returns the Caddy module information. +func (FileServer) CaddyModule() caddy.ModuleInfo { + return caddy.ModuleInfo{ + Name: "http.handlers.file_server", + New: func() caddy.Module { return new(FileServer) }, + } } // Provision sets up the static files responder. @@ -83,7 +86,7 @@ func (fsrv *FileServer) ServeHTTP(w http.ResponseWriter, r *http.Request, _ cadd filesToHide := fsrv.transformHidePaths(repl) - root := repl.ReplaceAll(fsrv.Root, "") + root := repl.ReplaceAll(fsrv.Root, ".") suffix := repl.ReplaceAll(r.URL.Path, "") filename := sanitizedPathJoin(root, suffix) @@ -302,7 +305,7 @@ func calculateEtag(d os.FileInfo) string { return `"` + t + s + `"` } -var defaultIndexNames = []string{"index.html"} +var defaultIndexNames = []string{"index.html", "index.txt"} var bufPool = sync.Pool{ New: func() interface{} { diff --git a/modules/caddyhttp/headers/caddyfile.go b/modules/caddyhttp/headers/caddyfile.go new file mode 100644 index 0000000..5eaf064 --- /dev/null +++ b/modules/caddyhttp/headers/caddyfile.go @@ -0,0 +1,91 @@ +// 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 headers + +import ( + "net/http" + "strings" + + "github.com/caddyserver/caddy/v2/caddyconfig/httpcaddyfile" + "github.com/caddyserver/caddy/v2/modules/caddyhttp" +) + +func init() { + httpcaddyfile.RegisterHandlerDirective("headers", parseCaddyfile) +} + +// parseCaddyfile sets up the handler from Caddyfile tokens. Syntax: +// +// headers [<matcher>] [[+|-]<field> <value>] { +// [+][<field>] [<value>] +// [-<field>] +// } +// +// Either a block can be opened or a single header field can be configured +// in the first line, but not both in the same directive. +func parseCaddyfile(h httpcaddyfile.Helper) (caddyhttp.MiddlewareHandler, error) { + hdr := new(Headers) + for h.Next() { + // first see if headers are in the initial line + var hasArgs bool + if h.NextArg() { + hasArgs = true + field := h.Val() + h.NextArg() + value := h.Val() + processCaddyfileLine(hdr, field, value) + } + + // if not, they should be in a block + for h.NextBlock() { + if hasArgs { + return nil, h.Err("cannot specify headers in both arguments and block") + } + field := h.Val() + var value string + if h.NextArg() { + value = h.Val() + } + processCaddyfileLine(hdr, field, value) + } + } + return hdr, nil +} + +func processCaddyfileLine(hdr *Headers, field, value string) { + if strings.HasPrefix(field, "+") { + if hdr.Response == nil { + hdr.Response = &RespHeaderOps{HeaderOps: new(HeaderOps)} + } + if hdr.Response.Add == nil { + hdr.Response.Add = make(http.Header) + } + hdr.Response.Add.Set(field[1:], value) + } else if strings.HasPrefix(field, "-") { + if hdr.Response == nil { + hdr.Response = &RespHeaderOps{HeaderOps: new(HeaderOps)} + } + hdr.Response.Delete = append(hdr.Response.Delete, field[1:]) + hdr.Response.Deferred = true + } else { + if hdr.Response == nil { + hdr.Response = &RespHeaderOps{HeaderOps: new(HeaderOps)} + } + if hdr.Response.Set == nil { + hdr.Response.Set = make(http.Header) + } + hdr.Response.Set.Set(field, value) + } +} diff --git a/modules/caddyhttp/headers/headers.go b/modules/caddyhttp/headers/headers.go index 8f4976a..e740004 100644 --- a/modules/caddyhttp/headers/headers.go +++ b/modules/caddyhttp/headers/headers.go @@ -23,10 +23,7 @@ import ( ) func init() { - caddy.RegisterModule(caddy.Module{ - Name: "http.handlers.headers", - New: func() interface{} { return new(Headers) }, - }) + caddy.RegisterModule(Headers{}) } // Headers is a middleware which can mutate HTTP headers. @@ -35,6 +32,14 @@ type Headers struct { Response *RespHeaderOps `json:"response,omitempty"` } +// CaddyModule returns the Caddy module information. +func (Headers) CaddyModule() caddy.ModuleInfo { + return caddy.ModuleInfo{ + Name: "http.handlers.headers", + New: func() caddy.Module { return new(Headers) }, + } +} + // HeaderOps defines some operations to // perform on HTTP headers. type HeaderOps struct { diff --git a/modules/caddyhttp/markdown/markdown.go b/modules/caddyhttp/markdown/markdown.go index 3ba4d02..122aad6 100644 --- a/modules/caddyhttp/markdown/markdown.go +++ b/modules/caddyhttp/markdown/markdown.go @@ -28,16 +28,21 @@ import ( ) func init() { - caddy.RegisterModule(caddy.Module{ - Name: "http.handlers.markdown", - New: func() interface{} { return new(Markdown) }, - }) + caddy.RegisterModule(Markdown{}) } // Markdown is a middleware for rendering a Markdown response body. type Markdown struct { } +// CaddyModule returns the Caddy module information. +func (Markdown) CaddyModule() caddy.ModuleInfo { + return caddy.ModuleInfo{ + Name: "http.handlers.markdown", + New: func() caddy.Module { return new(Markdown) }, + } +} + func (m Markdown) ServeHTTP(w http.ResponseWriter, r *http.Request, next caddyhttp.Handler) error { buf := bufPool.Get().(*bytes.Buffer) buf.Reset() diff --git a/modules/caddyhttp/matchers.go b/modules/caddyhttp/matchers.go index 6c5a23e..0dac151 100644 --- a/modules/caddyhttp/matchers.go +++ b/modules/caddyhttp/matchers.go @@ -28,6 +28,7 @@ import ( "strings" "github.com/caddyserver/caddy/v2" + "github.com/caddyserver/caddy/v2/caddyconfig/caddyfile" "github.com/caddyserver/caddy/v2/pkg/caddyscript" "go.starlark.net/starlark" ) @@ -79,50 +80,31 @@ type ( ) func init() { - caddy.RegisterModule(caddy.Module{ + caddy.RegisterModule(MatchHost{}) + caddy.RegisterModule(MatchPath{}) + caddy.RegisterModule(MatchPathRE{}) + caddy.RegisterModule(MatchMethod{}) + caddy.RegisterModule(MatchQuery{}) + caddy.RegisterModule(MatchHeader{}) + caddy.RegisterModule(MatchHeaderRE{}) + caddy.RegisterModule(new(MatchProtocol)) + caddy.RegisterModule(MatchRemoteIP{}) + caddy.RegisterModule(MatchNegate{}) + caddy.RegisterModule(new(MatchStarlarkExpr)) +} + +// CaddyModule returns the Caddy module information. +func (MatchHost) CaddyModule() caddy.ModuleInfo { + return caddy.ModuleInfo{ Name: "http.matchers.host", - New: func() interface{} { return new(MatchHost) }, - }) - caddy.RegisterModule(caddy.Module{ - Name: "http.matchers.path", - New: func() interface{} { return new(MatchPath) }, - }) - caddy.RegisterModule(caddy.Module{ - Name: "http.matchers.path_regexp", - New: func() interface{} { return new(MatchPathRE) }, - }) - caddy.RegisterModule(caddy.Module{ - Name: "http.matchers.method", - New: func() interface{} { return new(MatchMethod) }, - }) - caddy.RegisterModule(caddy.Module{ - Name: "http.matchers.query", - New: func() interface{} { return new(MatchQuery) }, - }) - caddy.RegisterModule(caddy.Module{ - Name: "http.matchers.header", - New: func() interface{} { return new(MatchHeader) }, - }) - caddy.RegisterModule(caddy.Module{ - Name: "http.matchers.header_regexp", - New: func() interface{} { return new(MatchHeaderRE) }, - }) - caddy.RegisterModule(caddy.Module{ - Name: "http.matchers.protocol", - New: func() interface{} { return new(MatchProtocol) }, - }) - caddy.RegisterModule(caddy.Module{ - Name: "http.matchers.remote_ip", - New: func() interface{} { return new(MatchRemoteIP) }, - }) - caddy.RegisterModule(caddy.Module{ - Name: "http.matchers.not", - New: func() interface{} { return new(MatchNegate) }, - }) - caddy.RegisterModule(caddy.Module{ - Name: "http.matchers.starlark_expr", - New: func() interface{} { return new(MatchStarlarkExpr) }, - }) + New: func() caddy.Module { return new(MatchHost) }, + } +} + +// UnmarshalCaddyfile implements caddyfile.Unmarshaler. +func (m *MatchHost) UnmarshalCaddyfile(d *caddyfile.Dispenser) error { + *m = d.RemainingArgs() + return nil } // Match returns true if r matches m. @@ -158,6 +140,14 @@ outer: return false } +// CaddyModule returns the Caddy module information. +func (MatchPath) CaddyModule() caddy.ModuleInfo { + return caddy.ModuleInfo{ + Name: "http.matchers.path", + New: func() caddy.Module { return new(MatchPath) }, + } +} + // Match returns true if r matches m. func (m MatchPath) Match(r *http.Request) bool { for _, matchPath := range m { @@ -177,12 +167,44 @@ func (m MatchPath) Match(r *http.Request) bool { return false } +// UnmarshalCaddyfile implements caddyfile.Unmarshaler. +func (m *MatchPath) UnmarshalCaddyfile(d *caddyfile.Dispenser) error { + for d.Next() { + *m = d.RemainingArgs() + } + return nil +} + +// CaddyModule returns the Caddy module information. +func (MatchPathRE) CaddyModule() caddy.ModuleInfo { + return caddy.ModuleInfo{ + Name: "http.matchers.path_regexp", + New: func() caddy.Module { return new(MatchPathRE) }, + } +} + // Match returns true if r matches m. func (m MatchPathRE) Match(r *http.Request) bool { repl := r.Context().Value(caddy.ReplacerCtxKey).(caddy.Replacer) return m.MatchRegexp.Match(r.URL.Path, repl, "path_regexp") } +// CaddyModule returns the Caddy module information. +func (MatchMethod) CaddyModule() caddy.ModuleInfo { + return caddy.ModuleInfo{ + Name: "http.matchers.method", + New: func() caddy.Module { return new(MatchMethod) }, + } +} + +// UnmarshalCaddyfile implements caddyfile.Unmarshaler. +func (m *MatchMethod) UnmarshalCaddyfile(d *caddyfile.Dispenser) error { + for d.Next() { + *m = d.RemainingArgs() + } + return nil +} + // Match returns true if r matches m. func (m MatchMethod) Match(r *http.Request) bool { for _, method := range m { @@ -193,6 +215,26 @@ func (m MatchMethod) Match(r *http.Request) bool { return false } +// CaddyModule returns the Caddy module information. +func (MatchQuery) CaddyModule() caddy.ModuleInfo { + return caddy.ModuleInfo{ + Name: "http.matchers.query", + New: func() caddy.Module { return new(MatchQuery) }, + } +} + +// UnmarshalCaddyfile implements caddyfile.Unmarshaler. +func (m *MatchQuery) UnmarshalCaddyfile(d *caddyfile.Dispenser) error { + for d.Next() { + parts := strings.SplitN(d.Val(), "=", 2) + if len(parts) != 2 { + return d.Errf("malformed query matcher token: %s; must be in param=val format", d.Val()) + } + url.Values(*m).Set(parts[0], parts[1]) + } + return nil +} + // Match returns true if r matches m. func (m MatchQuery) Match(r *http.Request) bool { for param, vals := range m { @@ -206,6 +248,26 @@ func (m MatchQuery) Match(r *http.Request) bool { return false } +// CaddyModule returns the Caddy module information. +func (MatchHeader) CaddyModule() caddy.ModuleInfo { + return caddy.ModuleInfo{ + Name: "http.matchers.header", + New: func() caddy.Module { return new(MatchHeader) }, + } +} + +// UnmarshalCaddyfile implements caddyfile.Unmarshaler. +func (m *MatchHeader) UnmarshalCaddyfile(d *caddyfile.Dispenser) error { + for d.Next() { + var field, val string + if !d.Args(&field, &val) { + return d.Errf("expected both field and value") + } + http.Header(*m).Set(field, val) + } + return nil +} + // Match returns true if r matches m. func (m MatchHeader) Match(r *http.Request) bool { for field, allowedFieldVals := range m { @@ -227,6 +289,29 @@ func (m MatchHeader) Match(r *http.Request) bool { return true } +// CaddyModule returns the Caddy module information. +func (MatchHeaderRE) CaddyModule() caddy.ModuleInfo { + return caddy.ModuleInfo{ + Name: "http.matchers.header_regexp", + New: func() caddy.Module { return new(MatchHeaderRE) }, + } +} + +// UnmarshalCaddyfile implements caddyfile.Unmarshaler. +func (m *MatchHeaderRE) UnmarshalCaddyfile(d *caddyfile.Dispenser) error { + if *m == nil { + *m = make(map[string]*MatchRegexp) + } + for d.Next() { + var field, val string + if !d.Args(&field, &val) { + return d.ArgErr() + } + (*m)[field] = &MatchRegexp{Pattern: val} + } + return nil +} + // Match returns true if r matches m. func (m MatchHeaderRE) Match(r *http.Request) bool { for field, rm := range m { @@ -261,6 +346,14 @@ func (m MatchHeaderRE) Validate() error { return nil } +// CaddyModule returns the Caddy module information. +func (MatchProtocol) CaddyModule() caddy.ModuleInfo { + return caddy.ModuleInfo{ + Name: "http.matchers.protocol", + New: func() caddy.Module { return new(MatchProtocol) }, + } +} + // Match returns true if r matches m. func (m MatchProtocol) Match(r *http.Request) bool { switch string(m) { @@ -274,6 +367,26 @@ func (m MatchProtocol) Match(r *http.Request) bool { return false } +// UnmarshalCaddyfile implements caddyfile.Unmarshaler. +func (m *MatchProtocol) UnmarshalCaddyfile(d *caddyfile.Dispenser) error { + for d.Next() { + var proto string + if !d.Args(&proto) { + return d.Err("expected exactly one protocol") + } + *m = MatchProtocol(proto) + } + return nil +} + +// CaddyModule returns the Caddy module information. +func (MatchNegate) CaddyModule() caddy.ModuleInfo { + return caddy.ModuleInfo{ + Name: "http.matchers.not", + New: func() caddy.Module { return new(MatchNegate) }, + } +} + // UnmarshalJSON unmarshals data into m's unexported map field. // This is done because we cannot embed the map directly into // the struct, but we need a struct because we need another @@ -282,6 +395,12 @@ func (m *MatchNegate) UnmarshalJSON(data []byte) error { return json.Unmarshal(data, &m.matchersRaw) } +// UnmarshalCaddyfile implements caddyfile.Unmarshaler. +func (m *MatchNegate) UnmarshalCaddyfile(d *caddyfile.Dispenser) error { + // TODO: figure out how this will work + return nil +} + // Provision loads the matcher modules to be negated. func (m *MatchNegate) Provision(ctx caddy.Context) error { for modName, rawMsg := range m.matchersRaw { @@ -301,6 +420,22 @@ func (m MatchNegate) Match(r *http.Request) bool { return !m.matchers.Match(r) } +// CaddyModule returns the Caddy module information. +func (MatchRemoteIP) CaddyModule() caddy.ModuleInfo { + return caddy.ModuleInfo{ + Name: "http.matchers.remote_ip", + New: func() caddy.Module { return new(MatchRemoteIP) }, + } +} + +// UnmarshalCaddyfile implements caddyfile.Unmarshaler. +func (m *MatchRemoteIP) UnmarshalCaddyfile(d *caddyfile.Dispenser) error { + for d.Next() { + m.Ranges = d.RemainingArgs() + } + return nil +} + // Provision parses m's IP ranges, either from IP or CIDR expressions. func (m *MatchRemoteIP) Provision(ctx caddy.Context) error { for _, str := range m.Ranges { @@ -362,6 +497,14 @@ func (m MatchRemoteIP) Match(r *http.Request) bool { return false } +// CaddyModule returns the Caddy module information. +func (MatchStarlarkExpr) CaddyModule() caddy.ModuleInfo { + return caddy.ModuleInfo{ + Name: "http.matchers.starlark_expr", // TODO: Rename to 'starlark'? + New: func() caddy.Module { return new(MatchStarlarkExpr) }, + } +} + // Match returns true if r matches m. func (m MatchStarlarkExpr) Match(r *http.Request) bool { input := string(m) @@ -379,7 +522,7 @@ func (m MatchStarlarkExpr) Match(r *http.Request) bool { // MatchRegexp is an embeddable type for matching // using regular expressions. type MatchRegexp struct { - Name string `json:"name"` + Name string `json:"name,omitempty"` Pattern string `json:"pattern"` compiled *regexp.Regexp } @@ -431,6 +574,23 @@ func (mre *MatchRegexp) Match(input string, repl caddy.Replacer, scope string) b return true } +// UnmarshalCaddyfile implements caddyfile.Unmarshaler. +func (mre *MatchRegexp) UnmarshalCaddyfile(d *caddyfile.Dispenser) error { + for d.Next() { + args := d.RemainingArgs() + switch len(args) { + case 1: + mre.Pattern = args[0] + case 2: + mre.Name = args[0] + mre.Pattern = args[1] + default: + return d.ArgErr() + } + } + return nil +} + // ResponseMatcher is a type which can determine if a given response // status code and its headers match some criteria. type ResponseMatcher struct { @@ -506,4 +666,14 @@ var ( _ caddy.Provisioner = (*MatchNegate)(nil) _ RequestMatcher = (*MatchStarlarkExpr)(nil) _ caddy.Provisioner = (*MatchRegexp)(nil) + + _ caddyfile.Unmarshaler = (*MatchHost)(nil) + _ caddyfile.Unmarshaler = (*MatchPath)(nil) + _ caddyfile.Unmarshaler = (*MatchPathRE)(nil) + _ caddyfile.Unmarshaler = (*MatchMethod)(nil) + _ caddyfile.Unmarshaler = (*MatchQuery)(nil) + _ caddyfile.Unmarshaler = (*MatchHeader)(nil) + _ caddyfile.Unmarshaler = (*MatchHeaderRE)(nil) + _ caddyfile.Unmarshaler = (*MatchProtocol)(nil) + _ caddyfile.Unmarshaler = (*MatchRemoteIP)(nil) ) diff --git a/modules/caddyhttp/replacer.go b/modules/caddyhttp/replacer.go index 439d245..cc29789 100644 --- a/modules/caddyhttp/replacer.go +++ b/modules/caddyhttp/replacer.go @@ -15,6 +15,7 @@ package caddyhttp import ( + "fmt" "net" "net/http" "net/textproto" @@ -28,6 +29,7 @@ import ( func addHTTPVarsToReplacer(repl caddy.Replacer, req *http.Request, w http.ResponseWriter) { httpVars := func(key string) (string, bool) { if req != nil { + // query string parameters if strings.HasPrefix(key, queryReplPrefix) { vals := req.URL.Query()[key[len(queryReplPrefix):]] // always return true, since the query param might @@ -35,6 +37,7 @@ func addHTTPVarsToReplacer(repl caddy.Replacer, req *http.Request, w http.Respon return strings.Join(vals, ","), true } + // request header fields if strings.HasPrefix(key, reqHeaderReplPrefix) { field := key[len(reqHeaderReplPrefix):] vals := req.Header[textproto.CanonicalMIMEHeaderKey(field)] @@ -43,6 +46,7 @@ func addHTTPVarsToReplacer(repl caddy.Replacer, req *http.Request, w http.Respon return strings.Join(vals, ","), true } + // cookies if strings.HasPrefix(key, cookieReplPrefix) { name := key[len(cookieReplPrefix):] for _, cookie := range req.Cookies() { @@ -87,14 +91,7 @@ func addHTTPVarsToReplacer(repl caddy.Replacer, req *http.Request, w http.Respon return req.URL.RawQuery, true } - if strings.HasPrefix(key, respHeaderReplPrefix) { - field := key[len(respHeaderReplPrefix):] - vals := w.Header()[textproto.CanonicalMIMEHeaderKey(field)] - // always return true, since the header field might - // be present only in some requests - return strings.Join(vals, ","), true - } - + // hostname labels if strings.HasPrefix(key, hostLabelReplPrefix) { idxStr := key[len(hostLabelReplPrefix):] idx, err := strconv.Atoi(idxStr) @@ -111,6 +108,7 @@ func addHTTPVarsToReplacer(repl caddy.Replacer, req *http.Request, w http.Respon return hostLabels[idx], true } + // path parts if strings.HasPrefix(key, pathPartsReplPrefix) { idxStr := key[len(pathPartsReplPrefix):] idx, err := strconv.Atoi(idxStr) @@ -129,9 +127,31 @@ func addHTTPVarsToReplacer(repl caddy.Replacer, req *http.Request, w http.Respon } return pathParts[idx], true } + + // middleware variables + if strings.HasPrefix(key, varsReplPrefix) { + varName := key[len(varsReplPrefix):] + tbl := req.Context().Value(VarCtxKey).(map[string]interface{}) + raw, ok := tbl[varName] + if !ok { + // variables can be dynamic, so always return true + // even when it may not be set; treat as empty + return "", true + } + // do our best to convert it to a string efficiently + switch val := raw.(type) { + case string: + return val, true + case fmt.Stringer: + return val.String(), true + default: + return fmt.Sprintf("%s", val), true + } + } } if w != nil { + // response header fields if strings.HasPrefix(key, respHeaderReplPrefix) { field := key[len(respHeaderReplPrefix):] vals := w.Header()[textproto.CanonicalMIMEHeaderKey(field)] @@ -153,5 +173,6 @@ const ( cookieReplPrefix = "http.request.cookie." hostLabelReplPrefix = "http.request.host.labels." pathPartsReplPrefix = "http.request.uri.path." + varsReplPrefix = "http.var." respHeaderReplPrefix = "http.response.header." ) diff --git a/modules/caddyhttp/requestbody/requestbody.go b/modules/caddyhttp/requestbody/requestbody.go index 3763cfe..9b16250 100644 --- a/modules/caddyhttp/requestbody/requestbody.go +++ b/modules/caddyhttp/requestbody/requestbody.go @@ -22,10 +22,7 @@ import ( ) func init() { - caddy.RegisterModule(caddy.Module{ - Name: "http.handlers.request_body", - New: func() interface{} { return new(RequestBody) }, - }) + caddy.RegisterModule(RequestBody{}) } // RequestBody is a middleware for manipulating the request body. @@ -33,6 +30,14 @@ type RequestBody struct { MaxSize int64 `json:"max_size,omitempty"` } +// CaddyModule returns the Caddy module information. +func (RequestBody) CaddyModule() caddy.ModuleInfo { + return caddy.ModuleInfo{ + Name: "http.handlers.request_body", // TODO: better name for this? + New: func() caddy.Module { return new(RequestBody) }, + } +} + func (rb RequestBody) ServeHTTP(w http.ResponseWriter, r *http.Request, next caddyhttp.Handler) error { if r.Body == nil { return next.ServeHTTP(w, r) diff --git a/modules/caddyhttp/reverseproxy/module.go b/modules/caddyhttp/reverseproxy/module.go index 2e6a338..21aca1d 100755 --- a/modules/caddyhttp/reverseproxy/module.go +++ b/modules/caddyhttp/reverseproxy/module.go @@ -16,12 +16,38 @@ package reverseproxy import ( "github.com/caddyserver/caddy/v2" + "github.com/caddyserver/caddy/v2/caddyconfig/httpcaddyfile" + "github.com/caddyserver/caddy/v2/modules/caddyhttp" ) -// Register caddy module. func init() { - caddy.RegisterModule(caddy.Module{ + caddy.RegisterModule(new(LoadBalanced)) + httpcaddyfile.RegisterHandlerDirective("reverse_proxy", parseCaddyfile) // TODO: "proxy"? +} + +// CaddyModule returns the Caddy module information. +func (*LoadBalanced) CaddyModule() caddy.ModuleInfo { + return caddy.ModuleInfo{ Name: "http.handlers.reverse_proxy", - New: func() interface{} { return new(LoadBalanced) }, - }) + New: func() caddy.Module { return new(LoadBalanced) }, + } +} + +// parseCaddyfile sets up the handler from Caddyfile tokens. Syntax: +// +// proxy [<matcher>] <to> +// +// TODO: This needs to be finished. It definitely needs to be able to open a block... +func parseCaddyfile(h httpcaddyfile.Helper) (caddyhttp.MiddlewareHandler, error) { + lb := new(LoadBalanced) + for h.Next() { + allTo := h.RemainingArgs() + if len(allTo) == 0 { + return nil, h.ArgErr() + } + for _, to := range allTo { + lb.Upstreams = append(lb.Upstreams, &UpstreamConfig{Host: to}) + } + } + return lb, nil } diff --git a/modules/caddyhttp/reverseproxy/upstream.go b/modules/caddyhttp/reverseproxy/upstream.go index 10df80b..1f0693e 100755 --- a/modules/caddyhttp/reverseproxy/upstream.go +++ b/modules/caddyhttp/reverseproxy/upstream.go @@ -179,21 +179,21 @@ type LoadBalanced struct { // The following struct fields are set by caddy configuration. // TryInterval is the max duration for which request retrys will be performed for a request. - TryInterval string `json:"try_interval"` + TryInterval string `json:"try_interval,omitempty"` // Upstreams are the configs for upstream hosts - Upstreams []*UpstreamConfig `json:"upstreams"` + Upstreams []*UpstreamConfig `json:"upstreams,omitempty"` // LoadBalanceType is the string representation of what loadbalancing algorithm to use. i.e. "random" or "round_robin". - LoadBalanceType string `json:"load_balance_type"` + LoadBalanceType string `json:"load_balance_type,omitempty"` // NoHealthyUpstreamsMessage is returned as a response when there are no healthy upstreams to loadbalance to. - NoHealthyUpstreamsMessage string `json:"no_healthy_upstreams_message"` + NoHealthyUpstreamsMessage string `json:"no_healthy_upstreams_message,omitempty"` // TODO :- store healthcheckers as package level state where each upstream gets a single healthchecker // currently a healthchecker is created for each upstream defined, even if a healthchecker was previously created // for that upstream - HealthCheckers []*HealthChecker + HealthCheckers []*HealthChecker `json:"health_checkers,omitempty"` } // Cleanup stops all health checkers on a loadbalanced reverse proxy. @@ -320,22 +320,22 @@ func (lb *LoadBalanced) random() *upstream { // UpstreamConfig represents the config of an upstream. type UpstreamConfig struct { // Host is the host name of the upstream server. - Host string `json:"host"` + Host string `json:"host,omitempty"` // FastHealthCheckDuration is the duration for which a health check is performed when a node is considered unhealthy. - FastHealthCheckDuration string `json:"fast_health_check_duration"` + FastHealthCheckDuration string `json:"fast_health_check_duration,omitempty"` - CircuitBreaker json.RawMessage `json:"circuit_breaker"` + CircuitBreaker json.RawMessage `json:"circuit_breaker,omitempty"` // // CircuitBreakerConfig is the config passed to setup a circuit breaker. - // CircuitBreakerConfig *circuitbreaker.Config `json:"circuit_breaker"` + // CircuitBreakerConfig *circuitbreaker.Config `json:"circuit_breaker,omitempty"` circuitbreaker CircuitBreaker // HealthCheckDuration is the default duration for which a health check is performed. - HealthCheckDuration string `json:"health_check_duration"` + HealthCheckDuration string `json:"health_check_duration,omitempty"` // HealthCheckPath is the path at the upstream host to use for healthchecks. - HealthCheckPath string `json:"health_check_path"` + HealthCheckPath string `json:"health_check_path,omitempty"` } // upstream represents an upstream host. diff --git a/modules/caddyhttp/rewrite/caddyfile.go b/modules/caddyhttp/rewrite/caddyfile.go new file mode 100644 index 0000000..a977a72 --- /dev/null +++ b/modules/caddyhttp/rewrite/caddyfile.go @@ -0,0 +1,37 @@ +// 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 rewrite + +import ( + "github.com/caddyserver/caddy/v2/caddyconfig/httpcaddyfile" + "github.com/caddyserver/caddy/v2/modules/caddyhttp" +) + +func init() { + httpcaddyfile.RegisterHandlerDirective("rewrite", parseCaddyfile) +} + +// parseCaddyfile sets up the handler from Caddyfile tokens. Syntax: +// +// rewrite [<matcher>] <to> +// +// The <to> parameter becomes the new URI. +func parseCaddyfile(h httpcaddyfile.Helper) (caddyhttp.MiddlewareHandler, error) { + var rewr Rewrite + for h.Next() { + rewr.URI = h.Val() + } + return rewr, nil +} diff --git a/modules/caddyhttp/rewrite/rewrite.go b/modules/caddyhttp/rewrite/rewrite.go index ac113ff..f434a38 100644 --- a/modules/caddyhttp/rewrite/rewrite.go +++ b/modules/caddyhttp/rewrite/rewrite.go @@ -24,10 +24,7 @@ import ( ) func init() { - caddy.RegisterModule(caddy.Module{ - Name: "http.handlers.rewrite", - New: func() interface{} { return new(Rewrite) }, - }) + caddy.RegisterModule(Rewrite{}) } // Rewrite is a middleware which can rewrite HTTP requests. @@ -37,6 +34,14 @@ type Rewrite struct { Rehandle bool `json:"rehandle,omitempty"` } +// CaddyModule returns the Caddy module information. +func (Rewrite) CaddyModule() caddy.ModuleInfo { + return caddy.ModuleInfo{ + Name: "http.handlers.rewrite", + New: func() caddy.Module { return new(Rewrite) }, + } +} + func (rewr Rewrite) ServeHTTP(w http.ResponseWriter, r *http.Request, next caddyhttp.Handler) error { repl := r.Context().Value(caddy.ReplacerCtxKey).(caddy.Replacer) var rehandleNeeded bool diff --git a/modules/caddyhttp/routes.go b/modules/caddyhttp/routes.go index b0672b1..1efbad6 100644 --- a/modules/caddyhttp/routes.go +++ b/modules/caddyhttp/routes.go @@ -22,37 +22,38 @@ import ( "github.com/caddyserver/caddy/v2" ) -// ServerRoute represents a set of matching rules, +// Route represents a set of matching rules, // middlewares, and a responder for handling HTTP // requests. -type ServerRoute struct { - Group string `json:"group,omitempty"` - MatcherSets []map[string]json.RawMessage `json:"match,omitempty"` - Handle []json.RawMessage `json:"handle,omitempty"` - Terminal bool `json:"terminal,omitempty"` +type Route struct { + Group string `json:"group,omitempty"` + MatcherSetsRaw []map[string]json.RawMessage `json:"match,omitempty"` + HandlersRaw []json.RawMessage `json:"handle,omitempty"` + Terminal bool `json:"terminal,omitempty"` // decoded values - matcherSets []MatcherSet - handlers []MiddlewareHandler + MatcherSets []MatcherSet `json:"-"` + Handlers []MiddlewareHandler `json:"-"` } // Empty returns true if the route has all zero/default values. -func (sr ServerRoute) Empty() bool { - return len(sr.MatcherSets) == 0 && - len(sr.Handle) == 0 && - len(sr.handlers) == 0 && - !sr.Terminal && - sr.Group == "" +func (r Route) Empty() bool { + return len(r.MatcherSetsRaw) == 0 && + len(r.MatcherSets) == 0 && + len(r.HandlersRaw) == 0 && + len(r.Handlers) == 0 && + !r.Terminal && + r.Group == "" } -func (sr ServerRoute) anyMatcherSetMatches(r *http.Request) bool { - for _, ms := range sr.matcherSets { - if ms.Match(r) { +func (r Route) anyMatcherSetMatches(req *http.Request) bool { + for _, ms := range r.MatcherSets { + if ms.Match(req) { return true } } // if no matchers, always match - return len(sr.matcherSets) == 0 + return len(r.MatcherSets) == 0 } // MatcherSet is a set of matchers which @@ -73,13 +74,13 @@ func (mset MatcherSet) Match(r *http.Request) bool { // RouteList is a list of server routes that can // create a middleware chain. -type RouteList []ServerRoute +type RouteList []Route // Provision sets up all the routes by loading the modules. func (routes RouteList) Provision(ctx caddy.Context) error { for i, route := range routes { // matchers - for _, matcherSet := range route.MatcherSets { + for _, matcherSet := range route.MatcherSetsRaw { var matchers MatcherSet for modName, rawMsg := range matcherSet { val, err := ctx.LoadModule("http.matchers."+modName, rawMsg) @@ -88,19 +89,19 @@ func (routes RouteList) Provision(ctx caddy.Context) error { } matchers = append(matchers, val.(RequestMatcher)) } - routes[i].matcherSets = append(routes[i].matcherSets, matchers) + routes[i].MatcherSets = append(routes[i].MatcherSets, matchers) } - routes[i].MatcherSets = nil // allow GC to deallocate - TODO: Does this help? + routes[i].MatcherSetsRaw = nil // allow GC to deallocate - TODO: Does this help? // handlers - for j, rawMsg := range route.Handle { + for j, rawMsg := range route.HandlersRaw { mh, err := ctx.LoadModuleInline("handler", "http.handlers", rawMsg) if err != nil { return fmt.Errorf("loading handler module in position %d: %v", j, err) } - routes[i].handlers = append(routes[i].handlers, mh.(MiddlewareHandler)) + routes[i].Handlers = append(routes[i].Handlers, mh.(MiddlewareHandler)) } - routes[i].Handle = nil // allow GC to deallocate - TODO: Does this help? + routes[i].HandlersRaw = nil // allow GC to deallocate - TODO: Does this help? } return nil } @@ -135,7 +136,7 @@ func (routes RouteList) BuildCompositeRoute(req *http.Request) Handler { } // apply the rest of the route - for _, mh := range route.handlers { + for _, mh := range route.Handlers { // we have to be sure to wrap mh outside // of our current stack frame so that the // reference to this mh isn't overwritten diff --git a/modules/caddyhttp/server.go b/modules/caddyhttp/server.go index d79d8d3..f820f71 100644 --- a/modules/caddyhttp/server.go +++ b/modules/caddyhttp/server.go @@ -57,7 +57,7 @@ func (s *Server) ServeHTTP(w http.ResponseWriter, r *http.Request) { repl := caddy.NewReplacer() ctx := context.WithValue(r.Context(), caddy.ReplacerCtxKey, repl) ctx = context.WithValue(ctx, ServerCtxKey, s) - ctx = context.WithValue(ctx, TableCtxKey, make(map[string]interface{})) // TODO: Implement this + ctx = context.WithValue(ctx, VarCtxKey, make(map[string]interface{})) r = r.WithContext(ctx) // once the pointer to the request won't change @@ -201,6 +201,14 @@ type AutoHTTPSConfig struct { // that certificates will not be provisioned and managed // for these names. SkipCerts []string `json:"skip_certificates,omitempty"` + + // By default, automatic HTTPS will obtain and renew + // certificates for qualifying hostnames. However, if + // a certificate with a matching SAN is already loaded + // into the cache, certificate management will not be + // enabled. To force automated certificate management + // regardless of loaded certificates, set this to true. + IgnoreLoadedCerts bool `json:"ignore_loaded_certificates,omitempty"` } // Skipped returns true if name is in skipSlice, which @@ -225,6 +233,6 @@ const ( // For referencing the server instance ServerCtxKey caddy.CtxKey = "server" - // For the request's variable table (TODO: implement this) - TableCtxKey caddy.CtxKey = "table" + // For the request's variable table + VarCtxKey caddy.CtxKey = "vars" ) diff --git a/modules/caddyhttp/staticerror.go b/modules/caddyhttp/staticerror.go index 3a8e8bc..3a45366 100644 --- a/modules/caddyhttp/staticerror.go +++ b/modules/caddyhttp/staticerror.go @@ -18,22 +18,26 @@ import ( "fmt" "net/http" "strconv" - "strings" "github.com/caddyserver/caddy/v2" ) func init() { - caddy.RegisterModule(caddy.Module{ - Name: "http.handlers.error", - New: func() interface{} { return new(StaticError) }, - }) + caddy.RegisterModule(StaticError{}) } // StaticError implements a simple handler that returns an error. type StaticError struct { Error string `json:"error,omitempty"` - StatusCode weakString `json:"status_code,omitempty"` + StatusCode WeakString `json:"status_code,omitempty"` +} + +// CaddyModule returns the Caddy module information. +func (StaticError) CaddyModule() caddy.ModuleInfo { + return caddy.ModuleInfo{ + Name: "http.handlers.error", + New: func() caddy.Module { return new(StaticError) }, + } } func (e StaticError) ServeHTTP(w http.ResponseWriter, r *http.Request, _ Handler) error { @@ -53,43 +57,3 @@ func (e StaticError) ServeHTTP(w http.ResponseWriter, r *http.Request, _ Handler // Interface guard var _ MiddlewareHandler = (*StaticError)(nil) - -// weakString is a type that unmarshals any JSON value -// as a string literal, and provides methods for -// getting the value as different primitive types. -// However, using this type removes any type safety -// as far as deserializing JSON is concerned. -type weakString string - -// UnmarshalJSON satisfies json.Unmarshaler. It -// unmarshals b by always interpreting it as a -// string literal. -func (ws *weakString) UnmarshalJSON(b []byte) error { - *ws = weakString(strings.Trim(string(b), `"`)) - return nil -} - -// Int returns ws as an integer. If ws is not an -// integer, 0 is returned. -func (ws weakString) Int() int { - num, _ := strconv.Atoi(string(ws)) - return num -} - -// Float64 returns ws as a float64. If ws is not a -// float value, the zero value is returned. -func (ws weakString) Float64() float64 { - num, _ := strconv.ParseFloat(string(ws), 64) - return num -} - -// Bool returns ws as a boolean. If ws is not a -// boolean, false is returned. -func (ws weakString) Bool() bool { - return string(ws) == "true" -} - -// String returns ws as a string. -func (ws weakString) String() string { - return string(ws) -} diff --git a/modules/caddyhttp/staticresp.go b/modules/caddyhttp/staticresp.go index 291d992..942459b 100644 --- a/modules/caddyhttp/staticresp.go +++ b/modules/caddyhttp/staticresp.go @@ -20,21 +20,61 @@ import ( "strconv" "github.com/caddyserver/caddy/v2" + "github.com/caddyserver/caddy/v2/caddyconfig/caddyfile" ) func init() { - caddy.RegisterModule(caddy.Module{ - Name: "http.handlers.static_response", - New: func() interface{} { return new(StaticResponse) }, - }) + caddy.RegisterModule(StaticResponse{}) + // TODO: Caddyfile directive } // StaticResponse implements a simple responder for static responses. type StaticResponse struct { - StatusCode weakString `json:"status_code"` - Headers http.Header `json:"headers"` - Body string `json:"body"` - Close bool `json:"close"` + StatusCode WeakString `json:"status_code,omitempty"` + Headers http.Header `json:"headers,omitempty"` + Body string `json:"body,omitempty"` + Close bool `json:"close,omitempty"` +} + +// CaddyModule returns the Caddy module information. +func (StaticResponse) CaddyModule() caddy.ModuleInfo { + return caddy.ModuleInfo{ + Name: "http.handlers.static_response", + New: func() caddy.Module { return new(StaticResponse) }, + } +} + +// UnmarshalCaddyfile sets up the handler from Caddyfile tokens. Syntax: +// +// static_response [<matcher>] <status> { +// body <text> +// close +// } +// +func (s *StaticResponse) UnmarshalCaddyfile(d *caddyfile.Dispenser) error { + for d.Next() { + var statusCodeStr string + if d.Args(&statusCodeStr) { + s.StatusCode = WeakString(statusCodeStr) + } + for d.NextBlock() { + switch d.Val() { + case "body": + if s.Body != "" { + return d.Err("body already specified") + } + if !d.Args(&s.Body) { + return d.ArgErr() + } + case "close": + if s.Close { + return d.Err("close already specified") + } + s.Close = true + } + } + } + return nil } func (s StaticResponse) ServeHTTP(w http.ResponseWriter, r *http.Request, _ Handler) error { diff --git a/modules/caddyhttp/staticresp_test.go b/modules/caddyhttp/staticresp_test.go index 49adedd..cd0d1a1 100644 --- a/modules/caddyhttp/staticresp_test.go +++ b/modules/caddyhttp/staticresp_test.go @@ -30,7 +30,7 @@ func TestStaticResponseHandler(t *testing.T) { w := httptest.NewRecorder() s := StaticResponse{ - StatusCode: weakString(strconv.Itoa(http.StatusNotFound)), + StatusCode: WeakString(strconv.Itoa(http.StatusNotFound)), Headers: http.Header{ "X-Test": []string{"Testing"}, }, diff --git a/modules/caddyhttp/subroute.go b/modules/caddyhttp/subroute.go index 9172146..3b0d718 100644 --- a/modules/caddyhttp/subroute.go +++ b/modules/caddyhttp/subroute.go @@ -22,10 +22,7 @@ import ( ) func init() { - caddy.RegisterModule(caddy.Module{ - Name: "http.handlers.subroute", - New: func() interface{} { return new(Subroute) }, - }) + caddy.RegisterModule(Subroute{}) } // Subroute implements a handler that compiles and executes routes. @@ -37,6 +34,14 @@ type Subroute struct { Routes RouteList `json:"routes,omitempty"` } +// CaddyModule returns the Caddy module information. +func (Subroute) CaddyModule() caddy.ModuleInfo { + return caddy.ModuleInfo{ + Name: "http.handlers.subroute", + New: func() caddy.Module { return new(Subroute) }, + } +} + // Provision sets up subrouting. func (sr *Subroute) Provision(ctx caddy.Context) error { if sr.Routes != nil { diff --git a/modules/caddyhttp/table.go b/modules/caddyhttp/table.go deleted file mode 100644 index 5b1fed5..0000000 --- a/modules/caddyhttp/table.go +++ /dev/null @@ -1,55 +0,0 @@ -// 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 caddyhttp - -import ( - "net/http" - - "github.com/caddyserver/caddy/v2" -) - -func init() { - caddy.RegisterModule(caddy.Module{ - Name: "http.handlers.table", - New: func() interface{} { return new(tableMiddleware) }, - }) - - caddy.RegisterModule(caddy.Module{ - Name: "http.matchers.table", - New: func() interface{} { return new(tableMatcher) }, - }) -} - -type tableMiddleware struct { -} - -func (t tableMiddleware) ServeHTTP(w http.ResponseWriter, r *http.Request, next Handler) error { - // tbl := r.Context().Value(TableCtxKey).(map[string]interface{}) - - // TODO: implement this... - - return nil -} - -type tableMatcher struct { -} - -func (m tableMatcher) Match(r *http.Request) bool { - return false // TODO: implement -} - -// Interface guards -var _ MiddlewareHandler = (*tableMiddleware)(nil) -var _ RequestMatcher = (*tableMatcher)(nil) diff --git a/modules/caddyhttp/templates/caddyfile.go b/modules/caddyhttp/templates/caddyfile.go new file mode 100644 index 0000000..d948da0 --- /dev/null +++ b/modules/caddyhttp/templates/caddyfile.go @@ -0,0 +1,62 @@ +// 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 templates + +import ( + "github.com/caddyserver/caddy/v2/caddyconfig/httpcaddyfile" + "github.com/caddyserver/caddy/v2/modules/caddyhttp" +) + +func init() { + httpcaddyfile.RegisterHandlerDirective("templates", parseCaddyfile) +} + +// parseCaddyfile sets up the handler from Caddyfile tokens. Syntax: +// +// templates [<matcher>] { +// mime <types...> +// between <open_delim> <close_delim> +// root <path> +// } +// +func parseCaddyfile(h httpcaddyfile.Helper) (caddyhttp.MiddlewareHandler, error) { + t := new(Templates) + for h.Next() { + for h.NextBlock() { + switch h.Val() { + case "mime": + t.MIMETypes = h.RemainingArgs() + if len(t.MIMETypes) == 0 { + return nil, h.ArgErr() + } + case "between": + t.Delimiters = h.RemainingArgs() + if len(t.Delimiters) != 2 { + return nil, h.ArgErr() + } + case "root": + if !h.Args(&t.IncludeRoot) { + return nil, h.ArgErr() + } + } + } + } + + if t.IncludeRoot == "" { + t.IncludeRoot = "{http.var.root}" + } + + return t, nil +} diff --git a/modules/caddyhttp/templates/templates.go b/modules/caddyhttp/templates/templates.go index 9a41b6d..1cd347c 100644 --- a/modules/caddyhttp/templates/templates.go +++ b/modules/caddyhttp/templates/templates.go @@ -27,10 +27,7 @@ import ( ) func init() { - caddy.RegisterModule(caddy.Module{ - Name: "http.handlers.templates", - New: func() interface{} { return new(Templates) }, - }) + caddy.RegisterModule(Templates{}) } // Templates is a middleware which execute response bodies as templates. @@ -40,6 +37,14 @@ type Templates struct { Delimiters []string `json:"delimiters,omitempty"` } +// CaddyModule returns the Caddy module information. +func (Templates) CaddyModule() caddy.ModuleInfo { + return caddy.ModuleInfo{ + Name: "http.handlers.templates", + New: func() caddy.Module { return new(Templates) }, + } +} + // Provision provisions t. func (t *Templates) Provision(ctx caddy.Context) error { if t.MIMETypes == nil { @@ -108,7 +113,8 @@ func (t *Templates) ServeHTTP(w http.ResponseWriter, r *http.Request, next caddy func (t *Templates) executeTemplate(rr caddyhttp.ResponseRecorder, r *http.Request) error { var fs http.FileSystem if t.IncludeRoot != "" { - fs = http.Dir(t.IncludeRoot) + repl := r.Context().Value(caddy.ReplacerCtxKey).(caddy.Replacer) + fs = http.Dir(repl.ReplaceAll(t.IncludeRoot, ".")) } ctx := &templateContext{ diff --git a/modules/caddyhttp/templates/tplcontext.go b/modules/caddyhttp/templates/tplcontext.go index ffcc636..a51e54b 100644 --- a/modules/caddyhttp/templates/tplcontext.go +++ b/modules/caddyhttp/templates/tplcontext.go @@ -136,19 +136,6 @@ func (c templateContext) Cookie(name string) string { return "" } -// Hostname gets the (remote) hostname of the client making the request. -// Performance warning: This involves a DNS lookup. -func (c templateContext) Hostname() string { - ip := c.RemoteIP() - - hostnameList, err := net.LookupAddr(ip) - if err != nil || len(hostnameList) == 0 { - return c.Req.RemoteAddr - } - - return hostnameList[0] -} - // RemoteIP gets the IP address of the client making the request. func (c templateContext) RemoteIP() string { ip, _, err := net.SplitHostPort(c.Req.RemoteAddr) diff --git a/modules/caddyhttp/vars.go b/modules/caddyhttp/vars.go new file mode 100644 index 0000000..bbd4568 --- /dev/null +++ b/modules/caddyhttp/vars.go @@ -0,0 +1,81 @@ +// 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 caddyhttp + +import ( + "net/http" + + "github.com/caddyserver/caddy/v2" +) + +func init() { + caddy.RegisterModule(VarsMiddleware{}) + caddy.RegisterModule(VarsMatcher{}) +} + +// VarsMiddleware is an HTTP middleware which sets variables +// in the context, mainly for use by placeholders. +type VarsMiddleware map[string]string + +// CaddyModule returns the Caddy module information. +func (VarsMiddleware) CaddyModule() caddy.ModuleInfo { + return caddy.ModuleInfo{ + Name: "http.handlers.vars", + New: func() caddy.Module { return new(VarsMiddleware) }, + } +} + +func (t VarsMiddleware) ServeHTTP(w http.ResponseWriter, r *http.Request, next Handler) error { + vars := r.Context().Value(VarCtxKey).(map[string]interface{}) + repl := r.Context().Value(caddy.ReplacerCtxKey).(caddy.Replacer) + for k, v := range t { + keyExpanded := repl.ReplaceAll(k, "") + valExpanded := repl.ReplaceAll(v, "") + vars[keyExpanded] = valExpanded + } + return next.ServeHTTP(w, r) +} + +// VarsMatcher is an HTTP request matcher which can match +// requests based on variables in the context. +type VarsMatcher map[string]string + +// CaddyModule returns the Caddy module information. +func (VarsMatcher) CaddyModule() caddy.ModuleInfo { + return caddy.ModuleInfo{ + Name: "http.matchers.vars", + New: func() caddy.Module { return new(VarsMatcher) }, + } +} + +// Match matches a request based on variables in the context. +func (m VarsMatcher) Match(r *http.Request) bool { + vars := r.Context().Value(VarCtxKey).(map[string]string) + repl := r.Context().Value(caddy.ReplacerCtxKey).(caddy.Replacer) + for k, v := range m { + keyExpanded := repl.ReplaceAll(k, "") + valExpanded := repl.ReplaceAll(v, "") + if vars[keyExpanded] != valExpanded { + return false + } + } + return true +} + +// Interface guards +var ( + _ MiddlewareHandler = (*VarsMiddleware)(nil) + _ RequestMatcher = (*VarsMatcher)(nil) +) diff --git a/modules/caddytls/acmemanager.go b/modules/caddytls/acmemanager.go index 578cdb3..36f1c21 100644 --- a/modules/caddytls/acmemanager.go +++ b/modules/caddytls/acmemanager.go @@ -28,10 +28,7 @@ import ( ) func init() { - caddy.RegisterModule(caddy.Module{ - Name: "tls.management.acme", - New: func() interface{} { return new(ACMEManagerMaker) }, - }) + caddy.RegisterModule(ACMEManagerMaker{}) } // ACMEManagerMaker makes an ACME manager @@ -57,9 +54,17 @@ type ACMEManagerMaker struct { keyType certcrypto.KeyType } -// newManager is a no-op to satisfy the ManagerMaker interface, +// CaddyModule returns the Caddy module information. +func (ACMEManagerMaker) CaddyModule() caddy.ModuleInfo { + return caddy.ModuleInfo{ + Name: "tls.management.acme", + New: func() caddy.Module { return new(ACMEManagerMaker) }, + } +} + +// NewManager is a no-op to satisfy the ManagerMaker interface, // because this manager type is a special case. -func (m *ACMEManagerMaker) newManager(interactive bool) (certmagic.Manager, error) { +func (m ACMEManagerMaker) NewManager(interactive bool) (certmagic.Manager, error) { return nil, nil } @@ -203,4 +208,4 @@ func onDemandAskRequest(ask string, name string) error { } // Interface guard -var _ managerMaker = (*ACMEManagerMaker)(nil) +var _ ManagerMaker = (*ACMEManagerMaker)(nil) diff --git a/modules/caddytls/connpolicy.go b/modules/caddytls/connpolicy.go index 8cb6ffe..e061281 100644 --- a/modules/caddytls/connpolicy.go +++ b/modules/caddytls/connpolicy.go @@ -172,7 +172,7 @@ func (p *ConnectionPolicy) buildStandardTLSConfig(ctx caddy.Context) error { // add all the cipher suites in order, without duplicates cipherSuitesAdded := make(map[uint16]struct{}) for _, csName := range p.CipherSuites { - csID := supportedCipherSuites[csName] + csID := SupportedCipherSuites[csName] if _, ok := cipherSuitesAdded[csID]; !ok { cipherSuitesAdded[csID] = struct{}{} cfg.CipherSuites = append(cfg.CipherSuites, csID) @@ -182,7 +182,7 @@ func (p *ConnectionPolicy) buildStandardTLSConfig(ctx caddy.Context) error { // add all the curve preferences in order, without duplicates curvesAdded := make(map[tls.CurveID]struct{}) for _, curveName := range p.Curves { - curveID := supportedCurves[curveName] + curveID := SupportedCurves[curveName] if _, ok := curvesAdded[curveID]; !ok { curvesAdded[curveID] = struct{}{} cfg.CurvePreferences = append(cfg.CurvePreferences, curveID) @@ -203,10 +203,10 @@ func (p *ConnectionPolicy) buildStandardTLSConfig(ctx caddy.Context) error { // min and max protocol versions if p.ProtocolMin != "" { - cfg.MinVersion = supportedProtocols[p.ProtocolMin] + cfg.MinVersion = SupportedProtocols[p.ProtocolMin] } if p.ProtocolMax != "" { - cfg.MaxVersion = supportedProtocols[p.ProtocolMax] + cfg.MaxVersion = SupportedProtocols[p.ProtocolMax] } if p.ProtocolMin > p.ProtocolMax { return fmt.Errorf("protocol min (%x) cannot be greater than protocol max (%x)", p.ProtocolMin, p.ProtocolMax) diff --git a/modules/caddytls/fileloader.go b/modules/caddytls/fileloader.go index 5f277c8..b2cc132 100644 --- a/modules/caddytls/fileloader.go +++ b/modules/caddytls/fileloader.go @@ -23,14 +23,19 @@ import ( ) func init() { - caddy.RegisterModule(caddy.Module{ - Name: "tls.certificates.load_files", - New: func() interface{} { return fileLoader{} }, - }) + caddy.RegisterModule(FileLoader{}) } -// fileLoader loads certificates and their associated keys from disk. -type fileLoader []CertKeyFilePair +// FileLoader loads certificates and their associated keys from disk. +type FileLoader []CertKeyFilePair + +// CaddyModule returns the Caddy module information. +func (FileLoader) CaddyModule() caddy.ModuleInfo { + return caddy.ModuleInfo{ + Name: "tls.certificates.load_files", + New: func() caddy.Module { return new(FileLoader) }, + } +} // CertKeyFilePair pairs certificate and key file names along with their // encoding format so that they can be loaded from disk. @@ -42,7 +47,7 @@ type CertKeyFilePair struct { } // LoadCertificates returns the certificates to be loaded by fl. -func (fl fileLoader) LoadCertificates() ([]Certificate, error) { +func (fl FileLoader) LoadCertificates() ([]Certificate, error) { var certs []Certificate for _, pair := range fl { certData, err := ioutil.ReadFile(pair.Certificate) @@ -73,4 +78,4 @@ func (fl fileLoader) LoadCertificates() ([]Certificate, error) { } // Interface guard -var _ CertificateLoader = (fileLoader)(nil) +var _ CertificateLoader = (FileLoader)(nil) diff --git a/modules/caddytls/folderloader.go b/modules/caddytls/folderloader.go index 24a7fbb..da1dff0 100644 --- a/modules/caddytls/folderloader.go +++ b/modules/caddytls/folderloader.go @@ -28,22 +28,27 @@ import ( ) func init() { - caddy.RegisterModule(caddy.Module{ - Name: "tls.certificates.load_folders", - New: func() interface{} { return folderLoader{} }, - }) + caddy.RegisterModule(FolderLoader{}) } -// folderLoader loads certificates and their associated keys from disk +// FolderLoader loads certificates and their associated keys from disk // by recursively walking the specified directories, looking for PEM // files which contain both a certificate and a key. -type folderLoader []string +type FolderLoader []string + +// CaddyModule returns the Caddy module information. +func (FolderLoader) CaddyModule() caddy.ModuleInfo { + return caddy.ModuleInfo{ + Name: "tls.certificates.load_folders", + New: func() caddy.Module { return new(FolderLoader) }, + } +} // LoadCertificates loads all the certificates+keys in the directories // listed in fl from all files ending with .pem. This method of loading // certificates expects the certificate and key to be bundled into the // same file. -func (fl folderLoader) LoadCertificates() ([]Certificate, error) { +func (fl FolderLoader) LoadCertificates() ([]Certificate, error) { var certs []Certificate for _, dir := range fl { err := filepath.Walk(dir, func(fpath string, info os.FileInfo, err error) error { @@ -135,4 +140,4 @@ func x509CertFromCertAndKeyPEMFile(fpath string) (tls.Certificate, error) { return cert, nil } -var _ CertificateLoader = (folderLoader)(nil) +var _ CertificateLoader = (FolderLoader)(nil) diff --git a/modules/caddytls/matchers.go b/modules/caddytls/matchers.go index ee146d4..47fb296 100644 --- a/modules/caddytls/matchers.go +++ b/modules/caddytls/matchers.go @@ -20,14 +20,19 @@ import ( "github.com/caddyserver/caddy/v2" ) +func init() { + caddy.RegisterModule(MatchServerName{}) +} + // MatchServerName matches based on SNI. type MatchServerName []string -func init() { - caddy.RegisterModule(caddy.Module{ +// CaddyModule returns the Caddy module information. +func (MatchServerName) CaddyModule() caddy.ModuleInfo { + return caddy.ModuleInfo{ Name: "tls.handshake_match.sni", - New: func() interface{} { return MatchServerName{} }, - }) + New: func() caddy.Module { return new(MatchServerName) }, + } } // Match matches hello based on SNI. diff --git a/modules/caddytls/sessiontickets.go b/modules/caddytls/sessiontickets.go index c47f823..2eb0773 100644 --- a/modules/caddytls/sessiontickets.go +++ b/modules/caddytls/sessiontickets.go @@ -29,7 +29,7 @@ import ( // SessionTicketService configures and manages TLS session tickets. type SessionTicketService struct { KeySource json.RawMessage `json:"key_source,omitempty"` - RotationInterval caddy.Duration `json:"rotation_interval,omitempty"` + RotationInterval caddy.Duration `json:"rotation_interval,omitempty"` MaxKeys int `json:"max_keys,omitempty"` DisableRotation bool `json:"disable_rotation,omitempty"` Disabled bool `json:"disabled,omitempty"` diff --git a/modules/caddytls/standardstek/stek.go b/modules/caddytls/standardstek/stek.go index 6a4b1c8..6d10c76 100644 --- a/modules/caddytls/standardstek/stek.go +++ b/modules/caddytls/standardstek/stek.go @@ -24,10 +24,7 @@ import ( ) func init() { - caddy.RegisterModule(caddy.Module{ - Name: "tls.stek.standard", - New: func() interface{} { return new(standardSTEKProvider) }, - }) + caddy.RegisterModule(standardSTEKProvider{}) } type standardSTEKProvider struct { @@ -35,6 +32,14 @@ type standardSTEKProvider struct { timer *time.Timer } +// CaddyModule returns the Caddy module information. +func (standardSTEKProvider) CaddyModule() caddy.ModuleInfo { + return caddy.ModuleInfo{ + Name: "tls.stek.standard", + New: func() caddy.Module { return new(standardSTEKProvider) }, + } +} + // Initialize sets the configuration for s and returns the starting keys. func (s *standardSTEKProvider) Initialize(config *caddytls.SessionTicketService) ([][32]byte, error) { // keep a reference to the config; we'll need it when rotating keys diff --git a/modules/caddytls/tls.go b/modules/caddytls/tls.go index e70fbd1..88b7790 100644 --- a/modules/caddytls/tls.go +++ b/modules/caddytls/tls.go @@ -30,10 +30,7 @@ import ( ) func init() { - caddy.RegisterModule(caddy.Module{ - Name: "tls", - New: func() interface{} { return new(TLS) }, - }) + caddy.RegisterModule(TLS{}) // opt-in TLS 1.3 for Go1.12 // TODO: remove this line when Go1.13 is released. @@ -45,14 +42,22 @@ func init() { // TLS represents a process-wide TLS configuration. type TLS struct { Certificates map[string]json.RawMessage `json:"certificates,omitempty"` - Automation AutomationConfig `json:"automation,omitempty"` - SessionTickets SessionTicketService `json:"session_tickets,omitempty"` + Automation AutomationConfig `json:"automation"` + SessionTickets SessionTicketService `json:"session_tickets"` certificateLoaders []CertificateLoader certCache *certmagic.Cache ctx caddy.Context } +// CaddyModule returns the Caddy module information. +func (TLS) CaddyModule() caddy.ModuleInfo { + return caddy.ModuleInfo{ + Name: "tls", + New: func() caddy.Module { return new(TLS) }, + } +} + // Provision sets up the configuration for the TLS app. func (t *TLS) Provision(ctx caddy.Context) error { t.ctx = ctx @@ -73,7 +78,7 @@ func (t *TLS) Provision(ctx caddy.Context) error { if err != nil { return fmt.Errorf("loading TLS automation management module: %s", err) } - t.Automation.Policies[i].Management = val.(managerMaker) + t.Automation.Policies[i].Management = val.(ManagerMaker) t.Automation.Policies[i].ManagementRaw = nil // allow GC to deallocate - TODO: Does this help? } @@ -105,16 +110,12 @@ func (t *TLS) Provision(ctx caddy.Context) error { onDemandRateLimiter.SetLimit(0) } - return nil -} - -// Start activates the TLS module. -func (t *TLS) Start() error { + // load manual/static (unmanaged) certificates - we do this in + // provision so that other apps (such as http) can know which + // certificates have been manually loaded magic := certmagic.New(t.certCache, certmagic.Config{ - Storage: t.ctx.Storage(), + Storage: ctx.Storage(), }) - - // load manual/static (unmanaged) certificates for _, loader := range t.certificateLoaders { certs, err := loader.LoadCertificates() if err != nil { @@ -128,6 +129,11 @@ func (t *TLS) Start() error { } } + return nil +} + +// Start activates the TLS module. +func (t *TLS) Start() error { // load automated (managed) certificates if automatedRawMsg, ok := t.Certificates[automateKey]; ok { var names []string @@ -204,6 +210,12 @@ func (t *TLS) getAutomationPolicyForName(name string) AutomationPolicy { return AutomationPolicy{Management: mgmt} } +// CertificatesWithSAN returns the list of all certificates +// in the cache the match the given SAN value. +func (t *TLS) CertificatesWithSAN(san string) []certmagic.Certificate { + return t.certCache.CertificatesWithSAN(san) +} + // CertificateLoader is a type that can load certificates. // Certificates can optionally be associated with tags. type CertificateLoader interface { @@ -230,7 +242,7 @@ type AutomationPolicy struct { Hosts []string `json:"hosts,omitempty"` ManagementRaw json.RawMessage `json:"management,omitempty"` - Management managerMaker `json:"-"` + Management ManagerMaker `json:"-"` } // makeCertMagicConfig converts ap into a CertMagic config. Passing onDemand @@ -245,7 +257,7 @@ func (ap AutomationPolicy) makeCertMagicConfig(ctx caddy.Context) certmagic.Conf } return certmagic.Config{ - NewManager: ap.Management.newManager, + NewManager: ap.Management.NewManager, } } @@ -283,9 +295,9 @@ type RateLimit struct { Burst int `json:"burst,omitempty"` } -// managerMaker makes a certificate manager. -type managerMaker interface { - newManager(interactive bool) (certmagic.Manager, error) +// ManagerMaker makes a certificate manager. +type ManagerMaker interface { + NewManager(interactive bool) (certmagic.Manager, error) } // These perpetual values are used for on-demand TLS. diff --git a/modules/caddytls/values.go b/modules/caddytls/values.go index 0c62058..b10fe22 100644 --- a/modules/caddytls/values.go +++ b/modules/caddytls/values.go @@ -22,12 +22,16 @@ import ( "github.com/klauspost/cpuid" ) -// supportedCipherSuites is the unordered map of cipher suite +// SupportedCipherSuites is the unordered map of cipher suite // string names to their definition in crypto/tls. All values // should be IANA-reserved names. See // https://www.iana.org/assignments/tls-parameters/tls-parameters.xhtml +// Two of the cipher suite constants in the standard lib do not use the +// full IANA name, but we do; see: +// https://github.com/golang/go/issues/32061 and +// https://github.com/golang/go/issues/30325#issuecomment-512862374. // TODO: might not be needed much longer: https://github.com/golang/go/issues/30325 -var supportedCipherSuites = map[string]uint16{ +var SupportedCipherSuites = map[string]uint16{ "TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384": tls.TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384, "TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384": tls.TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384, "TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256": tls.TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256, @@ -84,22 +88,24 @@ func getOptimalDefaultCipherSuites() []uint16 { return defaultCipherSuitesWithoutAESNI } -// supportedCurves is the unordered map of supported curves. +// SupportedCurves is the unordered map of supported curves. // https://golang.org/pkg/crypto/tls/#CurveID -var supportedCurves = map[string]tls.CurveID{ - "X25519": tls.X25519, - "P256": tls.CurveP256, - "P384": tls.CurveP384, - "P521": tls.CurveP521, +var SupportedCurves = map[string]tls.CurveID{ + // TODO: Use IANA names, probably? see https://www.iana.org/assignments/tls-parameters/tls-parameters.xhtml#tls-parameters-8 + // All named crypto/elliptic curves have secpXXXr1 IANA names. + "x25519": tls.X25519, // x25519, 29 + "p256": tls.CurveP256, // secp256r1, 23 + "p384": tls.CurveP384, // secp384r1, 24 + "p521": tls.CurveP521, // secp521r1, 25 } // supportedCertKeyTypes is all the key types that are supported // for certificates that are obtained through ACME. var supportedCertKeyTypes = map[string]certcrypto.KeyType{ - "RSA2048": certcrypto.RSA2048, - "RSA4096": certcrypto.RSA4096, - "P256": certcrypto.EC256, - "P384": certcrypto.EC384, + "rsa_2048": certcrypto.RSA2048, + "rsa_4096": certcrypto.RSA4096, + "ec_p256": certcrypto.EC256, + "ec_p384": certcrypto.EC384, } // defaultCurves is the list of only the curves we want to use @@ -115,9 +121,9 @@ var defaultCurves = []tls.CurveID{ tls.CurveP256, } -// supportedProtocols is a map of supported protocols. -// HTTP/2 only supports TLS 1.2 and higher. -var supportedProtocols = map[string]uint16{ +// SupportedProtocols is a map of supported protocols. +// Note that HTTP/2 only supports TLS 1.2 and higher. +var SupportedProtocols = map[string]uint16{ "tls1.0": tls.VersionTLS10, "tls1.1": tls.VersionTLS11, "tls1.2": tls.VersionTLS12, |