diff --git a/caddytest/integration/caddyfile_adapt/log_filters.txt b/caddytest/integration/caddyfile_adapt/log_filters.txt index 0949c1d40a8a..7873b1c9b44c 100644 --- a/caddytest/integration/caddyfile_adapt/log_filters.txt +++ b/caddytest/integration/caddyfile_adapt/log_filters.txt @@ -5,6 +5,10 @@ log { format filter { wrap console fields { + uri query { + replace foo REDACTED + delete bar + } request>headers>Authorization replace REDACTED request>headers>Server delete request>remote_addr ip_mask { @@ -40,6 +44,20 @@ log { "filter": "ip_mask", "ipv4_cidr": 24, "ipv6_cidr": 32 + }, + "uri": { + "actions": [ + { + "parameter": "foo", + "type": "replace", + "value": "REDACTED" + }, + { + "parameter": "bar", + "type": "delete" + } + ], + "filter": "query" } }, "format": "filter", diff --git a/modules/logging/filters.go b/modules/logging/filters.go index ef5a4cb9e2aa..ba90bb3b69c3 100644 --- a/modules/logging/filters.go +++ b/modules/logging/filters.go @@ -16,6 +16,7 @@ package logging import ( "net" + "net/url" "strconv" "github.com/caddyserver/caddy/v2" @@ -27,6 +28,7 @@ func init() { caddy.RegisterModule(DeleteFilter{}) caddy.RegisterModule(ReplaceFilter{}) caddy.RegisterModule(IPMaskFilter{}) + caddy.RegisterModule(QueryFilter{}) } // LogFieldFilter can filter (or manipulate) @@ -185,15 +187,107 @@ func (m IPMaskFilter) Filter(in zapcore.Field) zapcore.Field { return in } +type ActionType string + +const ( + ReplaceType ActionType = "replace" + DeleteType ActionType = "delete" +) + +type queryFilterAction struct { + Type ActionType `json:"type"` + Parameter string `json:"parameter"` + Value string `json:"value,omitempty"` +} + +// QueryFilter is a Caddy log field filter that +// filters query parameters. +type QueryFilter struct { + Actions []queryFilterAction `json:"actions"` +} + +// CaddyModule returns the Caddy module information. +func (QueryFilter) CaddyModule() caddy.ModuleInfo { + return caddy.ModuleInfo{ + ID: "caddy.logging.encoders.filter.query", + New: func() caddy.Module { return new(QueryFilter) }, + } +} + +// UnmarshalCaddyfile sets up the module from Caddyfile tokens. +func (m *QueryFilter) UnmarshalCaddyfile(d *caddyfile.Dispenser) error { + for d.Next() { + for d.NextBlock(0) { + qfa := queryFilterAction{} + switch d.Val() { + case "replace": + if !d.NextArg() { + return d.ArgErr() + } + + qfa.Type = ReplaceType + qfa.Parameter = d.Val() + + if !d.NextArg() { + return d.ArgErr() + } + qfa.Value = d.Val() + + case "delete": + if !d.NextArg() { + return d.ArgErr() + } + + qfa.Type = DeleteType + qfa.Parameter = d.Val() + + default: + return d.Errf("unrecognized subdirective %s", d.Val()) + } + + m.Actions = append(m.Actions, qfa) + } + } + return nil +} + +// Filter filters the input field. +func (m QueryFilter) Filter(in zapcore.Field) zapcore.Field { + u, err := url.Parse(in.String) + if err != nil { + return in + } + + q := u.Query() + for _, a := range m.Actions { + switch a.Type { + case ReplaceType: + if q.Has(a.Parameter) { + q.Set(a.Parameter, a.Value) + } + + case DeleteType: + q.Del(a.Parameter) + } + } + + u.RawQuery = q.Encode() + in.String = u.String() + + return in +} + // Interface guards var ( _ LogFieldFilter = (*DeleteFilter)(nil) _ LogFieldFilter = (*ReplaceFilter)(nil) _ LogFieldFilter = (*IPMaskFilter)(nil) + _ LogFieldFilter = (*QueryFilter)(nil) _ caddyfile.Unmarshaler = (*DeleteFilter)(nil) _ caddyfile.Unmarshaler = (*ReplaceFilter)(nil) _ caddyfile.Unmarshaler = (*IPMaskFilter)(nil) + _ caddyfile.Unmarshaler = (*QueryFilter)(nil) _ caddy.Provisioner = (*IPMaskFilter)(nil) ) diff --git a/modules/logging/filters_test.go b/modules/logging/filters_test.go new file mode 100644 index 000000000000..2144f0b8bef8 --- /dev/null +++ b/modules/logging/filters_test.go @@ -0,0 +1,19 @@ +package logging + +import ( + "testing" + + "go.uber.org/zap/zapcore" +) + +func TestQueryFilter(t *testing.T) { + f := QueryFilter{[]queryFilterAction{ + {ReplaceType, "foo", "REDACTED"}, + {DeleteType, "bar", ""}, + }} + + out := f.Filter(zapcore.Field{String: "/path?foo=a&foo=b&bar=c&bar=d&baz=e"}) + if out.String != "/path?baz=e&foo=REDACTED" { + t.Fatalf("query parameters have not been filtered: %s", out.String) + } +}