mirror of
https://github.com/amir20/dozzle.git
synced 2025-12-21 13:23:07 +01:00
fix: fixes url pattern matcher with quotes (#3902)
This commit is contained in:
@@ -1,184 +0,0 @@
|
||||
package search
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"regexp"
|
||||
"strings"
|
||||
|
||||
"github.com/amir20/dozzle/internal/container"
|
||||
"github.com/rs/zerolog/log"
|
||||
orderedmap "github.com/wk8/go-ordered-map/v2"
|
||||
)
|
||||
|
||||
const (
|
||||
MarkerStart = "\uE000"
|
||||
MarkerEnd = "\uE001"
|
||||
)
|
||||
|
||||
func ParseRegex(search string) (*regexp.Regexp, error) {
|
||||
flags := ""
|
||||
|
||||
if search == strings.ToLower(search) {
|
||||
flags = "(?i)"
|
||||
}
|
||||
re, err := regexp.Compile(flags + search)
|
||||
|
||||
if err != nil {
|
||||
log.Debug().Err(err).Str("search", search).Msg("failed to compile regex")
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return re, nil
|
||||
}
|
||||
|
||||
func Search(re *regexp.Regexp, logEvent *container.LogEvent) bool {
|
||||
switch value := logEvent.Message.(type) {
|
||||
case string:
|
||||
if re.MatchString(value) {
|
||||
logEvent.Message = re.ReplaceAllString(value, MarkerStart+"$0"+MarkerEnd)
|
||||
return true
|
||||
}
|
||||
|
||||
case *orderedmap.OrderedMap[string, any]:
|
||||
return searchMapAny(re, value)
|
||||
|
||||
case *orderedmap.OrderedMap[string, string]:
|
||||
return searchMapString(re, value)
|
||||
|
||||
case map[string]interface{}:
|
||||
panic("not implemented")
|
||||
case map[string]string:
|
||||
panic("not implemented")
|
||||
default:
|
||||
log.Debug().Type("type", value).Msg("unknown logEvent type")
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
func searchMapAny(re *regexp.Regexp, orderedMap *orderedmap.OrderedMap[string, any]) bool {
|
||||
found := false
|
||||
for pair := orderedMap.Oldest(); pair != nil; pair = pair.Next() {
|
||||
switch value := pair.Value.(type) {
|
||||
case string:
|
||||
if replaced, matched := searchString(re, value); matched {
|
||||
found = true
|
||||
orderedMap.Set(pair.Key, replaced)
|
||||
}
|
||||
|
||||
case []any:
|
||||
if searchArray(re, value) {
|
||||
found = true
|
||||
}
|
||||
|
||||
case *orderedmap.OrderedMap[string, any]:
|
||||
if searchMapAny(re, value) {
|
||||
found = true
|
||||
}
|
||||
|
||||
case *orderedmap.OrderedMap[string, string]:
|
||||
if searchMapString(re, value) {
|
||||
found = true
|
||||
}
|
||||
|
||||
case map[string]interface{}:
|
||||
if searchMap(re, value) {
|
||||
found = true
|
||||
}
|
||||
|
||||
case int, float64, bool:
|
||||
formatted := fmt.Sprintf("%v", value)
|
||||
if re.MatchString(formatted) {
|
||||
orderedMap.Set(pair.Key, re.ReplaceAllString(formatted, MarkerStart+"$0"+MarkerEnd))
|
||||
found = true
|
||||
}
|
||||
|
||||
default:
|
||||
log.Debug().Type("type", value).Msg("unknown logEvent type inside searchMapAny")
|
||||
}
|
||||
}
|
||||
|
||||
return found
|
||||
}
|
||||
|
||||
func searchMap(re *regexp.Regexp, data map[string]interface{}) bool {
|
||||
found := false
|
||||
for key, value := range data {
|
||||
switch value := value.(type) {
|
||||
case string:
|
||||
if replaced, matched := searchString(re, value); matched {
|
||||
found = true
|
||||
data[key] = replaced
|
||||
}
|
||||
case []any:
|
||||
if searchArray(re, value) {
|
||||
found = true
|
||||
}
|
||||
|
||||
case map[string]interface{}:
|
||||
if searchMap(re, value) {
|
||||
found = true
|
||||
}
|
||||
|
||||
case int, float64, bool:
|
||||
formatted := fmt.Sprintf("%v", value)
|
||||
if re.MatchString(formatted) {
|
||||
data[key] = re.ReplaceAllString(formatted, MarkerStart+"$0"+MarkerEnd)
|
||||
found = true
|
||||
}
|
||||
default:
|
||||
log.Debug().Type("type", value).Msg("unknown logEvent type inside searchMap")
|
||||
}
|
||||
}
|
||||
|
||||
return found
|
||||
}
|
||||
|
||||
func searchMapString(re *regexp.Regexp, orderedMap *orderedmap.OrderedMap[string, string]) bool {
|
||||
found := false
|
||||
for pair := orderedMap.Oldest(); pair != nil; pair = pair.Next() {
|
||||
if replaced, matched := searchString(re, pair.Value); matched {
|
||||
found = true
|
||||
orderedMap.Set(pair.Key, replaced)
|
||||
}
|
||||
}
|
||||
return found
|
||||
}
|
||||
|
||||
func searchArray(re *regexp.Regexp, data []any) bool {
|
||||
found := false
|
||||
for i, value := range data {
|
||||
switch value := value.(type) {
|
||||
case string:
|
||||
if replaced, matched := searchString(re, value); matched {
|
||||
found = true
|
||||
data[i] = replaced
|
||||
}
|
||||
case int, float64, bool:
|
||||
formatted := fmt.Sprintf("%v", value)
|
||||
if re.MatchString(formatted) {
|
||||
data[i] = re.ReplaceAllString(formatted, MarkerStart+"$0"+MarkerEnd)
|
||||
found = true
|
||||
}
|
||||
case []any:
|
||||
if searchArray(re, value) {
|
||||
found = true
|
||||
}
|
||||
case map[string]interface{}:
|
||||
if searchMap(re, value) {
|
||||
found = true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return found
|
||||
}
|
||||
|
||||
func searchString(re *regexp.Regexp, value string) (string, bool) {
|
||||
if re.MatchString(value) {
|
||||
replaced := re.ReplaceAllString(value, MarkerStart+"$0"+MarkerEnd)
|
||||
return replaced, true
|
||||
}
|
||||
|
||||
return value, false
|
||||
}
|
||||
@@ -2,20 +2,25 @@ package support_web
|
||||
|
||||
import (
|
||||
"html"
|
||||
"regexp"
|
||||
"strings"
|
||||
|
||||
"github.com/amir20/dozzle/internal/container"
|
||||
"github.com/amir20/dozzle/internal/support/search"
|
||||
|
||||
"github.com/rs/zerolog/log"
|
||||
orderedmap "github.com/wk8/go-ordered-map/v2"
|
||||
)
|
||||
|
||||
// URL marker regex compiled once for performance
|
||||
var urlMarkerRegex = regexp.MustCompile(URLMarkerStart + "(.*?)" + URLMarkerEnd)
|
||||
|
||||
func EscapeHTMLValues(logEvent *container.LogEvent) {
|
||||
// Mark URLs before HTML escaping
|
||||
MarkURLs(logEvent)
|
||||
|
||||
switch value := logEvent.Message.(type) {
|
||||
case string:
|
||||
value = html.EscapeString(value)
|
||||
value = strings.ReplaceAll(value, search.MarkerStart, "<mark>")
|
||||
logEvent.Message = strings.ReplaceAll(value, search.MarkerEnd, "</mark>")
|
||||
logEvent.Message = escapeAndProcessMarkers(value)
|
||||
|
||||
case *orderedmap.OrderedMap[string, any]:
|
||||
escapeAnyMap(value)
|
||||
@@ -34,28 +39,53 @@ func EscapeHTMLValues(logEvent *container.LogEvent) {
|
||||
}
|
||||
}
|
||||
|
||||
func escapeAndProcessMarkers(value string) string {
|
||||
value = html.EscapeString(value)
|
||||
value = strings.ReplaceAll(value, MarkerStart, "<mark>")
|
||||
value = strings.ReplaceAll(value, MarkerEnd, "</mark>")
|
||||
// Process URL markers
|
||||
value = urlMarkerRegex.ReplaceAllString(value, "<a href=\"$1\" target=\"_blank\" rel=\"noopener noreferrer external\">$1</a>")
|
||||
return value
|
||||
}
|
||||
|
||||
func escapeAnyMap(orderedMap *orderedmap.OrderedMap[string, any]) {
|
||||
for pair := orderedMap.Oldest(); pair != nil; pair = pair.Next() {
|
||||
switch value := pair.Value.(type) {
|
||||
case string:
|
||||
value = html.EscapeString(value)
|
||||
value = strings.ReplaceAll(value, search.MarkerStart, "<mark>")
|
||||
value = strings.ReplaceAll(value, search.MarkerEnd, "</mark>")
|
||||
orderedMap.Set(pair.Key, value)
|
||||
orderedMap.Set(pair.Key, escapeAndProcessMarkers(value))
|
||||
case *orderedmap.OrderedMap[string, any]:
|
||||
escapeAnyMap(value)
|
||||
case *orderedmap.OrderedMap[string, string]:
|
||||
escapeStringMap(value)
|
||||
case map[string]interface{}:
|
||||
escapeMapStringInterface(value)
|
||||
case map[string]string:
|
||||
escapeStringMapString(value)
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
func escapeStringMap(orderedMap *orderedmap.OrderedMap[string, string]) {
|
||||
for pair := orderedMap.Oldest(); pair != nil; pair = pair.Next() {
|
||||
value := html.EscapeString(pair.Value)
|
||||
value = strings.ReplaceAll(value, search.MarkerStart, "<mark>")
|
||||
value = strings.ReplaceAll(value, search.MarkerEnd, "</mark>")
|
||||
orderedMap.Set(pair.Key, value)
|
||||
orderedMap.Set(pair.Key, escapeAndProcessMarkers(pair.Value))
|
||||
}
|
||||
}
|
||||
|
||||
func escapeMapStringInterface(value map[string]interface{}) {
|
||||
for key, val := range value {
|
||||
switch val := val.(type) {
|
||||
case string:
|
||||
value[key] = escapeAndProcessMarkers(val)
|
||||
case map[string]interface{}:
|
||||
escapeMapStringInterface(val)
|
||||
case map[string]string:
|
||||
escapeStringMapString(val)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func escapeStringMapString(value map[string]string) {
|
||||
for key, val := range value {
|
||||
value[key] = escapeAndProcessMarkers(val)
|
||||
}
|
||||
}
|
||||
|
||||
198
internal/support/web/regex.go
Normal file
198
internal/support/web/regex.go
Normal file
@@ -0,0 +1,198 @@
|
||||
package support_web
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"regexp"
|
||||
"strings"
|
||||
|
||||
"github.com/amir20/dozzle/internal/container"
|
||||
"github.com/rs/zerolog/log"
|
||||
orderedmap "github.com/wk8/go-ordered-map/v2"
|
||||
)
|
||||
|
||||
// PatternMatcher defines the interface for a regex pattern matcher
|
||||
type PatternMatcher struct {
|
||||
Regex *regexp.Regexp
|
||||
MarkerStart string
|
||||
MarkerEnd string
|
||||
}
|
||||
|
||||
// NewPatternMatcher creates a new pattern matcher with the specified regex and markers
|
||||
func NewPatternMatcher(re *regexp.Regexp, markerStart, markerEnd string) *PatternMatcher {
|
||||
return &PatternMatcher{
|
||||
Regex: re,
|
||||
MarkerStart: markerStart,
|
||||
MarkerEnd: markerEnd,
|
||||
}
|
||||
}
|
||||
|
||||
// CreateRegex compiles a regex pattern with optional case-insensitive flag
|
||||
func CreateRegex(pattern string, caseInsensitive bool) (*regexp.Regexp, error) {
|
||||
flags := ""
|
||||
if caseInsensitive || pattern == strings.ToLower(pattern) {
|
||||
flags = "(?i)"
|
||||
}
|
||||
|
||||
re, err := regexp.Compile(flags + pattern)
|
||||
if err != nil {
|
||||
log.Debug().Err(err).Str("pattern", pattern).Msg("failed to compile regex")
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return re, nil
|
||||
}
|
||||
|
||||
// MarkInLogEvent applies the pattern matcher to the log event's message
|
||||
func (pm *PatternMatcher) MarkInLogEvent(logEvent *container.LogEvent) bool {
|
||||
switch value := logEvent.Message.(type) {
|
||||
case string:
|
||||
if pm.Regex.MatchString(value) {
|
||||
logEvent.Message = pm.Regex.ReplaceAllString(value, pm.MarkerStart+"$0"+pm.MarkerEnd)
|
||||
return true
|
||||
}
|
||||
|
||||
case *orderedmap.OrderedMap[string, any]:
|
||||
return pm.markMapAny(value)
|
||||
|
||||
case *orderedmap.OrderedMap[string, string]:
|
||||
return pm.markMapString(value)
|
||||
|
||||
case map[string]interface{}:
|
||||
return pm.markMap(value)
|
||||
|
||||
case map[string]string:
|
||||
panic("not implemented")
|
||||
|
||||
default:
|
||||
log.Debug().Type("type", value).Msg("unknown logEvent type")
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
func (pm *PatternMatcher) markMapAny(orderedMap *orderedmap.OrderedMap[string, any]) bool {
|
||||
found := false
|
||||
for pair := orderedMap.Oldest(); pair != nil; pair = pair.Next() {
|
||||
switch value := pair.Value.(type) {
|
||||
case string:
|
||||
if replaced, matched := pm.markString(value); matched {
|
||||
found = true
|
||||
orderedMap.Set(pair.Key, replaced)
|
||||
}
|
||||
|
||||
case []any:
|
||||
if pm.markArray(value) {
|
||||
found = true
|
||||
}
|
||||
|
||||
case *orderedmap.OrderedMap[string, any]:
|
||||
if pm.markMapAny(value) {
|
||||
found = true
|
||||
}
|
||||
|
||||
case *orderedmap.OrderedMap[string, string]:
|
||||
if pm.markMapString(value) {
|
||||
found = true
|
||||
}
|
||||
|
||||
case map[string]interface{}:
|
||||
if pm.markMap(value) {
|
||||
found = true
|
||||
}
|
||||
|
||||
case int, float64, bool:
|
||||
formatted := fmt.Sprintf("%v", value)
|
||||
if pm.Regex.MatchString(formatted) {
|
||||
orderedMap.Set(pair.Key, pm.Regex.ReplaceAllString(formatted, pm.MarkerStart+"$0"+pm.MarkerEnd))
|
||||
found = true
|
||||
}
|
||||
|
||||
default:
|
||||
log.Debug().Type("type", value).Msg("unknown logEvent type inside markMapAny")
|
||||
}
|
||||
}
|
||||
|
||||
return found
|
||||
}
|
||||
|
||||
func (pm *PatternMatcher) markMap(data map[string]interface{}) bool {
|
||||
found := false
|
||||
for key, value := range data {
|
||||
switch value := value.(type) {
|
||||
case string:
|
||||
if replaced, matched := pm.markString(value); matched {
|
||||
found = true
|
||||
data[key] = replaced
|
||||
}
|
||||
case []any:
|
||||
if pm.markArray(value) {
|
||||
found = true
|
||||
}
|
||||
|
||||
case map[string]interface{}:
|
||||
if pm.markMap(value) {
|
||||
found = true
|
||||
}
|
||||
|
||||
case int, float64, bool:
|
||||
formatted := fmt.Sprintf("%v", value)
|
||||
if pm.Regex.MatchString(formatted) {
|
||||
data[key] = pm.Regex.ReplaceAllString(formatted, pm.MarkerStart+"$0"+pm.MarkerEnd)
|
||||
found = true
|
||||
}
|
||||
default:
|
||||
log.Debug().Type("type", value).Msg("unknown logEvent type inside markMap")
|
||||
}
|
||||
}
|
||||
|
||||
return found
|
||||
}
|
||||
|
||||
func (pm *PatternMatcher) markMapString(orderedMap *orderedmap.OrderedMap[string, string]) bool {
|
||||
found := false
|
||||
for pair := orderedMap.Oldest(); pair != nil; pair = pair.Next() {
|
||||
if replaced, matched := pm.markString(pair.Value); matched {
|
||||
found = true
|
||||
orderedMap.Set(pair.Key, replaced)
|
||||
}
|
||||
}
|
||||
return found
|
||||
}
|
||||
|
||||
func (pm *PatternMatcher) markArray(data []any) bool {
|
||||
found := false
|
||||
for i, value := range data {
|
||||
switch value := value.(type) {
|
||||
case string:
|
||||
if replaced, matched := pm.markString(value); matched {
|
||||
found = true
|
||||
data[i] = replaced
|
||||
}
|
||||
case int, float64, bool:
|
||||
formatted := fmt.Sprintf("%v", value)
|
||||
if pm.Regex.MatchString(formatted) {
|
||||
data[i] = pm.Regex.ReplaceAllString(formatted, pm.MarkerStart+"$0"+pm.MarkerEnd)
|
||||
found = true
|
||||
}
|
||||
case []any:
|
||||
if pm.markArray(value) {
|
||||
found = true
|
||||
}
|
||||
case map[string]interface{}:
|
||||
if pm.markMap(value) {
|
||||
found = true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return found
|
||||
}
|
||||
|
||||
func (pm *PatternMatcher) markString(value string) (string, bool) {
|
||||
if pm.Regex.MatchString(value) {
|
||||
replaced := pm.Regex.ReplaceAllString(value, pm.MarkerStart+"$0"+pm.MarkerEnd)
|
||||
return replaced, true
|
||||
}
|
||||
|
||||
return value, false
|
||||
}
|
||||
22
internal/support/web/search.go
Normal file
22
internal/support/web/search.go
Normal file
@@ -0,0 +1,22 @@
|
||||
package support_web
|
||||
|
||||
import (
|
||||
"regexp"
|
||||
"strings"
|
||||
|
||||
"github.com/amir20/dozzle/internal/container"
|
||||
)
|
||||
|
||||
const (
|
||||
MarkerStart = "\uE000"
|
||||
MarkerEnd = "\uE001"
|
||||
)
|
||||
|
||||
func ParseRegex(search string) (*regexp.Regexp, error) {
|
||||
return CreateRegex(search, search == strings.ToLower(search))
|
||||
}
|
||||
|
||||
func Search(re *regexp.Regexp, logEvent *container.LogEvent) bool {
|
||||
matcher := NewPatternMatcher(re, MarkerStart, MarkerEnd)
|
||||
return matcher.MarkInLogEvent(logEvent)
|
||||
}
|
||||
21
internal/support/web/url.go
Normal file
21
internal/support/web/url.go
Normal file
@@ -0,0 +1,21 @@
|
||||
package support_web
|
||||
|
||||
import (
|
||||
"regexp"
|
||||
|
||||
"github.com/amir20/dozzle/internal/container"
|
||||
)
|
||||
|
||||
const (
|
||||
URLMarkerStart = "\uE002"
|
||||
URLMarkerEnd = "\uE003"
|
||||
)
|
||||
|
||||
// Standard URL regex pattern to match http/https URLs
|
||||
var urlRegex = regexp.MustCompile(`(https?:\/\/[-a-zA-Z0-9@:%._\+~#=]{1,256}\.[a-zA-Z0-9()]{1,6}\b(?:[-a-zA-Z0-9()@:%_\+.~#?&//=]*))`)
|
||||
|
||||
// MarkURLs marks URLs in the logEvent message with special markers
|
||||
func MarkURLs(logEvent *container.LogEvent) bool {
|
||||
matcher := NewPatternMatcher(urlRegex, URLMarkerStart, URLMarkerEnd)
|
||||
return matcher.MarkInLogEvent(logEvent)
|
||||
}
|
||||
Reference in New Issue
Block a user