diff --git a/caddytest/integration/caddyfile_adapt/log_filters.txt b/caddytest/integration/caddyfile_adapt/log_filters.txt new file mode 100644 index 00000000..549f4e6a --- /dev/null +++ b/caddytest/integration/caddyfile_adapt/log_filters.txt @@ -0,0 +1,69 @@ +:80 + +log { + output stdout + format filter { + wrap console + fields { + request>headers>Authorization delete + request>headers>Server delete + request>remote_addr ip_mask { + ipv4 24 + ipv6 32 + } + } + } +} +---------- +{ + "logging": { + "logs": { + "default": { + "exclude": [ + "http.log.access.log0" + ] + }, + "log0": { + "writer": { + "output": "stdout" + }, + "encoder": { + "fields": { + "request\u003eheaders\u003eAuthorization": { + "filter": "delete" + }, + "request\u003eheaders\u003eServer": { + "filter": "delete" + }, + "request\u003eremote_addr": { + "filter": "ip_mask", + "ipv4_cidr": 24, + "ipv6_cidr": 32 + } + }, + "format": "filter", + "wrap": { + "format": "console" + } + }, + "include": [ + "http.log.access.log0" + ] + } + } + }, + "apps": { + "http": { + "servers": { + "srv0": { + "listen": [ + ":80" + ], + "logs": { + "default_logger_name": "log0" + } + } + } + } + } +} \ No newline at end of file diff --git a/modules/logging/filterencoder.go b/modules/logging/filterencoder.go index 7a8108ca..d1c335f4 100644 --- a/modules/logging/filterencoder.go +++ b/modules/logging/filterencoder.go @@ -20,6 +20,8 @@ import ( "time" "github.com/caddyserver/caddy/v2" + "github.com/caddyserver/caddy/v2/caddyconfig" + "github.com/caddyserver/caddy/v2/caddyconfig/caddyfile" "go.uber.org/zap" "go.uber.org/zap/buffer" "go.uber.org/zap/zapcore" @@ -94,6 +96,80 @@ func (fe *FilterEncoder) Provision(ctx caddy.Context) error { return nil } +// UnmarshalCaddyfile sets up the module from Caddyfile tokens. Syntax: +// +// filter { +// wrap +// fields { +// { +// +// } +// } +// } +func (fe *FilterEncoder) UnmarshalCaddyfile(d *caddyfile.Dispenser) error { + for d.Next() { + for d.NextBlock(0) { + switch d.Val() { + case "wrap": + if !d.NextArg() { + return d.ArgErr() + } + moduleName := d.Val() + mod, err := caddy.GetModule("caddy.logging.encoders." + moduleName) + if err != nil { + return d.Errf("getting log encoder module named '%s': %v", moduleName, err) + } + unm, ok := mod.New().(caddyfile.Unmarshaler) + if !ok { + return d.Errf("log encoder module '%s' is not a Caddyfile unmarshaler", mod) + } + err = unm.UnmarshalCaddyfile(d.NewFromNextSegment()) + if err != nil { + return err + } + enc, ok := unm.(zapcore.Encoder) + if !ok { + return d.Errf("module %s is not a zapcore.Encoder", mod) + } + fe.WrappedRaw = caddyconfig.JSONModuleObject(enc, "format", moduleName, nil) + + case "fields": + for d.NextBlock(1) { + field := d.Val() + if !d.NextArg() { + return d.ArgErr() + } + filterName := d.Val() + mod, err := caddy.GetModule("caddy.logging.encoders.filter." + filterName) + if err != nil { + return d.Errf("getting log filter module named '%s': %v", filterName, err) + } + unm, ok := mod.New().(caddyfile.Unmarshaler) + if !ok { + return d.Errf("log encoder module '%s' is not a Caddyfile unmarshaler", mod) + } + err = unm.UnmarshalCaddyfile(d.NewFromNextSegment()) + if err != nil { + return err + } + f, ok := unm.(LogFieldFilter) + if !ok { + return d.Errf("module %s is not a LogFieldFilter", mod) + } + if fe.FieldsRaw == nil { + fe.FieldsRaw = make(map[string]json.RawMessage) + } + fe.FieldsRaw[field] = caddyconfig.JSONModuleObject(f, "filter", filterName, nil) + } + + default: + return d.Errf("unrecognized subdirective %s", d.Val()) + } + } + } + return nil +} + // AddArray is part of the zapcore.ObjectEncoder interface. // Array elements do not get filtered. func (fe FilterEncoder) AddArray(key string, marshaler zapcore.ArrayMarshaler) error { @@ -330,4 +406,5 @@ func (mom logObjectMarshalerWrapper) MarshalLogObject(_ zapcore.ObjectEncoder) e var ( _ zapcore.Encoder = (*FilterEncoder)(nil) _ zapcore.ObjectMarshaler = (*logObjectMarshalerWrapper)(nil) + _ caddyfile.Unmarshaler = (*FilterEncoder)(nil) ) diff --git a/modules/logging/filters.go b/modules/logging/filters.go index cd3bbb13..3bc1e2cb 100644 --- a/modules/logging/filters.go +++ b/modules/logging/filters.go @@ -16,8 +16,10 @@ package logging import ( "net" + "strconv" "github.com/caddyserver/caddy/v2" + "github.com/caddyserver/caddy/v2/caddyconfig/caddyfile" "go.uber.org/zap/zapcore" ) @@ -44,6 +46,11 @@ func (DeleteFilter) CaddyModule() caddy.ModuleInfo { } } +// UnmarshalCaddyfile sets up the module from Caddyfile tokens. +func (DeleteFilter) UnmarshalCaddyfile(d *caddyfile.Dispenser) error { + return nil +} + // Filter filters the input field. func (DeleteFilter) Filter(in zapcore.Field) zapcore.Field { in.Type = zapcore.SkipType @@ -53,11 +60,14 @@ func (DeleteFilter) Filter(in zapcore.Field) zapcore.Field { // IPMaskFilter is a Caddy log field filter that // masks IP addresses. type IPMaskFilter struct { - // The IPv4 range in CIDR notation. - IPv4CIDR int `json:"ipv4_cidr,omitempty"` + // The IPv4 mask, as an subnet size CIDR. + IPv4MaskRaw int `json:"ipv4_cidr,omitempty"` - // The IPv6 range in CIDR notation. - IPv6CIDR int `json:"ipv6_cidr,omitempty"` + // The IPv6 mask, as an subnet size CIDR. + IPv6MaskRaw int `json:"ipv6_cidr,omitempty"` + + v4Mask net.IPMask + v6Mask net.IPMask } // CaddyModule returns the Caddy module information. @@ -68,6 +78,58 @@ func (IPMaskFilter) CaddyModule() caddy.ModuleInfo { } } +// UnmarshalCaddyfile sets up the module from Caddyfile tokens. +func (m *IPMaskFilter) UnmarshalCaddyfile(d *caddyfile.Dispenser) error { + for d.Next() { + for d.NextBlock(0) { + switch d.Val() { + case "ipv4": + if !d.NextArg() { + return d.ArgErr() + } + val, err := strconv.Atoi(d.Val()) + if err != nil { + return d.Errf("error parsing %s: %v", d.Val(), err) + } + m.IPv4MaskRaw = val + + case "ipv6": + if !d.NextArg() { + return d.ArgErr() + } + val, err := strconv.Atoi(d.Val()) + if err != nil { + return d.Errf("error parsing %s: %v", d.Val(), err) + } + m.IPv6MaskRaw = val + + default: + return d.Errf("unrecognized subdirective %s", d.Val()) + } + } + } + return nil +} + +// Provision parses m's IP masks, from integers. +func (m *IPMaskFilter) Provision(ctx caddy.Context) error { + parseRawToMask := func(rawField int, bitLen int) net.IPMask { + if rawField == 0 { + return nil + } + + // we assume the int is a subnet size CIDR + // e.g. "16" being equivalent to masking the last + // two bytes of an ipv4 address, like "255.255.0.0" + return net.CIDRMask(rawField, bitLen) + } + + m.v4Mask = parseRawToMask(m.IPv4MaskRaw, 32) + m.v6Mask = parseRawToMask(m.IPv6MaskRaw, 128) + + return nil +} + // Filter filters the input field. func (m IPMaskFilter) Filter(in zapcore.Field) zapcore.Field { host, port, err := net.SplitHostPort(in.String) @@ -78,13 +140,10 @@ func (m IPMaskFilter) Filter(in zapcore.Field) zapcore.Field { if ipAddr == nil { return in } - bitLen := 32 - cidrPrefix := m.IPv4CIDR + mask := m.v4Mask if ipAddr.To16() != nil { - bitLen = 128 - cidrPrefix = m.IPv6CIDR + mask = m.v6Mask } - mask := net.CIDRMask(cidrPrefix, bitLen) masked := ipAddr.Mask(mask) if port == "" { in.String = masked.String() @@ -93,3 +152,14 @@ func (m IPMaskFilter) Filter(in zapcore.Field) zapcore.Field { } return in } + +// Interface guards +var ( + _ LogFieldFilter = (*DeleteFilter)(nil) + _ LogFieldFilter = (*IPMaskFilter)(nil) + + _ caddyfile.Unmarshaler = (*DeleteFilter)(nil) + _ caddyfile.Unmarshaler = (*IPMaskFilter)(nil) + + _ caddy.Provisioner = (*IPMaskFilter)(nil) +)