Skip to content

Commit

Permalink
Merge branch 'master' into tls-cert-cache-internal-reload
Browse files Browse the repository at this point in the history
  • Loading branch information
mholt authored Apr 24, 2024
2 parents 15d6031 + 76c4cf5 commit c66e0ee
Show file tree
Hide file tree
Showing 7 changed files with 227 additions and 67 deletions.
1 change: 1 addition & 0 deletions caddyconfig/httpcaddyfile/options.go
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,7 @@ func init() {
RegisterGlobalOption("auto_https", parseOptAutoHTTPS)
RegisterGlobalOption("servers", parseServerOptions)
RegisterGlobalOption("ocsp_stapling", parseOCSPStaplingOptions)
RegisterGlobalOption("cert_lifetime", parseOptDuration)
RegisterGlobalOption("log", parseLogOptions)
RegisterGlobalOption("preferred_chains", parseOptPreferredChains)
RegisterGlobalOption("persist_config", parseOptPersistConfig)
Expand Down
5 changes: 5 additions & 0 deletions caddyconfig/httpcaddyfile/tlsapp.go
Original file line number Diff line number Diff line change
Expand Up @@ -456,6 +456,7 @@ func fillInGlobalACMEDefaults(issuer certmagic.Issuer, options map[string]any) e
globalACMEDNS := options["acme_dns"]
globalACMEEAB := options["acme_eab"]
globalPreferredChains := options["preferred_chains"]
globalCertLifetime := options["cert_lifetime"]

if globalEmail != nil && acmeIssuer.Email == "" {
acmeIssuer.Email = globalEmail.(string)
Expand All @@ -479,6 +480,10 @@ func fillInGlobalACMEDefaults(issuer certmagic.Issuer, options map[string]any) e
if globalPreferredChains != nil && acmeIssuer.PreferredChains == nil {
acmeIssuer.PreferredChains = globalPreferredChains.(*caddytls.ChainPreference)
}

if globalCertLifetime != nil && acmeIssuer.CertificateLifetime == 0 {
acmeIssuer.CertificateLifetime = globalCertLifetime.(caddy.Duration)
}
return nil
}

Expand Down
1 change: 1 addition & 0 deletions caddytest/integration/testdata/foo.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
foo
6 changes: 6 additions & 0 deletions modules/caddyhttp/templates/tplcontext.go
Original file line number Diff line number Diff line change
Expand Up @@ -249,6 +249,12 @@ func (c *TemplateContext) executeTemplateInBuffer(tplName string, buf *bytes.Buf

func (c TemplateContext) funcPlaceholder(name string) string {
repl := c.Req.Context().Value(caddy.ReplacerCtxKey).(*caddy.Replacer)

// For safety, we don't want to allow the file placeholder in
// templates because it could be used to read arbitrary files
// if the template contents were not trusted.
repl = repl.WithoutFile()

value, _ := repl.GetString(name)
return value
}
Expand Down
24 changes: 24 additions & 0 deletions modules/caddytls/acmeissuer.go
Original file line number Diff line number Diff line change
Expand Up @@ -88,6 +88,15 @@ type ACMEIssuer struct {
// will be selected.
PreferredChains *ChainPreference `json:"preferred_chains,omitempty"`

// The validity period to ask the CA to issue a certificate for.
// Default: 0 (CA chooses lifetime).
// This value is used to compute the "notAfter" field of the ACME order;
// therefore the system must have a reasonably synchronized clock.
// NOTE: Not all CAs support this. Check with your CA's ACME
// documentation to see if this is allowed and what values may
// be used. EXPERIMENTAL: Subject to change.
CertificateLifetime caddy.Duration `json:"certificate_lifetime,omitempty"`

rootPool *x509.CertPool
logger *zap.Logger

Expand Down Expand Up @@ -178,6 +187,7 @@ func (iss *ACMEIssuer) makeIssuerTemplate() (certmagic.ACMEIssuer, error) {
CertObtainTimeout: time.Duration(iss.ACMETimeout),
TrustedRoots: iss.rootPool,
ExternalAccount: iss.ExternalAccount,
NotAfter: time.Duration(iss.CertificateLifetime),
Logger: iss.logger,
}

Expand Down Expand Up @@ -349,6 +359,20 @@ func (iss *ACMEIssuer) UnmarshalCaddyfile(d *caddyfile.Dispenser) error {

for d.NextBlock(0) {
switch d.Val() {
case "lifetime":
var lifetimeStr string
if !d.AllArgs(&lifetimeStr) {
return d.ArgErr()
}
lifetime, err := caddy.ParseDuration(lifetimeStr)
if err != nil {
return d.Errf("invalid lifetime %s: %v", lifetimeStr, err)
}
if lifetime < 0 {
return d.Errf("lifetime must be >= 0: %s", lifetime)
}
iss.CertificateLifetime = caddy.Duration(lifetime)

Check failure on line 375 in modules/caddytls/acmeissuer.go

View workflow job for this annotation

GitHub Actions / lint (linux)

File is not `gci`-ed with --skip-generated -s standard -s default -s prefix(github.com/caddyserver/caddy/v2/cmd) -s prefix(github.com/caddyserver/caddy) --custom-order (gci)
case "dir":
if iss.CA != "" {
return d.Errf("directory is already specified: %s", iss.CA)
Expand Down
107 changes: 92 additions & 15 deletions replacer.go
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ package caddy

import (
"fmt"
"io"
"net/http"
"os"
"path/filepath"
Expand All @@ -24,6 +25,8 @@ import (
"strings"
"sync"
"time"

"go.uber.org/zap"
)

// NewReplacer returns a new Replacer.
Expand All @@ -32,9 +35,10 @@ func NewReplacer() *Replacer {
static: make(map[string]any),
mapMutex: &sync.RWMutex{},
}
rep.providers = []ReplacerFunc{
globalDefaultReplacements,
rep.fromStatic,
rep.providers = []replacementProvider{
globalDefaultReplacementProvider{},
fileReplacementProvider{},
ReplacerFunc(rep.fromStatic),
}
return rep
}
Expand All @@ -46,8 +50,8 @@ func NewEmptyReplacer() *Replacer {
static: make(map[string]any),
mapMutex: &sync.RWMutex{},
}
rep.providers = []ReplacerFunc{
rep.fromStatic,
rep.providers = []replacementProvider{
ReplacerFunc(rep.fromStatic),
}
return rep
}
Expand All @@ -56,10 +60,25 @@ func NewEmptyReplacer() *Replacer {
// A default/empty Replacer is not valid;
// use NewReplacer to make one.
type Replacer struct {
providers []ReplacerFunc
providers []replacementProvider
static map[string]any
mapMutex *sync.RWMutex
}

static map[string]any
mapMutex *sync.RWMutex
// WithoutFile returns a copy of the current Replacer
// without support for the {file.*} placeholder, which
// may be unsafe in some contexts.
//
// EXPERIMENTAL: Subject to change or removal.
func (r *Replacer) WithoutFile() *Replacer {
rep := &Replacer{static: r.static}
for _, v := range r.providers {
if _, ok := v.(fileReplacementProvider); ok {
continue
}
rep.providers = append(rep.providers, v)
}
return rep
}

// Map adds mapFunc to the list of value providers.
Expand All @@ -79,7 +98,7 @@ func (r *Replacer) Set(variable string, value any) {
// the value and whether the variable was known.
func (r *Replacer) Get(variable string) (any, bool) {
for _, mapFunc := range r.providers {
if val, ok := mapFunc(variable); ok {
if val, ok := mapFunc.replace(variable); ok {
return val, true
}
}
Expand Down Expand Up @@ -298,14 +317,52 @@ func ToString(val any) string {
}
}

// ReplacerFunc is a function that returns a replacement
// for the given key along with true if the function is able
// to service that key (even if the value is blank). If the
// function does not recognize the key, false should be
// returned.
// ReplacerFunc is a function that returns a replacement for the
// given key along with true if the function is able to service
// that key (even if the value is blank). If the function does
// not recognize the key, false should be returned.
type ReplacerFunc func(key string) (any, bool)

func globalDefaultReplacements(key string) (any, bool) {
func (f ReplacerFunc) replace(key string) (any, bool) {
return f(key)
}

// replacementProvider is a type that can provide replacements
// for placeholders. Allows for type assertion to determine
// which type of provider it is.
type replacementProvider interface {
replace(key string) (any, bool)
}

// fileReplacementsProvider handles {file.*} replacements,
// reading a file from disk and replacing with its contents.
type fileReplacementProvider struct{}

func (f fileReplacementProvider) replace(key string) (any, bool) {
if !strings.HasPrefix(key, filePrefix) {
return nil, false
}

filename := key[len(filePrefix):]
maxSize := 1024 * 1024
body, err := readFileIntoBuffer(filename, maxSize)
if err != nil {
wd, _ := os.Getwd()
Log().Error("placeholder: failed to read file",
zap.String("file", filename),
zap.String("working_dir", wd),
zap.Error(err))
return nil, true
}
return string(body), true
}

// globalDefaultReplacementsProvider handles replacements
// that can be used in any context, such as system variables,
// time, or environment variables.
type globalDefaultReplacementProvider struct{}

func (f globalDefaultReplacementProvider) replace(key string) (any, bool) {
// check environment variable
const envPrefix = "env."
if strings.HasPrefix(key, envPrefix) {
Expand Down Expand Up @@ -347,6 +404,24 @@ func globalDefaultReplacements(key string) (any, bool) {
return nil, false
}

// readFileIntoBuffer reads the file at filePath into a size limited buffer.
func readFileIntoBuffer(filename string, size int) ([]byte, error) {
file, err := os.Open(filename)
if err != nil {
return nil, err
}
defer file.Close()

buffer := make([]byte, size)
n, err := file.Read(buffer)
if err != nil && err != io.EOF {
return nil, err
}

// slice the buffer to the actual size
return buffer[:n], nil
}

// ReplacementFunc is a function that is called when a
// replacement is being performed. It receives the
// variable (i.e. placeholder name) and the value that
Expand All @@ -363,3 +438,5 @@ var nowFunc = time.Now
const ReplacerCtxKey CtxKey = "replacer"

const phOpen, phClose, phEscape = '{', '}', '\\'

const filePrefix = "file."
Loading

0 comments on commit c66e0ee

Please sign in to comment.