mirror of
https://github.com/caddyserver/caddy.git
synced 2025-03-27 15:44:50 +01:00
* caddytls: Add support for ZeroSSL; add Caddyfile support for issuers Configuring issuers explicitly in a Caddyfile is not easily compatible with existing ACME-specific parameters such as email or acme_ca which infer the kind of issuer it creates (this is complicated now because the ZeroSSL issuer wraps the ACME issuer)... oh well, we can revisit that later if we need to. New Caddyfile global option: { cert_issuer <name> ... } Or, alternatively, as a tls subdirective: tls { issuer <name> ... } For example, to use ZeroSSL with an API key: { cert_issuser zerossl API_KEY } For now, that still uses ZeroSSL's ACME endpoint; it fetches EAB credentials for you. You can also provide the EAB credentials directly just like any other ACME endpoint: { cert_issuer acme { eab KEY_ID MAC_KEY } } All these examples use the new global option (or tls subdirective). You can still use traditional/existing options with ZeroSSL, since it's just another ACME endpoint: { acme_ca https://acme.zerossl.com/v2/DV90 acme_eab KEY_ID MAC_KEY } That's all there is to it. You just can't mix-and-match acme_* options with cert_issuer, because it becomes confusing/ambiguous/complicated to merge the settings. * Fix broken test This test was asserting buggy behavior, oops - glad this branch both discovers and fixes the bug at the same time! * Fix broken test (post-merge) * Update modules/caddytls/acmeissuer.go Fix godoc comment Co-authored-by: Francis Lavoie <lavofr@gmail.com> * Add support for ZeroSSL's EAB-by-email endpoint Also transform the ACMEIssuer into ZeroSSLIssuer implicitly if set to the ZeroSSL endpoint without EAB (the ZeroSSLIssuer is needed to generate EAB if not already provided); this is now possible with either an API key or an email address. * go.mod: Use latest certmagic, acmez, and x/net * Wrap underlying logic rather than repeating it Oops, duh * Form-encode email info into request body for EAB endpoint Co-authored-by: Francis Lavoie <lavofr@gmail.com>
672 lines
19 KiB
Go
672 lines
19 KiB
Go
// 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 httpcaddyfile
|
|
|
|
import (
|
|
"encoding/base64"
|
|
"encoding/pem"
|
|
"fmt"
|
|
"html"
|
|
"io/ioutil"
|
|
"net/http"
|
|
"reflect"
|
|
"strings"
|
|
|
|
"github.com/caddyserver/caddy/v2"
|
|
"github.com/caddyserver/caddy/v2/caddyconfig"
|
|
"github.com/caddyserver/caddy/v2/caddyconfig/caddyfile"
|
|
"github.com/caddyserver/caddy/v2/modules/caddyhttp"
|
|
"github.com/caddyserver/caddy/v2/modules/caddytls"
|
|
"github.com/caddyserver/certmagic"
|
|
"github.com/mholt/acmez/acme"
|
|
"go.uber.org/zap/zapcore"
|
|
)
|
|
|
|
func init() {
|
|
RegisterDirective("bind", parseBind)
|
|
RegisterDirective("tls", parseTLS)
|
|
RegisterHandlerDirective("root", parseRoot)
|
|
RegisterHandlerDirective("redir", parseRedir)
|
|
RegisterHandlerDirective("respond", parseRespond)
|
|
RegisterHandlerDirective("route", parseRoute)
|
|
RegisterHandlerDirective("handle", parseHandle)
|
|
RegisterDirective("handle_errors", parseHandleErrors)
|
|
RegisterDirective("log", parseLog)
|
|
}
|
|
|
|
// parseBind parses the bind directive. Syntax:
|
|
//
|
|
// bind <addresses...>
|
|
//
|
|
func parseBind(h Helper) ([]ConfigValue, error) {
|
|
var lnHosts []string
|
|
for h.Next() {
|
|
lnHosts = append(lnHosts, h.RemainingArgs()...)
|
|
}
|
|
return h.NewBindAddresses(lnHosts), nil
|
|
}
|
|
|
|
// parseTLS parses the tls directive. Syntax:
|
|
//
|
|
// tls [<email>|internal]|[<cert_file> <key_file>] {
|
|
// protocols <min> [<max>]
|
|
// ciphers <cipher_suites...>
|
|
// curves <curves...>
|
|
// client_auth {
|
|
// mode [request|require|verify_if_given|require_and_verify]
|
|
// trusted_ca_cert <base64_der>
|
|
// trusted_ca_cert_file <filename>
|
|
// trusted_leaf_cert <base64_der>
|
|
// trusted_leaf_cert_file <filename>
|
|
// }
|
|
// alpn <values...>
|
|
// load <paths...>
|
|
// ca <acme_ca_endpoint>
|
|
// ca_root <pem_file>
|
|
// dns <provider_name>
|
|
// on_demand
|
|
// issuer <module_name> ...
|
|
// }
|
|
//
|
|
func parseTLS(h Helper) ([]ConfigValue, error) {
|
|
cp := new(caddytls.ConnectionPolicy)
|
|
var fileLoader caddytls.FileLoader
|
|
var folderLoader caddytls.FolderLoader
|
|
var certSelector caddytls.CustomCertSelectionPolicy
|
|
var acmeIssuer *caddytls.ACMEIssuer
|
|
var internalIssuer *caddytls.InternalIssuer
|
|
var issuer certmagic.Issuer
|
|
var onDemand bool
|
|
|
|
for h.Next() {
|
|
// file certificate loader
|
|
firstLine := h.RemainingArgs()
|
|
switch len(firstLine) {
|
|
case 0:
|
|
case 1:
|
|
if firstLine[0] == "internal" {
|
|
internalIssuer = new(caddytls.InternalIssuer)
|
|
} else if !strings.Contains(firstLine[0], "@") {
|
|
return nil, h.Err("single argument must either be 'internal' or an email address")
|
|
} else {
|
|
if acmeIssuer == nil {
|
|
acmeIssuer = new(caddytls.ACMEIssuer)
|
|
}
|
|
acmeIssuer.Email = firstLine[0]
|
|
}
|
|
|
|
case 2:
|
|
certFilename := firstLine[0]
|
|
keyFilename := firstLine[1]
|
|
|
|
// tag this certificate so if multiple certs match, specifically
|
|
// this one that the user has provided will be used, see #2588:
|
|
// https://github.com/caddyserver/caddy/issues/2588 ... but we
|
|
// must be careful about how we do this; being careless will
|
|
// lead to failed handshakes
|
|
//
|
|
// we need to remember which cert files we've seen, since we
|
|
// must load each cert only once; otherwise, they each get a
|
|
// different tag... since a cert loaded twice has the same
|
|
// bytes, it will overwrite the first one in the cache, and
|
|
// only the last cert (and its tag) will survive, so a any conn
|
|
// policy that is looking for any tag but the last one to be
|
|
// loaded won't find it, and TLS handshakes will fail (see end)
|
|
// of issue #3004)
|
|
//
|
|
// tlsCertTags maps certificate filenames to their tag.
|
|
// This is used to remember which tag is used for each
|
|
// certificate files, since we need to avoid loading
|
|
// the same certificate files more than once, overwriting
|
|
// previous tags
|
|
tlsCertTags, ok := h.State["tlsCertTags"].(map[string]string)
|
|
if !ok {
|
|
tlsCertTags = make(map[string]string)
|
|
h.State["tlsCertTags"] = tlsCertTags
|
|
}
|
|
|
|
tag, ok := tlsCertTags[certFilename]
|
|
if !ok {
|
|
// haven't seen this cert file yet, let's give it a tag
|
|
// and add a loader for it
|
|
tag = fmt.Sprintf("cert%d", len(tlsCertTags))
|
|
fileLoader = append(fileLoader, caddytls.CertKeyFilePair{
|
|
Certificate: certFilename,
|
|
Key: keyFilename,
|
|
Tags: []string{tag},
|
|
})
|
|
// remember this for next time we see this cert file
|
|
tlsCertTags[certFilename] = tag
|
|
}
|
|
certSelector.AnyTag = append(certSelector.AnyTag, tag)
|
|
|
|
default:
|
|
return nil, h.ArgErr()
|
|
}
|
|
|
|
var hasBlock bool
|
|
for nesting := h.Nesting(); h.NextBlock(nesting); {
|
|
hasBlock = true
|
|
|
|
switch h.Val() {
|
|
case "protocols":
|
|
args := h.RemainingArgs()
|
|
if len(args) == 0 {
|
|
return nil, h.SyntaxErr("one or two protocols")
|
|
}
|
|
if len(args) > 0 {
|
|
if _, ok := caddytls.SupportedProtocols[args[0]]; !ok {
|
|
return nil, h.Errf("Wrong protocol name or protocol not supported: '%s'", args[0])
|
|
}
|
|
cp.ProtocolMin = args[0]
|
|
}
|
|
if len(args) > 1 {
|
|
if _, ok := caddytls.SupportedProtocols[args[1]]; !ok {
|
|
return nil, h.Errf("Wrong protocol name or protocol not supported: '%s'", args[1])
|
|
}
|
|
cp.ProtocolMax = args[1]
|
|
}
|
|
|
|
case "ciphers":
|
|
for h.NextArg() {
|
|
if !caddytls.CipherSuiteNameSupported(h.Val()) {
|
|
return nil, h.Errf("Wrong cipher suite name or cipher suite not supported: '%s'", h.Val())
|
|
}
|
|
cp.CipherSuites = append(cp.CipherSuites, h.Val())
|
|
}
|
|
|
|
case "curves":
|
|
for h.NextArg() {
|
|
if _, ok := caddytls.SupportedCurves[h.Val()]; !ok {
|
|
return nil, h.Errf("Wrong curve name or curve not supported: '%s'", h.Val())
|
|
}
|
|
cp.Curves = append(cp.Curves, h.Val())
|
|
}
|
|
|
|
case "client_auth":
|
|
cp.ClientAuthentication = &caddytls.ClientAuthentication{}
|
|
for nesting := h.Nesting(); h.NextBlock(nesting); {
|
|
subdir := h.Val()
|
|
switch subdir {
|
|
case "mode":
|
|
if !h.Args(&cp.ClientAuthentication.Mode) {
|
|
return nil, h.ArgErr()
|
|
}
|
|
if h.NextArg() {
|
|
return nil, h.ArgErr()
|
|
}
|
|
|
|
case "trusted_ca_cert",
|
|
"trusted_leaf_cert":
|
|
if !h.NextArg() {
|
|
return nil, h.ArgErr()
|
|
}
|
|
if subdir == "trusted_ca_cert" {
|
|
cp.ClientAuthentication.TrustedCACerts = append(cp.ClientAuthentication.TrustedCACerts, h.Val())
|
|
} else {
|
|
cp.ClientAuthentication.TrustedLeafCerts = append(cp.ClientAuthentication.TrustedLeafCerts, h.Val())
|
|
}
|
|
|
|
case "trusted_ca_cert_file",
|
|
"trusted_leaf_cert_file":
|
|
if !h.NextArg() {
|
|
return nil, h.ArgErr()
|
|
}
|
|
filename := h.Val()
|
|
certDataPEM, err := ioutil.ReadFile(filename)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
block, _ := pem.Decode(certDataPEM)
|
|
if block == nil || block.Type != "CERTIFICATE" {
|
|
return nil, h.Errf("no CERTIFICATE pem block found in %s", h.Val())
|
|
}
|
|
if subdir == "trusted_ca_cert_file" {
|
|
cp.ClientAuthentication.TrustedCACerts = append(cp.ClientAuthentication.TrustedCACerts,
|
|
base64.StdEncoding.EncodeToString(block.Bytes))
|
|
} else {
|
|
cp.ClientAuthentication.TrustedLeafCerts = append(cp.ClientAuthentication.TrustedLeafCerts,
|
|
base64.StdEncoding.EncodeToString(block.Bytes))
|
|
}
|
|
|
|
default:
|
|
return nil, h.Errf("unknown subdirective for client_auth: %s", subdir)
|
|
}
|
|
}
|
|
|
|
case "alpn":
|
|
args := h.RemainingArgs()
|
|
if len(args) == 0 {
|
|
return nil, h.ArgErr()
|
|
}
|
|
cp.ALPN = args
|
|
|
|
case "load":
|
|
folderLoader = append(folderLoader, h.RemainingArgs()...)
|
|
|
|
case "ca":
|
|
arg := h.RemainingArgs()
|
|
if len(arg) != 1 {
|
|
return nil, h.ArgErr()
|
|
}
|
|
if acmeIssuer == nil {
|
|
acmeIssuer = new(caddytls.ACMEIssuer)
|
|
}
|
|
acmeIssuer.CA = arg[0]
|
|
|
|
case "eab":
|
|
arg := h.RemainingArgs()
|
|
if len(arg) != 2 {
|
|
return nil, h.ArgErr()
|
|
}
|
|
if acmeIssuer == nil {
|
|
acmeIssuer = new(caddytls.ACMEIssuer)
|
|
}
|
|
acmeIssuer.ExternalAccount = &acme.EAB{
|
|
KeyID: arg[0],
|
|
MACKey: arg[1],
|
|
}
|
|
|
|
case "issuer":
|
|
if !h.NextArg() {
|
|
return nil, h.ArgErr()
|
|
}
|
|
modName := h.Val()
|
|
mod, err := caddy.GetModule("tls.issuance." + modName)
|
|
if err != nil {
|
|
return nil, h.Errf("getting issuer module '%s': %v", modName, err)
|
|
}
|
|
unm, ok := mod.New().(caddyfile.Unmarshaler)
|
|
if !ok {
|
|
return nil, h.Errf("issuer module '%s' is not a Caddyfile unmarshaler", mod.ID)
|
|
}
|
|
err = unm.UnmarshalCaddyfile(h.NewFromNextSegment())
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
issuer, ok = unm.(certmagic.Issuer)
|
|
if !ok {
|
|
return nil, h.Errf("module %s is not a certmagic.Issuer", mod.ID)
|
|
}
|
|
|
|
case "dns":
|
|
if !h.NextArg() {
|
|
return nil, h.ArgErr()
|
|
}
|
|
provName := h.Val()
|
|
if acmeIssuer == nil {
|
|
acmeIssuer = new(caddytls.ACMEIssuer)
|
|
}
|
|
if acmeIssuer.Challenges == nil {
|
|
acmeIssuer.Challenges = new(caddytls.ChallengesConfig)
|
|
acmeIssuer.Challenges.DNS = new(caddytls.DNSChallengeConfig)
|
|
}
|
|
dnsProvModule, err := caddy.GetModule("dns.providers." + provName)
|
|
if err != nil {
|
|
return nil, h.Errf("getting DNS provider module named '%s': %v", provName, err)
|
|
}
|
|
dnsProvModuleInstance := dnsProvModule.New()
|
|
if unm, ok := dnsProvModuleInstance.(caddyfile.Unmarshaler); ok {
|
|
err = unm.UnmarshalCaddyfile(h.NewFromNextSegment())
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
}
|
|
acmeIssuer.Challenges.DNS.ProviderRaw = caddyconfig.JSONModuleObject(dnsProvModuleInstance, "name", provName, h.warnings)
|
|
|
|
case "ca_root":
|
|
arg := h.RemainingArgs()
|
|
if len(arg) != 1 {
|
|
return nil, h.ArgErr()
|
|
}
|
|
if acmeIssuer == nil {
|
|
acmeIssuer = new(caddytls.ACMEIssuer)
|
|
}
|
|
acmeIssuer.TrustedRootsPEMFiles = append(acmeIssuer.TrustedRootsPEMFiles, arg[0])
|
|
|
|
case "on_demand":
|
|
if h.NextArg() {
|
|
return nil, h.ArgErr()
|
|
}
|
|
onDemand = true
|
|
|
|
default:
|
|
return nil, h.Errf("unknown subdirective: %s", h.Val())
|
|
}
|
|
}
|
|
|
|
// a naked tls directive is not allowed
|
|
if len(firstLine) == 0 && !hasBlock {
|
|
return nil, h.ArgErr()
|
|
}
|
|
}
|
|
|
|
// begin building the final config values
|
|
var configVals []ConfigValue
|
|
|
|
// certificate loaders
|
|
if len(fileLoader) > 0 {
|
|
configVals = append(configVals, ConfigValue{
|
|
Class: "tls.cert_loader",
|
|
Value: fileLoader,
|
|
})
|
|
}
|
|
if len(folderLoader) > 0 {
|
|
configVals = append(configVals, ConfigValue{
|
|
Class: "tls.cert_loader",
|
|
Value: folderLoader,
|
|
})
|
|
}
|
|
|
|
// issuer
|
|
if acmeIssuer != nil && internalIssuer != nil {
|
|
// the logic to support this would be complex
|
|
return nil, h.Err("cannot use both ACME and internal issuers in same server block")
|
|
}
|
|
if issuer != nil && (acmeIssuer != nil || internalIssuer != nil) {
|
|
// similarly, the logic to support this would be complex
|
|
return nil, h.Err("when defining an issuer, all its config must be in its block, rather than from separate tls subdirectives")
|
|
}
|
|
switch {
|
|
case issuer != nil:
|
|
configVals = append(configVals, ConfigValue{
|
|
Class: "tls.cert_issuer",
|
|
Value: issuer,
|
|
})
|
|
|
|
case internalIssuer != nil:
|
|
configVals = append(configVals, ConfigValue{
|
|
Class: "tls.cert_issuer",
|
|
Value: internalIssuer,
|
|
})
|
|
|
|
case acmeIssuer != nil:
|
|
// fill in global defaults, if configured
|
|
if email := h.Option("email"); email != nil && acmeIssuer.Email == "" {
|
|
acmeIssuer.Email = email.(string)
|
|
}
|
|
if acmeCA := h.Option("acme_ca"); acmeCA != nil && acmeIssuer.CA == "" {
|
|
acmeIssuer.CA = acmeCA.(string)
|
|
}
|
|
if caPemFile := h.Option("acme_ca_root"); caPemFile != nil {
|
|
acmeIssuer.TrustedRootsPEMFiles = append(acmeIssuer.TrustedRootsPEMFiles, caPemFile.(string))
|
|
}
|
|
configVals = append(configVals, ConfigValue{
|
|
Class: "tls.cert_issuer",
|
|
Value: disambiguateACMEIssuer(acmeIssuer),
|
|
})
|
|
}
|
|
|
|
// on-demand TLS
|
|
if onDemand {
|
|
configVals = append(configVals, ConfigValue{
|
|
Class: "tls.on_demand",
|
|
Value: true,
|
|
})
|
|
}
|
|
|
|
// custom certificate selection
|
|
if len(certSelector.AnyTag) > 0 {
|
|
cp.CertSelection = &certSelector
|
|
}
|
|
|
|
// connection policy -- always add one, to ensure that TLS
|
|
// is enabled, because this directive was used (this is
|
|
// needed, for instance, when a site block has a key of
|
|
// just ":5000" - i.e. no hostname, and only on-demand TLS
|
|
// is enabled)
|
|
configVals = append(configVals, ConfigValue{
|
|
Class: "tls.connection_policy",
|
|
Value: cp,
|
|
})
|
|
|
|
return configVals, nil
|
|
}
|
|
|
|
// parseRoot parses the root directive. Syntax:
|
|
//
|
|
// root [<matcher>] <path>
|
|
//
|
|
func parseRoot(h Helper) (caddyhttp.MiddlewareHandler, error) {
|
|
var root string
|
|
for h.Next() {
|
|
if !h.NextArg() {
|
|
return nil, h.ArgErr()
|
|
}
|
|
root = h.Val()
|
|
if h.NextArg() {
|
|
return nil, h.ArgErr()
|
|
}
|
|
}
|
|
return caddyhttp.VarsMiddleware{"root": root}, nil
|
|
}
|
|
|
|
// parseRedir parses the redir directive. Syntax:
|
|
//
|
|
// redir [<matcher>] <to> [<code>]
|
|
//
|
|
func parseRedir(h Helper) (caddyhttp.MiddlewareHandler, error) {
|
|
if !h.Next() {
|
|
return nil, h.ArgErr()
|
|
}
|
|
|
|
if !h.NextArg() {
|
|
return nil, h.ArgErr()
|
|
}
|
|
to := h.Val()
|
|
|
|
var code string
|
|
if h.NextArg() {
|
|
code = h.Val()
|
|
}
|
|
if code == "permanent" {
|
|
code = "301"
|
|
}
|
|
if code == "temporary" || code == "" {
|
|
code = "302"
|
|
}
|
|
var body string
|
|
if code == "html" {
|
|
// Script tag comes first since that will better imitate a redirect in the browser's
|
|
// history, but the meta tag is a fallback for most non-JS clients.
|
|
const metaRedir = `<!DOCTYPE html>
|
|
<html>
|
|
<head>
|
|
<title>Redirecting...</title>
|
|
<script>window.location.replace("%s");</script>
|
|
<meta http-equiv="refresh" content="0; URL='%s'">
|
|
</head>
|
|
<body>Redirecting to <a href="%s">%s</a>...</body>
|
|
</html>
|
|
`
|
|
safeTo := html.EscapeString(to)
|
|
body = fmt.Sprintf(metaRedir, safeTo, safeTo, safeTo, safeTo)
|
|
}
|
|
|
|
return caddyhttp.StaticResponse{
|
|
StatusCode: caddyhttp.WeakString(code),
|
|
Headers: http.Header{"Location": []string{to}},
|
|
Body: body,
|
|
}, nil
|
|
}
|
|
|
|
// parseRespond parses the respond directive.
|
|
func parseRespond(h Helper) (caddyhttp.MiddlewareHandler, error) {
|
|
sr := new(caddyhttp.StaticResponse)
|
|
err := sr.UnmarshalCaddyfile(h.Dispenser)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return sr, nil
|
|
}
|
|
|
|
// parseRoute parses the route directive.
|
|
func parseRoute(h Helper) (caddyhttp.MiddlewareHandler, error) {
|
|
sr := new(caddyhttp.Subroute)
|
|
|
|
allResults, err := parseSegmentAsConfig(h)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
for _, result := range allResults {
|
|
switch handler := result.Value.(type) {
|
|
case caddyhttp.Route:
|
|
sr.Routes = append(sr.Routes, handler)
|
|
case caddyhttp.Subroute:
|
|
// directives which return a literal subroute instead of a route
|
|
// means they intend to keep those handlers together without
|
|
// them being reordered; we're doing that anyway since we're in
|
|
// the route directive, so just append its handlers
|
|
sr.Routes = append(sr.Routes, handler.Routes...)
|
|
default:
|
|
return nil, h.Errf("%s directive returned something other than an HTTP route or subroute: %#v (only handler directives can be used in routes)", result.directive, result.Value)
|
|
}
|
|
}
|
|
|
|
return sr, nil
|
|
}
|
|
|
|
func parseHandle(h Helper) (caddyhttp.MiddlewareHandler, error) {
|
|
return ParseSegmentAsSubroute(h)
|
|
}
|
|
|
|
func parseHandleErrors(h Helper) ([]ConfigValue, error) {
|
|
subroute, err := ParseSegmentAsSubroute(h)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return []ConfigValue{
|
|
{
|
|
Class: "error_route",
|
|
Value: subroute,
|
|
},
|
|
}, nil
|
|
}
|
|
|
|
// parseLog parses the log directive. Syntax:
|
|
//
|
|
// log {
|
|
// output <writer_module> ...
|
|
// format <encoder_module> ...
|
|
// level <level>
|
|
// }
|
|
//
|
|
func parseLog(h Helper) ([]ConfigValue, error) {
|
|
var configValues []ConfigValue
|
|
for h.Next() {
|
|
// log does not currently support any arguments
|
|
if h.NextArg() {
|
|
return nil, h.ArgErr()
|
|
}
|
|
|
|
cl := new(caddy.CustomLog)
|
|
|
|
for h.NextBlock(0) {
|
|
switch h.Val() {
|
|
case "output":
|
|
if !h.NextArg() {
|
|
return nil, h.ArgErr()
|
|
}
|
|
moduleName := h.Val()
|
|
|
|
// can't use the usual caddyfile.Unmarshaler flow with the
|
|
// standard writers because they are in the caddy package
|
|
// (because they are the default) and implementing that
|
|
// interface there would unfortunately create circular import
|
|
var wo caddy.WriterOpener
|
|
switch moduleName {
|
|
case "stdout":
|
|
wo = caddy.StdoutWriter{}
|
|
case "stderr":
|
|
wo = caddy.StderrWriter{}
|
|
case "discard":
|
|
wo = caddy.DiscardWriter{}
|
|
default:
|
|
mod, err := caddy.GetModule("caddy.logging.writers." + moduleName)
|
|
if err != nil {
|
|
return nil, h.Errf("getting log writer module named '%s': %v", moduleName, err)
|
|
}
|
|
unm, ok := mod.New().(caddyfile.Unmarshaler)
|
|
if !ok {
|
|
return nil, h.Errf("log writer module '%s' is not a Caddyfile unmarshaler", mod)
|
|
}
|
|
err = unm.UnmarshalCaddyfile(h.NewFromNextSegment())
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
wo, ok = unm.(caddy.WriterOpener)
|
|
if !ok {
|
|
return nil, h.Errf("module %s is not a WriterOpener", mod)
|
|
}
|
|
}
|
|
cl.WriterRaw = caddyconfig.JSONModuleObject(wo, "output", moduleName, h.warnings)
|
|
|
|
case "format":
|
|
if !h.NextArg() {
|
|
return nil, h.ArgErr()
|
|
}
|
|
moduleName := h.Val()
|
|
mod, err := caddy.GetModule("caddy.logging.encoders." + moduleName)
|
|
if err != nil {
|
|
return nil, h.Errf("getting log encoder module named '%s': %v", moduleName, err)
|
|
}
|
|
unm, ok := mod.New().(caddyfile.Unmarshaler)
|
|
if !ok {
|
|
return nil, h.Errf("log encoder module '%s' is not a Caddyfile unmarshaler", mod)
|
|
}
|
|
err = unm.UnmarshalCaddyfile(h.NewFromNextSegment())
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
enc, ok := unm.(zapcore.Encoder)
|
|
if !ok {
|
|
return nil, h.Errf("module %s is not a zapcore.Encoder", mod)
|
|
}
|
|
cl.EncoderRaw = caddyconfig.JSONModuleObject(enc, "format", moduleName, h.warnings)
|
|
|
|
case "level":
|
|
if !h.NextArg() {
|
|
return nil, h.ArgErr()
|
|
}
|
|
cl.Level = h.Val()
|
|
if h.NextArg() {
|
|
return nil, h.ArgErr()
|
|
}
|
|
|
|
default:
|
|
return nil, h.Errf("unrecognized subdirective: %s", h.Val())
|
|
}
|
|
}
|
|
|
|
var val namedCustomLog
|
|
if !reflect.DeepEqual(cl, new(caddy.CustomLog)) {
|
|
logCounter, ok := h.State["logCounter"].(int)
|
|
if !ok {
|
|
logCounter = 0
|
|
}
|
|
val.name = fmt.Sprintf("log%d", logCounter)
|
|
cl.Include = []string{"http.log.access." + val.name}
|
|
val.log = cl
|
|
logCounter++
|
|
h.State["logCounter"] = logCounter
|
|
}
|
|
configValues = append(configValues, ConfigValue{
|
|
Class: "custom_log",
|
|
Value: val,
|
|
})
|
|
}
|
|
return configValues, nil
|
|
}
|