Fast and extensible multi-platform HTTP/1-2-3 web server with automatic HTTPS
Mirrored from https://github.com/Slazee/caddy.git
Synced Nov 13, 09:15 UTC
logging: Perform filtering on arrays of strings (where possible) (#5101)
* logging: Perform filtering on arrays of strings (where possible) * Add test for ip_mask filter * Oops, need to continue when it's not an IP * Test for invalid IPs
Francis Lavoie committed
Oct 5, 2022 at 01:21 UTC
ea58d519078916d4cf273628653e348befbaf6c0
2 files changed
+171
-24
modules/logging/filters.go
+61
-22
@@ -23,6 +23,7 @@ import (
23
"net/url"
24
"regexp"
25
"strconv"
26
+ "strings"
27
28
"github.com/caddyserver/caddy/v2"
29
"github.com/caddyserver/caddy/v2/caddyconfig/caddyfile"
@@ -76,7 +77,10 @@ func hash(s string) string {
77
}
78
79
// HashFilter is a Caddy log field filter that
79
-// replaces the field with the initial 4 bytes of the SHA-256 hash of the content.
80
+// replaces the field with the initial 4 bytes
81
+// of the SHA-256 hash of the content. Operates
82
+// on string fields, or on arrays of strings
83
+// where each string is hashed.
84
type HashFilter struct {
85
}
86
@@ -95,7 +99,13 @@ func (f *HashFilter) UnmarshalCaddyfile(d *caddyfile.Dispenser) error {
99
100
// Filter filters the input field with the replacement value.
101
func (f *HashFilter) Filter(in zapcore.Field) zapcore.Field {
98
- in.String = hash(in.String)
102
+ if array, ok := in.Interface.(caddyhttp.LoggableStringArray); ok {
103
+ for i, s := range array {
104
+ array[i] = hash(s)
105
+ }
106
+ } else {
107
+ in.String = hash(in.String)
108
+ }
109
return in
110
}
111
@@ -131,7 +141,10 @@ func (f *ReplaceFilter) Filter(in zapcore.Field) zapcore.Field {
141
}
142
143
// IPMaskFilter is a Caddy log field filter that
134
-// masks IP addresses.
144
+// masks IP addresses in a string, or in an array
145
+// of strings. The string may be a comma separated
146
+// list of IP addresses, where all of the values
147
+// will be masked.
148
type IPMaskFilter struct {
149
// The IPv4 mask, as an subnet size CIDR.
150
IPv4MaskRaw int `json:"ipv4_cidr,omitempty"`
@@ -205,27 +218,45 @@ func (m *IPMaskFilter) Provision(ctx caddy.Context) error {
218
219
// Filter filters the input field.
220
func (m IPMaskFilter) Filter(in zapcore.Field) zapcore.Field {
208
- host, port, err := net.SplitHostPort(in.String)
209
- if err != nil {
210
- host = in.String // assume whole thing was IP address
211
- }
212
- ipAddr := net.ParseIP(host)
213
- if ipAddr == nil {
214
- return in
215
- }
216
- mask := m.v4Mask
217
- if ipAddr.To4() == nil {
218
- mask = m.v6Mask
219
- }
220
- masked := ipAddr.Mask(mask)
221
- if port == "" {
222
- in.String = masked.String()
221
+ if array, ok := in.Interface.(caddyhttp.LoggableStringArray); ok {
222
+ for i, s := range array {
223
+ array[i] = m.mask(s)
224
+ }
225
} else {
224
- in.String = net.JoinHostPort(masked.String(), port)
226
+ in.String = m.mask(in.String)
227
}
228
+
229
return in
230
}
231
232
+func (m IPMaskFilter) mask(s string) string {
233
+ output := ""
234
+ for _, value := range strings.Split(s, ",") {
235
+ value = strings.TrimSpace(value)
236
+ host, port, err := net.SplitHostPort(value)
237
+ if err != nil {
238
+ host = value // assume whole thing was IP address
239
+ }
240
+ ipAddr := net.ParseIP(host)
241
+ if ipAddr == nil {
242
+ output += value + ", "
243
+ continue
244
+ }
245
+ mask := m.v4Mask
246
+ if ipAddr.To4() == nil {
247
+ mask = m.v6Mask
248
+ }
249
+ masked := ipAddr.Mask(mask)
250
+ if port == "" {
251
+ output += masked.String() + ", "
252
+ continue
253
+ }
254
+
255
+ output += net.JoinHostPort(masked.String(), port) + ", "
256
+ }
257
+ return strings.TrimSuffix(output, ", ")
258
+}
259
+
260
type filterAction string
261
262
const (
@@ -499,7 +530,10 @@ OUTER:
530
}
531
532
// RegexpFilter is a Caddy log field filter that
502
-// replaces the field matching the provided regexp with the indicated string.
533
+// replaces the field matching the provided regexp
534
+// with the indicated string. If the field is an
535
+// array of strings, each of them will have the
536
+// regexp replacement applied.
537
type RegexpFilter struct {
538
// The regular expression pattern defining what to replace.
539
RawRegexp string `json:"regexp,omitempty"`
@@ -545,7 +579,13 @@ func (m *RegexpFilter) Provision(ctx caddy.Context) error {
579
580
// Filter filters the input field with the replacement value if it matches the regexp.
581
func (f *RegexpFilter) Filter(in zapcore.Field) zapcore.Field {
548
- in.String = f.regexp.ReplaceAllString(in.String, f.Value)
582
+ if array, ok := in.Interface.(caddyhttp.LoggableStringArray); ok {
583
+ for i, s := range array {
584
+ array[i] = f.regexp.ReplaceAllString(s, f.Value)
585
+ }
586
+ } else {
587
+ in.String = f.regexp.ReplaceAllString(in.String, f.Value)
588
+ }
589
590
return in
591
}
@@ -576,7 +616,6 @@ func (f *RenameFilter) UnmarshalCaddyfile(d *caddyfile.Dispenser) error {
616
617
// Filter renames the input field with the replacement name.
618
func (f *RenameFilter) Filter(in zapcore.Field) zapcore.Field {
579
- in.Type = zapcore.StringType
619
in.Key = f.Name
620
return in
621
}
modules/logging/filters_test.go
+110
-2
@@ -8,6 +8,81 @@ import (
8
"go.uber.org/zap/zapcore"
9
)
10
11
+func TestIPMaskSingleValue(t *testing.T) {
12
+ f := IPMaskFilter{IPv4MaskRaw: 16, IPv6MaskRaw: 32}
13
+ f.Provision(caddy.Context{})
14
+
15
+ out := f.Filter(zapcore.Field{String: "255.255.255.255"})
16
+ if out.String != "255.255.0.0" {
17
+ t.Fatalf("field has not been filtered: %s", out.String)
18
+ }
19
+
20
+ out = f.Filter(zapcore.Field{String: "ffff:ffff:ffff:ffff:ffff:ffff:ffff:ffff"})
21
+ if out.String != "ffff:ffff::" {
22
+ t.Fatalf("field has not been filtered: %s", out.String)
23
+ }
24
+
25
+ out = f.Filter(zapcore.Field{String: "not-an-ip"})
26
+ if out.String != "not-an-ip" {
27
+ t.Fatalf("field has been filtered: %s", out.String)
28
+ }
29
+}
30
+
31
+func TestIPMaskCommaValue(t *testing.T) {
32
+ f := IPMaskFilter{IPv4MaskRaw: 16, IPv6MaskRaw: 32}
33
+ f.Provision(caddy.Context{})
34
+
35
+ out := f.Filter(zapcore.Field{String: "255.255.255.255, 244.244.244.244"})
36
+ if out.String != "255.255.0.0, 244.244.0.0" {
37
+ t.Fatalf("field has not been filtered: %s", out.String)
38
+ }
39
+
40
+ out = f.Filter(zapcore.Field{String: "ffff:ffff:ffff:ffff:ffff:ffff:ffff:ffff, ff00:ffff:ffff:ffff:ffff:ffff:ffff:ffff"})
41
+ if out.String != "ffff:ffff::, ff00:ffff::" {
42
+ t.Fatalf("field has not been filtered: %s", out.String)
43
+ }
44
+
45
+ out = f.Filter(zapcore.Field{String: "not-an-ip, 255.255.255.255"})
46
+ if out.String != "not-an-ip, 255.255.0.0" {
47
+ t.Fatalf("field has not been filtered: %s", out.String)
48
+ }
49
+}
50
+
51
+func TestIPMaskMultiValue(t *testing.T) {
52
+ f := IPMaskFilter{IPv4MaskRaw: 16, IPv6MaskRaw: 32}
53
+ f.Provision(caddy.Context{})
54
+
55
+ out := f.Filter(zapcore.Field{Interface: caddyhttp.LoggableStringArray{
56
+ "255.255.255.255",
57
+ "244.244.244.244",
58
+ }})
59
+ arr, ok := out.Interface.(caddyhttp.LoggableStringArray)
60
+ if !ok {
61
+ t.Fatalf("field is wrong type: %T", out.Integer)
62
+ }
63
+ if arr[0] != "255.255.0.0" {
64
+ t.Fatalf("field entry 0 has not been filtered: %s", arr[0])
65
+ }
66
+ if arr[1] != "244.244.0.0" {
67
+ t.Fatalf("field entry 1 has not been filtered: %s", arr[1])
68
+ }
69
+
70
+ out = f.Filter(zapcore.Field{Interface: caddyhttp.LoggableStringArray{
71
+ "ffff:ffff:ffff:ffff:ffff:ffff:ffff:ffff",
72
+ "ff00:ffff:ffff:ffff:ffff:ffff:ffff:ffff",
73
+ }})
74
+ arr, ok = out.Interface.(caddyhttp.LoggableStringArray)
75
+ if !ok {
76
+ t.Fatalf("field is wrong type: %T", out.Integer)
77
+ }
78
+ if arr[0] != "ffff:ffff::" {
79
+ t.Fatalf("field entry 0 has not been filtered: %s", arr[0])
80
+ }
81
+ if arr[1] != "ff00:ffff::" {
82
+ t.Fatalf("field entry 1 has not been filtered: %s", arr[1])
83
+ }
84
+}
85
+
86
func TestQueryFilter(t *testing.T) {
87
f := QueryFilter{[]queryFilterAction{
88
{replaceAction, "foo", "REDACTED"},
@@ -78,7 +153,7 @@ func TestValidateCookieFilter(t *testing.T) {
153
}
154
}
155
81
-func TestRegexpFilter(t *testing.T) {
156
+func TestRegexpFilterSingleValue(t *testing.T) {
157
f := RegexpFilter{RawRegexp: `secret`, Value: "REDACTED"}
158
f.Provision(caddy.Context{})
159
@@ -88,7 +163,24 @@ func TestRegexpFilter(t *testing.T) {
163
}
164
}
165
91
-func TestHashFilter(t *testing.T) {
166
+func TestRegexpFilterMultiValue(t *testing.T) {
167
+ f := RegexpFilter{RawRegexp: `secret`, Value: "REDACTED"}
168
+ f.Provision(caddy.Context{})
169
+
170
+ out := f.Filter(zapcore.Field{Interface: caddyhttp.LoggableStringArray{"foo-secret-bar", "bar-secret-foo"}})
171
+ arr, ok := out.Interface.(caddyhttp.LoggableStringArray)
172
+ if !ok {
173
+ t.Fatalf("field is wrong type: %T", out.Integer)
174
+ }
175
+ if arr[0] != "foo-REDACTED-bar" {
176
+ t.Fatalf("field entry 0 has not been filtered: %s", arr[0])
177
+ }
178
+ if arr[1] != "bar-REDACTED-foo" {
179
+ t.Fatalf("field entry 1 has not been filtered: %s", arr[1])
180
+ }
181
+}
182
+
183
+func TestHashFilterSingleValue(t *testing.T) {
184
f := HashFilter{}
185
186
out := f.Filter(zapcore.Field{String: "foo"})
@@ -96,3 +188,19 @@ func TestHashFilter(t *testing.T) {
188
t.Fatalf("field has not been filtered: %s", out.String)
189
}
190
}
191
+
192
+func TestHashFilterMultiValue(t *testing.T) {
193
+ f := HashFilter{}
194
+
195
+ out := f.Filter(zapcore.Field{Interface: caddyhttp.LoggableStringArray{"foo", "bar"}})
196
+ arr, ok := out.Interface.(caddyhttp.LoggableStringArray)
197
+ if !ok {
198
+ t.Fatalf("field is wrong type: %T", out.Integer)
199
+ }
200
+ if arr[0] != "2c26b46b" {
201
+ t.Fatalf("field entry 0 has not been filtered: %s", arr[0])
202
+ }
203
+ if arr[1] != "fcde2b2e" {
204
+ t.Fatalf("field entry 1 has not been filtered: %s", arr[1])
205
+ }
206
+}