@marcel91200 / caddy Mirror · read-only
Fork 0

Fast and extensible multi-platform HTTP/1-2-3 web server with automatic HTTPS

Mirrored from https://github.com/Slazee/caddy.git

Synced Nov 11, 01:00 UTC

master
go 1,093 lines 29 KB
Raw
1 // Copyright 2015 Matthew Holt and The Caddy Authors
2 //
3 // Licensed under the Apache License, Version 2.0 (the "License");
4 // you may not use this file except in compliance with the License.
5 // You may obtain a copy of the License at
6 //
7 // http://www.apache.org/licenses/LICENSE-2.0
8 //
9 // Unless required by applicable law or agreed to in writing, software
10 // distributed under the License is distributed on an "AS IS" BASIS,
11 // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 // See the License for the specific language governing permissions and
13 // limitations under the License.
14
15 package logging
16
17 import (
18 "crypto/sha256"
19 "errors"
20 "fmt"
21 "net"
22 "net/http"
23 "net/url"
24 "regexp"
25 "strconv"
26 "strings"
27 "unicode"
28
29 "go.uber.org/zap/zapcore"
30
31 "github.com/caddyserver/caddy/v2"
32 "github.com/caddyserver/caddy/v2/caddyconfig/caddyfile"
33 "github.com/caddyserver/caddy/v2/internal"
34 )
35
36 func init() {
37 caddy.RegisterModule(DeleteFilter{})
38 caddy.RegisterModule(HashFilter{})
39 caddy.RegisterModule(ReplaceFilter{})
40 caddy.RegisterModule(IPMaskFilter{})
41 caddy.RegisterModule(QueryFilter{})
42 caddy.RegisterModule(CookieFilter{})
43 caddy.RegisterModule(SetCookieFilter{})
44 caddy.RegisterModule(RegexpFilter{})
45 caddy.RegisterModule(RenameFilter{})
46 caddy.RegisterModule(MultiRegexpFilter{})
47 }
48
49 // LogFieldFilter can filter (or manipulate)
50 // a field in a log entry.
51 type LogFieldFilter interface {
52 Filter(zapcore.Field) zapcore.Field
53 }
54
55 // DeleteFilter is a Caddy log field filter that
56 // deletes the field.
57 type DeleteFilter struct{}
58
59 // CaddyModule returns the Caddy module information.
60 func (DeleteFilter) CaddyModule() caddy.ModuleInfo {
61 return caddy.ModuleInfo{
62 ID: "caddy.logging.encoders.filter.delete",
63 New: func() caddy.Module { return new(DeleteFilter) },
64 }
65 }
66
67 // UnmarshalCaddyfile sets up the module from Caddyfile tokens.
68 func (DeleteFilter) UnmarshalCaddyfile(d *caddyfile.Dispenser) error {
69 return nil
70 }
71
72 // Filter filters the input field.
73 func (DeleteFilter) Filter(in zapcore.Field) zapcore.Field {
74 in.Type = zapcore.SkipType
75 return in
76 }
77
78 // hash returns the first 4 bytes of the SHA-256 hash of the given data as hexadecimal
79 func hash(s string) string {
80 return fmt.Sprintf("%.4x", sha256.Sum256([]byte(s)))
81 }
82
83 // HashFilter is a Caddy log field filter that
84 // replaces the field with the initial 4 bytes
85 // of the SHA-256 hash of the content. Operates
86 // on string fields, or on arrays of strings
87 // where each string is hashed.
88 type HashFilter struct{}
89
90 // CaddyModule returns the Caddy module information.
91 func (HashFilter) CaddyModule() caddy.ModuleInfo {
92 return caddy.ModuleInfo{
93 ID: "caddy.logging.encoders.filter.hash",
94 New: func() caddy.Module { return new(HashFilter) },
95 }
96 }
97
98 // UnmarshalCaddyfile sets up the module from Caddyfile tokens.
99 func (f *HashFilter) UnmarshalCaddyfile(d *caddyfile.Dispenser) error {
100 return nil
101 }
102
103 // Filter filters the input field with the replacement value.
104 func (f *HashFilter) Filter(in zapcore.Field) zapcore.Field {
105 if array, ok := in.Interface.(internal.LoggableStringArray); ok {
106 newArray := make(internal.LoggableStringArray, len(array))
107 for i, s := range array {
108 newArray[i] = hash(s)
109 }
110 in.Interface = newArray
111 } else {
112 in.String = hash(in.String)
113 }
114
115 return in
116 }
117
118 // ReplaceFilter is a Caddy log field filter that
119 // replaces the field with the indicated string.
120 type ReplaceFilter struct {
121 Value string `json:"value,omitempty"`
122 }
123
124 // CaddyModule returns the Caddy module information.
125 func (ReplaceFilter) CaddyModule() caddy.ModuleInfo {
126 return caddy.ModuleInfo{
127 ID: "caddy.logging.encoders.filter.replace",
128 New: func() caddy.Module { return new(ReplaceFilter) },
129 }
130 }
131
132 // UnmarshalCaddyfile sets up the module from Caddyfile tokens.
133 func (f *ReplaceFilter) UnmarshalCaddyfile(d *caddyfile.Dispenser) error {
134 d.Next() // consume filter name
135 if d.NextArg() {
136 f.Value = d.Val()
137 }
138 return nil
139 }
140
141 // Filter filters the input field with the replacement value.
142 func (f *ReplaceFilter) Filter(in zapcore.Field) zapcore.Field {
143 in.Type = zapcore.StringType
144 in.String = f.Value
145 return in
146 }
147
148 // IPMaskFilter is a Caddy log field filter that
149 // masks IP addresses in a string, or in an array
150 // of strings. The string may be a comma separated
151 // list of IP addresses, where all of the values
152 // will be masked.
153 type IPMaskFilter struct {
154 // The IPv4 mask, as a subnet size CIDR.
155 IPv4MaskRaw int `json:"ipv4_cidr,omitempty"`
156
157 // The IPv6 mask, as a subnet size CIDR.
158 IPv6MaskRaw int `json:"ipv6_cidr,omitempty"`
159
160 v4Mask net.IPMask
161 v6Mask net.IPMask
162 }
163
164 // CaddyModule returns the Caddy module information.
165 func (IPMaskFilter) CaddyModule() caddy.ModuleInfo {
166 return caddy.ModuleInfo{
167 ID: "caddy.logging.encoders.filter.ip_mask",
168 New: func() caddy.Module { return new(IPMaskFilter) },
169 }
170 }
171
172 // UnmarshalCaddyfile sets up the module from Caddyfile tokens.
173 func (m *IPMaskFilter) UnmarshalCaddyfile(d *caddyfile.Dispenser) error {
174 d.Next() // consume filter name
175
176 args := d.RemainingArgs()
177 if len(args) > 2 {
178 return d.Errf("too many arguments")
179 }
180 if len(args) > 0 {
181 val, err := strconv.Atoi(args[0])
182 if err != nil {
183 return d.Errf("error parsing %s: %v", args[0], err)
184 }
185 m.IPv4MaskRaw = val
186
187 if len(args) > 1 {
188 val, err := strconv.Atoi(args[1])
189 if err != nil {
190 return d.Errf("error parsing %s: %v", args[1], err)
191 }
192 m.IPv6MaskRaw = val
193 }
194 }
195
196 for d.NextBlock(0) {
197 switch d.Val() {
198 case "ipv4":
199 if !d.NextArg() {
200 return d.ArgErr()
201 }
202 val, err := strconv.Atoi(d.Val())
203 if err != nil {
204 return d.Errf("error parsing %s: %v", d.Val(), err)
205 }
206 m.IPv4MaskRaw = val
207
208 case "ipv6":
209 if !d.NextArg() {
210 return d.ArgErr()
211 }
212 val, err := strconv.Atoi(d.Val())
213 if err != nil {
214 return d.Errf("error parsing %s: %v", d.Val(), err)
215 }
216 m.IPv6MaskRaw = val
217
218 default:
219 return d.Errf("unrecognized subdirective %s", d.Val())
220 }
221 }
222 return nil
223 }
224
225 // Provision parses m's IP masks, from integers.
226 func (m *IPMaskFilter) Provision(ctx caddy.Context) error {
227 if m.IPv4MaskRaw < 0 || m.IPv4MaskRaw > 32 {
228 return fmt.Errorf("invalid IPv4 mask: %d (must be between 0 and 32)", m.IPv4MaskRaw)
229 }
230 if m.IPv6MaskRaw < 0 || m.IPv6MaskRaw > 128 {
231 return fmt.Errorf("invalid IPv6 mask: %d (must be between 0 and 128)", m.IPv6MaskRaw)
232 }
233
234 parseRawToMask := func(rawField int, bitLen int) net.IPMask {
235 if rawField == 0 {
236 return nil
237 }
238
239 // we assume the int is a subnet size CIDR
240 // e.g. "16" being equivalent to masking the last
241 // two bytes of an ipv4 address, like "255.255.0.0"
242 return net.CIDRMask(rawField, bitLen)
243 }
244
245 m.v4Mask = parseRawToMask(m.IPv4MaskRaw, 32)
246 m.v6Mask = parseRawToMask(m.IPv6MaskRaw, 128)
247
248 return nil
249 }
250
251 // Filter filters the input field.
252 func (m IPMaskFilter) Filter(in zapcore.Field) zapcore.Field {
253 if array, ok := in.Interface.(internal.LoggableStringArray); ok {
254 newArray := make(internal.LoggableStringArray, len(array))
255 for i, s := range array {
256 newArray[i] = m.mask(s)
257 }
258 in.Interface = newArray
259 } else {
260 in.String = m.mask(in.String)
261 }
262
263 return in
264 }
265
266 func (m IPMaskFilter) mask(s string) string {
267 parts := make([]string, 0)
268 for value := range strings.SplitSeq(s, ",") {
269 value = strings.TrimSpace(value)
270 host, port, err := net.SplitHostPort(value)
271 if err != nil {
272 host = value // assume whole thing was IP address
273 }
274 ipAddr := net.ParseIP(host)
275 if ipAddr == nil {
276 parts = append(parts, value)
277 continue
278 }
279 mask := m.v4Mask
280 if ipAddr.To4() == nil {
281 mask = m.v6Mask
282 }
283 masked := ipAddr.Mask(mask)
284 if port == "" {
285 parts = append(parts, masked.String())
286 continue
287 }
288
289 parts = append(parts, net.JoinHostPort(masked.String(), port))
290 }
291 return strings.Join(parts, ", ")
292 }
293
294 type filterAction string
295
296 const (
297 // Replace value(s).
298 replaceAction filterAction = "replace"
299
300 // Hash value(s).
301 hashAction filterAction = "hash"
302
303 // Delete.
304 deleteAction filterAction = "delete"
305 )
306
307 func (a filterAction) IsValid() error {
308 switch a {
309 case replaceAction, deleteAction, hashAction:
310 return nil
311 }
312
313 return errors.New("invalid action type")
314 }
315
316 type queryFilterAction struct {
317 // `replace` to replace the value(s) associated with the parameter(s), `hash` to replace them with the 4 initial bytes of the SHA-256 of their content or `delete` to remove them entirely.
318 Type filterAction `json:"type"`
319
320 // The name of the query parameter.
321 Parameter string `json:"parameter"`
322
323 // The value to use as replacement if the action is `replace`.
324 Value string `json:"value,omitempty"`
325 }
326
327 // QueryFilter is a Caddy log field filter that filters
328 // query parameters from a URL.
329 //
330 // This filter updates the logged URL string to remove, replace or hash
331 // query parameters containing sensitive data. For instance, it can be
332 // used to redact any kind of secrets which were passed as query parameters,
333 // such as OAuth access tokens, session IDs, magic link tokens, etc.
334 type QueryFilter struct {
335 // A list of actions to apply to the query parameters of the URL.
336 Actions []queryFilterAction `json:"actions"`
337 }
338
339 // Validate checks that action types are correct.
340 func (f *QueryFilter) Validate() error {
341 for _, a := range f.Actions {
342 if err := a.Type.IsValid(); err != nil {
343 return err
344 }
345 }
346
347 return nil
348 }
349
350 // CaddyModule returns the Caddy module information.
351 func (QueryFilter) CaddyModule() caddy.ModuleInfo {
352 return caddy.ModuleInfo{
353 ID: "caddy.logging.encoders.filter.query",
354 New: func() caddy.Module { return new(QueryFilter) },
355 }
356 }
357
358 // UnmarshalCaddyfile sets up the module from Caddyfile tokens.
359 func (m *QueryFilter) UnmarshalCaddyfile(d *caddyfile.Dispenser) error {
360 d.Next() // consume filter name
361 for d.NextBlock(0) {
362 qfa := queryFilterAction{}
363 switch d.Val() {
364 case "replace":
365 if !d.NextArg() {
366 return d.ArgErr()
367 }
368
369 qfa.Type = replaceAction
370 qfa.Parameter = d.Val()
371
372 if !d.NextArg() {
373 return d.ArgErr()
374 }
375 qfa.Value = d.Val()
376
377 case "hash":
378 if !d.NextArg() {
379 return d.ArgErr()
380 }
381
382 qfa.Type = hashAction
383 qfa.Parameter = d.Val()
384
385 case "delete":
386 if !d.NextArg() {
387 return d.ArgErr()
388 }
389
390 qfa.Type = deleteAction
391 qfa.Parameter = d.Val()
392
393 default:
394 return d.Errf("unrecognized subdirective %s", d.Val())
395 }
396
397 m.Actions = append(m.Actions, qfa)
398 }
399 return nil
400 }
401
402 // Filter filters the input field.
403 func (m QueryFilter) Filter(in zapcore.Field) zapcore.Field {
404 if array, ok := in.Interface.(internal.LoggableStringArray); ok {
405 newArray := make(internal.LoggableStringArray, len(array))
406 for i, s := range array {
407 newArray[i] = m.processQueryString(s)
408 }
409 in.Interface = newArray
410 } else {
411 in.String = m.processQueryString(in.String)
412 }
413
414 return in
415 }
416
417 func (m QueryFilter) processQueryString(s string) string {
418 parse := url.Parse
419 if strings.HasPrefix(s, "/") {
420 // parse a request-target like the server does. Browsers and most
421 // clients strip a #fragment before sending, but a client that
422 // sends a raw '#' has it kept in the query by the server
423 // (url.ParseRequestURI does not split it off), so filter it too
424 parse = url.ParseRequestURI
425 }
426 u, err := parse(s)
427 if err != nil {
428 return s
429 }
430
431 q := u.Query()
432 for _, a := range m.Actions {
433 switch a.Type {
434 case replaceAction:
435 for i := range q[a.Parameter] {
436 q[a.Parameter][i] = a.Value
437 }
438
439 case hashAction:
440 for i := range q[a.Parameter] {
441 q[a.Parameter][i] = hash(q[a.Parameter][i])
442 }
443
444 case deleteAction:
445 q.Del(a.Parameter)
446 }
447 }
448
449 u.RawQuery = q.Encode()
450 return u.String()
451 }
452
453 type cookieFilterAction struct {
454 // `replace` to replace the value of the cookie, `hash` to replace it with the 4 initial bytes of the SHA-256 of its content or `delete` to remove it entirely.
455 Type filterAction `json:"type"`
456
457 // The name of the cookie.
458 Name string `json:"name"`
459
460 // The value to use as replacement if the action is `replace`.
461 Value string `json:"value,omitempty"`
462 }
463
464 // CookieFilter is a Caddy log field filter that filters
465 // cookies.
466 //
467 // This filter updates the logged HTTP header string
468 // to remove, replace or hash cookies containing sensitive data. For instance,
469 // it can be used to redact any kind of secrets, such as session IDs.
470 //
471 // If several actions are configured for the same cookie name, only the first
472 // will be applied.
473 type CookieFilter struct {
474 // A list of actions to apply to the cookies.
475 Actions []cookieFilterAction `json:"actions"`
476 }
477
478 // Validate checks that action types are correct.
479 func (f *CookieFilter) Validate() error {
480 for _, a := range f.Actions {
481 if err := a.Type.IsValid(); err != nil {
482 return err
483 }
484 }
485
486 return nil
487 }
488
489 // CaddyModule returns the Caddy module information.
490 func (CookieFilter) CaddyModule() caddy.ModuleInfo {
491 return caddy.ModuleInfo{
492 ID: "caddy.logging.encoders.filter.cookie",
493 New: func() caddy.Module { return new(CookieFilter) },
494 }
495 }
496
497 // UnmarshalCaddyfile sets up the module from Caddyfile tokens.
498 func (m *CookieFilter) UnmarshalCaddyfile(d *caddyfile.Dispenser) error {
499 d.Next() // consume filter name
500 for d.NextBlock(0) {
501 cfa := cookieFilterAction{}
502 switch d.Val() {
503 case "replace":
504 if !d.NextArg() {
505 return d.ArgErr()
506 }
507
508 cfa.Type = replaceAction
509 cfa.Name = d.Val()
510
511 if !d.NextArg() {
512 return d.ArgErr()
513 }
514 cfa.Value = d.Val()
515
516 case "hash":
517 if !d.NextArg() {
518 return d.ArgErr()
519 }
520
521 cfa.Type = hashAction
522 cfa.Name = d.Val()
523
524 case "delete":
525 if !d.NextArg() {
526 return d.ArgErr()
527 }
528
529 cfa.Type = deleteAction
530 cfa.Name = d.Val()
531
532 default:
533 return d.Errf("unrecognized subdirective %s", d.Val())
534 }
535
536 m.Actions = append(m.Actions, cfa)
537 }
538 return nil
539 }
540
541 // Filter filters the input field.
542 func (m CookieFilter) Filter(in zapcore.Field) zapcore.Field {
543 cookiesSlice, ok := in.Interface.(internal.LoggableStringArray)
544 if !ok {
545 return in
546 }
547
548 // using a dummy Request to make use of the Cookies() function to parse it
549 originRequest := http.Request{Header: http.Header{"Cookie": cookiesSlice}}
550 cookies := originRequest.Cookies()
551 transformedRequest := http.Request{Header: make(http.Header)}
552
553 OUTER:
554 for _, c := range cookies {
555 for _, a := range m.Actions {
556 if c.Name != a.Name {
557 continue
558 }
559
560 switch a.Type {
561 case replaceAction:
562 c.Value = a.Value
563 transformedRequest.AddCookie(c)
564 continue OUTER
565
566 case hashAction:
567 c.Value = hash(c.Value)
568 transformedRequest.AddCookie(c)
569 continue OUTER
570
571 case deleteAction:
572 continue OUTER
573 }
574 }
575
576 transformedRequest.AddCookie(c)
577 }
578
579 in.Interface = internal.LoggableStringArray(transformedRequest.Header["Cookie"])
580
581 return in
582 }
583
584 // SetCookieFilter is a Caddy log field filter that filters
585 // Set-Cookie header values.
586 //
587 // This filter updates logged Set-Cookie header values to remove,
588 // replace or hash cookies containing sensitive data while preserving
589 // the original attribute suffix verbatim.
590 //
591 // It is typically used with access log fields such as
592 // "resp_headers>Set-Cookie".
593 //
594 // If several actions are configured for the same cookie name, only the first
595 // will be applied.
596 type SetCookieFilter struct {
597 // A list of actions to apply to the Set-Cookie values.
598 Actions []cookieFilterAction `json:"actions"`
599 }
600
601 // Validate checks that action types are correct.
602 func (f *SetCookieFilter) Validate() error {
603 for _, a := range f.Actions {
604 if err := a.Type.IsValid(); err != nil {
605 return err
606 }
607 }
608
609 return nil
610 }
611
612 // CaddyModule returns the Caddy module information.
613 func (SetCookieFilter) CaddyModule() caddy.ModuleInfo {
614 return caddy.ModuleInfo{
615 ID: "caddy.logging.encoders.filter.set_cookie",
616 New: func() caddy.Module { return new(SetCookieFilter) },
617 }
618 }
619
620 // UnmarshalCaddyfile sets up the module from Caddyfile tokens.
621 func (m *SetCookieFilter) UnmarshalCaddyfile(d *caddyfile.Dispenser) error {
622 d.Next() // consume filter name
623 for d.NextBlock(0) {
624 cfa := cookieFilterAction{}
625 switch d.Val() {
626 case "replace":
627 if !d.NextArg() {
628 return d.ArgErr()
629 }
630
631 cfa.Type = replaceAction
632 cfa.Name = d.Val()
633
634 if !d.NextArg() {
635 return d.ArgErr()
636 }
637 cfa.Value = d.Val()
638
639 case "hash":
640 if !d.NextArg() {
641 return d.ArgErr()
642 }
643
644 cfa.Type = hashAction
645 cfa.Name = d.Val()
646
647 case "delete":
648 if !d.NextArg() {
649 return d.ArgErr()
650 }
651
652 cfa.Type = deleteAction
653 cfa.Name = d.Val()
654
655 default:
656 return d.Errf("unrecognized subdirective %s", d.Val())
657 }
658
659 m.Actions = append(m.Actions, cfa)
660 }
661 return nil
662 }
663
664 // Filter filters the input field.
665 func (m SetCookieFilter) Filter(in zapcore.Field) zapcore.Field {
666 setCookies, ok := in.Interface.(internal.LoggableStringArray)
667 if !ok {
668 return in
669 }
670
671 transformed := make(internal.LoggableStringArray, 0, len(setCookies))
672 for _, line := range setCookies {
673 filtered, keep := m.filterLine(line)
674 if keep {
675 transformed = append(transformed, filtered)
676 }
677 }
678
679 in.Interface = transformed
680
681 return in
682 }
683
684 func (m SetCookieFilter) filterLine(line string) (string, bool) {
685 cookie, err := http.ParseSetCookie(line)
686 if err != nil {
687 return line, true
688 }
689
690 for _, a := range m.Actions {
691 if cookie.Name != a.Name {
692 continue
693 }
694
695 switch a.Type {
696 case replaceAction:
697 return replaceSetCookieValue(line, cookie, a.Value), true
698
699 case hashAction:
700 // A Set-Cookie-based deletion may still have an empty value
701 // (for example with Max-Age=0). As with the existing hash filter,
702 // hashing an empty value yields the SHA-256 prefix "e3b0c442".
703 return replaceSetCookieValue(line, cookie, hash(cookie.Value)), true
704
705 case deleteAction:
706 return "", false
707 }
708 }
709
710 return line, true
711 }
712
713 // replaceSetCookieValue keeps the original Set-Cookie suffix verbatim and only
714 // swaps out the leading cookie value after ParseSetCookie has identified it.
715 func replaceSetCookieValue(line string, cookie *http.Cookie, newValue string) string {
716 // Only operate on the first Set-Cookie segment: "name=value".
717 headEnd := strings.IndexByte(line, ';')
718 if headEnd == -1 {
719 headEnd = len(line)
720 }
721
722 head := line[:headEnd]
723 eq := strings.IndexByte(head, '=')
724 if eq == -1 {
725 return line
726 }
727
728 name := strings.TrimSpace(head[:eq])
729 if name != cookie.Name {
730 return line
731 }
732
733 // Preserve the original spacing around the value so the logged header stays
734 // as close as possible to the original input.
735 rawValue := head[eq+1:]
736 trimmedValue := strings.TrimSpace(rawValue)
737 leadingWhitespaceLen := len(rawValue) - len(strings.TrimLeftFunc(rawValue, unicode.IsSpace))
738 trailingWhitespaceLen := len(rawValue) - len(strings.TrimRightFunc(rawValue, unicode.IsSpace))
739
740 replacement := newValue
741 // If the original cookie value was quoted, keep the replacement quoted too.
742 if cookie.Quoted && len(trimmedValue) >= 2 && trimmedValue[0] == '"' && trimmedValue[len(trimmedValue)-1] == '"' {
743 replacement = `"` + newValue + `"`
744 }
745
746 // Rebuild only the first "name=value" pair and keep the attribute suffix
747 // byte-for-byte unchanged.
748 return line[:eq+1] +
749 rawValue[:leadingWhitespaceLen] +
750 replacement +
751 rawValue[len(rawValue)-trailingWhitespaceLen:] +
752 line[headEnd:]
753 }
754
755 // RegexpFilter is a Caddy log field filter that
756 // replaces the field matching the provided regexp
757 // with the indicated string. If the field is an
758 // array of strings, each of them will have the
759 // regexp replacement applied.
760 type RegexpFilter struct {
761 // The regular expression pattern defining what to replace.
762 RawRegexp string `json:"regexp,omitempty"`
763
764 // The value to use as replacement
765 Value string `json:"value,omitempty"`
766
767 regexp *regexp.Regexp
768 }
769
770 // CaddyModule returns the Caddy module information.
771 func (RegexpFilter) CaddyModule() caddy.ModuleInfo {
772 return caddy.ModuleInfo{
773 ID: "caddy.logging.encoders.filter.regexp",
774 New: func() caddy.Module { return new(RegexpFilter) },
775 }
776 }
777
778 // UnmarshalCaddyfile sets up the module from Caddyfile tokens.
779 func (f *RegexpFilter) UnmarshalCaddyfile(d *caddyfile.Dispenser) error {
780 d.Next() // consume filter name
781 if d.NextArg() {
782 f.RawRegexp = d.Val()
783 }
784 if d.NextArg() {
785 f.Value = d.Val()
786 }
787 return nil
788 }
789
790 // Provision compiles m's regexp.
791 func (m *RegexpFilter) Provision(ctx caddy.Context) error {
792 r, err := regexp.Compile(m.RawRegexp)
793 if err != nil {
794 return err
795 }
796
797 m.regexp = r
798
799 return nil
800 }
801
802 // Filter filters the input field with the replacement value if it matches the regexp.
803 func (f *RegexpFilter) Filter(in zapcore.Field) zapcore.Field {
804 if array, ok := in.Interface.(internal.LoggableStringArray); ok {
805 newArray := make(internal.LoggableStringArray, len(array))
806 for i, s := range array {
807 newArray[i] = f.regexp.ReplaceAllString(s, f.Value)
808 }
809 in.Interface = newArray
810 } else {
811 in.String = f.regexp.ReplaceAllString(in.String, f.Value)
812 }
813
814 return in
815 }
816
817 // regexpFilterOperation represents a single regexp operation
818 // within a MultiRegexpFilter.
819 type regexpFilterOperation struct {
820 // The regular expression pattern defining what to replace.
821 RawRegexp string `json:"regexp,omitempty"`
822
823 // The value to use as replacement
824 Value string `json:"value,omitempty"`
825
826 regexp *regexp.Regexp
827 }
828
829 // MultiRegexpFilter is a Caddy log field filter that
830 // can apply multiple regular expression replacements to
831 // the same field. This filter processes operations in the
832 // order they are defined, applying each regexp replacement
833 // sequentially to the result of the previous operation.
834 //
835 // This allows users to define multiple regexp filters for
836 // the same field without them overwriting each other.
837 //
838 // Security considerations:
839 // - Uses Go's regexp package (RE2 engine) which is safe from ReDoS attacks
840 // - Validates all patterns during provisioning
841 // - Limits the maximum number of operations to prevent resource exhaustion
842 // - Sanitizes input to prevent injection attacks
843 type MultiRegexpFilter struct {
844 // A list of regexp operations to apply in sequence.
845 // Maximum of 50 operations allowed for security and performance.
846 Operations []regexpFilterOperation `json:"operations"`
847 }
848
849 // Security constants
850 const (
851 maxRegexpOperations = 50 // Maximum operations to prevent resource exhaustion
852 maxPatternLength = 1000 // Maximum pattern length to prevent abuse
853 )
854
855 // CaddyModule returns the Caddy module information.
856 func (MultiRegexpFilter) CaddyModule() caddy.ModuleInfo {
857 return caddy.ModuleInfo{
858 ID: "caddy.logging.encoders.filter.multi_regexp",
859 New: func() caddy.Module { return new(MultiRegexpFilter) },
860 }
861 }
862
863 // UnmarshalCaddyfile sets up the module from Caddyfile tokens.
864 // Syntax:
865 //
866 // multi_regexp {
867 // regexp <pattern> <replacement>
868 // regexp <pattern> <replacement>
869 // ...
870 // }
871 func (f *MultiRegexpFilter) UnmarshalCaddyfile(d *caddyfile.Dispenser) error {
872 d.Next() // consume filter name
873 for d.NextBlock(0) {
874 switch d.Val() {
875 case "regexp":
876 // Security check: limit number of operations
877 if len(f.Operations) >= maxRegexpOperations {
878 return d.Errf("too many regexp operations (maximum %d allowed)", maxRegexpOperations)
879 }
880
881 op := regexpFilterOperation{}
882 if !d.NextArg() {
883 return d.ArgErr()
884 }
885 op.RawRegexp = d.Val()
886
887 // Security validation: check pattern length
888 if len(op.RawRegexp) > maxPatternLength {
889 return d.Errf("regexp pattern too long (maximum %d characters)", maxPatternLength)
890 }
891
892 // Security validation: basic pattern validation
893 if op.RawRegexp == "" {
894 return d.Errf("regexp pattern cannot be empty")
895 }
896
897 if !d.NextArg() {
898 return d.ArgErr()
899 }
900 op.Value = d.Val()
901 f.Operations = append(f.Operations, op)
902 default:
903 return d.Errf("unrecognized subdirective %s", d.Val())
904 }
905 }
906
907 // Security check: ensure at least one operation is defined
908 if len(f.Operations) == 0 {
909 return d.Err("multi_regexp filter requires at least one regexp operation")
910 }
911
912 return nil
913 }
914
915 // Provision compiles all regexp patterns with security validation.
916 func (f *MultiRegexpFilter) Provision(ctx caddy.Context) error {
917 // Security check: validate operation count
918 if len(f.Operations) > maxRegexpOperations {
919 return fmt.Errorf("too many regexp operations: %d (maximum %d allowed)", len(f.Operations), maxRegexpOperations)
920 }
921
922 if len(f.Operations) == 0 {
923 return fmt.Errorf("multi_regexp filter requires at least one operation")
924 }
925
926 for i := range f.Operations {
927 // Security validation: pattern length check
928 if len(f.Operations[i].RawRegexp) > maxPatternLength {
929 return fmt.Errorf("regexp pattern %d too long: %d characters (maximum %d)", i, len(f.Operations[i].RawRegexp), maxPatternLength)
930 }
931
932 // Security validation: empty pattern check
933 if f.Operations[i].RawRegexp == "" {
934 return fmt.Errorf("regexp pattern %d cannot be empty", i)
935 }
936
937 // Compile and validate the pattern (uses RE2 engine - safe from ReDoS)
938 r, err := regexp.Compile(f.Operations[i].RawRegexp)
939 if err != nil {
940 return fmt.Errorf("compiling regexp pattern %d (%s): %v", i, f.Operations[i].RawRegexp, err)
941 }
942 f.Operations[i].regexp = r
943 }
944 return nil
945 }
946
947 // Validate ensures the filter is properly configured with security checks.
948 func (f *MultiRegexpFilter) Validate() error {
949 if len(f.Operations) == 0 {
950 return fmt.Errorf("multi_regexp filter requires at least one operation")
951 }
952
953 if len(f.Operations) > maxRegexpOperations {
954 return fmt.Errorf("too many regexp operations: %d (maximum %d allowed)", len(f.Operations), maxRegexpOperations)
955 }
956
957 for i, op := range f.Operations {
958 if op.RawRegexp == "" {
959 return fmt.Errorf("regexp pattern %d cannot be empty", i)
960 }
961 if len(op.RawRegexp) > maxPatternLength {
962 return fmt.Errorf("regexp pattern %d too long: %d characters (maximum %d)", i, len(op.RawRegexp), maxPatternLength)
963 }
964 if op.regexp == nil {
965 return fmt.Errorf("regexp pattern %d not compiled (call Provision first)", i)
966 }
967 }
968 return nil
969 }
970
971 // Filter applies all regexp operations sequentially to the input field.
972 // Input is sanitized and validated for security.
973 func (f *MultiRegexpFilter) Filter(in zapcore.Field) zapcore.Field {
974 if array, ok := in.Interface.(internal.LoggableStringArray); ok {
975 newArray := make(internal.LoggableStringArray, len(array))
976 for i, s := range array {
977 newArray[i] = f.processString(s)
978 }
979 in.Interface = newArray
980 } else {
981 in.String = f.processString(in.String)
982 }
983
984 return in
985 }
986
987 // processString applies all regexp operations to a single string with input validation.
988 func (f *MultiRegexpFilter) processString(s string) string {
989 // Security: validate input string length to prevent resource exhaustion
990 const maxInputLength = 1000000 // 1MB max input size
991 if len(s) > maxInputLength {
992 // Log warning but continue processing (truncated)
993 s = s[:maxInputLength]
994 }
995
996 result := s
997 for _, op := range f.Operations {
998 // Each regexp operation is applied sequentially
999 // Using RE2 engine which is safe from ReDoS attacks
1000 result = op.regexp.ReplaceAllString(result, op.Value)
1001
1002 // Ensure result doesn't exceed max length after each operation
1003 if len(result) > maxInputLength {
1004 result = result[:maxInputLength]
1005 }
1006 }
1007 return result
1008 }
1009
1010 // AddOperation adds a single regexp operation to the filter with validation.
1011 // This is used when merging multiple RegexpFilter instances.
1012 func (f *MultiRegexpFilter) AddOperation(rawRegexp, value string) error {
1013 // Security checks
1014 if len(f.Operations) >= maxRegexpOperations {
1015 return fmt.Errorf("cannot add operation: maximum %d operations allowed", maxRegexpOperations)
1016 }
1017
1018 if rawRegexp == "" {
1019 return fmt.Errorf("regexp pattern cannot be empty")
1020 }
1021
1022 if len(rawRegexp) > maxPatternLength {
1023 return fmt.Errorf("regexp pattern too long: %d characters (maximum %d)", len(rawRegexp), maxPatternLength)
1024 }
1025
1026 f.Operations = append(f.Operations, regexpFilterOperation{
1027 RawRegexp: rawRegexp,
1028 Value: value,
1029 })
1030 return nil
1031 }
1032
1033 // RenameFilter is a Caddy log field filter that
1034 // renames the field's key with the indicated name.
1035 type RenameFilter struct {
1036 Name string `json:"name,omitempty"`
1037 }
1038
1039 // CaddyModule returns the Caddy module information.
1040 func (RenameFilter) CaddyModule() caddy.ModuleInfo {
1041 return caddy.ModuleInfo{
1042 ID: "caddy.logging.encoders.filter.rename",
1043 New: func() caddy.Module { return new(RenameFilter) },
1044 }
1045 }
1046
1047 // UnmarshalCaddyfile sets up the module from Caddyfile tokens.
1048 func (f *RenameFilter) UnmarshalCaddyfile(d *caddyfile.Dispenser) error {
1049 d.Next() // consume filter name
1050 if d.NextArg() {
1051 f.Name = d.Val()
1052 }
1053 return nil
1054 }
1055
1056 // Filter renames the input field with the replacement name.
1057 func (f *RenameFilter) Filter(in zapcore.Field) zapcore.Field {
1058 in.Key = f.Name
1059 return in
1060 }
1061
1062 // Interface guards
1063 var (
1064 _ LogFieldFilter = (*DeleteFilter)(nil)
1065 _ LogFieldFilter = (*HashFilter)(nil)
1066 _ LogFieldFilter = (*ReplaceFilter)(nil)
1067 _ LogFieldFilter = (*IPMaskFilter)(nil)
1068 _ LogFieldFilter = (*QueryFilter)(nil)
1069 _ LogFieldFilter = (*CookieFilter)(nil)
1070 _ LogFieldFilter = (*SetCookieFilter)(nil)
1071 _ LogFieldFilter = (*RegexpFilter)(nil)
1072 _ LogFieldFilter = (*RenameFilter)(nil)
1073 _ LogFieldFilter = (*MultiRegexpFilter)(nil)
1074
1075 _ caddyfile.Unmarshaler = (*DeleteFilter)(nil)
1076 _ caddyfile.Unmarshaler = (*HashFilter)(nil)
1077 _ caddyfile.Unmarshaler = (*ReplaceFilter)(nil)
1078 _ caddyfile.Unmarshaler = (*IPMaskFilter)(nil)
1079 _ caddyfile.Unmarshaler = (*QueryFilter)(nil)
1080 _ caddyfile.Unmarshaler = (*CookieFilter)(nil)
1081 _ caddyfile.Unmarshaler = (*SetCookieFilter)(nil)
1082 _ caddyfile.Unmarshaler = (*RegexpFilter)(nil)
1083 _ caddyfile.Unmarshaler = (*RenameFilter)(nil)
1084 _ caddyfile.Unmarshaler = (*MultiRegexpFilter)(nil)
1085
1086 _ caddy.Provisioner = (*IPMaskFilter)(nil)
1087 _ caddy.Provisioner = (*RegexpFilter)(nil)
1088 _ caddy.Provisioner = (*MultiRegexpFilter)(nil)
1089
1090 _ caddy.Validator = (*QueryFilter)(nil)
1091 _ caddy.Validator = (*SetCookieFilter)(nil)
1092 _ caddy.Validator = (*MultiRegexpFilter)(nil)
1093 )