1
0
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:
Amir Raminfar
2025-05-21 08:52:26 -07:00
committed by GitHub
parent 50c3608a5e
commit 804199aa9a
11 changed files with 319 additions and 235 deletions

View File

@@ -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
}

View File

@@ -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)
}
}

View 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
}

View 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)
}

View 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)
}

View File

@@ -20,7 +20,6 @@ import (
"github.com/amir20/dozzle/internal/auth"
"github.com/amir20/dozzle/internal/container"
container_support "github.com/amir20/dozzle/internal/support/container"
"github.com/amir20/dozzle/internal/support/search"
support_web "github.com/amir20/dozzle/internal/support/web"
"github.com/amir20/dozzle/internal/utils"
"github.com/dustin/go-humanize"
@@ -68,7 +67,7 @@ func (h *handler) fetchLogsBetweenDates(w http.ResponseWriter, r *http.Request)
var regex *regexp.Regexp
if r.URL.Query().Has("filter") {
regex, err = search.ParseRegex(r.URL.Query().Get("filter"))
regex, err = support_web.ParseRegex(r.URL.Query().Get("filter"))
if err != nil {
http.Error(w, err.Error(), http.StatusBadRequest)
return
@@ -144,7 +143,7 @@ func (h *handler) fetchLogsBetweenDates(w http.ResponseWriter, r *http.Request)
}
} else {
if regex != nil {
if !search.Search(regex, event) {
if !support_web.Search(regex, event) {
continue
}
}
@@ -158,7 +157,7 @@ func (h *handler) fetchLogsBetweenDates(w http.ResponseWriter, r *http.Request)
break
}
support_web.EscapeHTMLValues(event) // only escape when not exporting
support_web.EscapeHTMLValues(event)
buffer.Push(event)
}
}
@@ -280,7 +279,7 @@ func (h *handler) streamLogsForContainers(w http.ResponseWriter, r *http.Request
if r.URL.Query().Has("filter") {
var err error
regex, err = search.ParseRegex(r.URL.Query().Get("filter"))
regex, err = support_web.ParseRegex(r.URL.Query().Get("filter"))
if err != nil {
http.Error(w, err.Error(), http.StatusBadRequest)
return
@@ -317,7 +316,7 @@ func (h *handler) streamLogsForContainers(w http.ResponseWriter, r *http.Request
if _, ok := levels[log.Level]; !ok {
continue
}
if search.Search(regex, log) {
if support_web.Search(regex, log) {
events = append(events, log)
}
}
@@ -384,7 +383,7 @@ loop:
select {
case logEvent := <-liveLogs:
if regex != nil {
if !search.Search(regex, logEvent) {
if !support_web.Search(regex, logEvent) {
continue
}
}