feat(sync): full code sync from release
This commit is contained in:
@@ -3,7 +3,9 @@ package logredact
|
||||
import (
|
||||
"encoding/json"
|
||||
"regexp"
|
||||
"sort"
|
||||
"strings"
|
||||
"sync"
|
||||
)
|
||||
|
||||
// maxRedactDepth 限制递归深度以防止栈溢出
|
||||
@@ -31,9 +33,18 @@ var defaultSensitiveKeyList = []string{
|
||||
"password",
|
||||
}
|
||||
|
||||
type textRedactPatterns struct {
|
||||
reJSONLike *regexp.Regexp
|
||||
reQueryLike *regexp.Regexp
|
||||
rePlain *regexp.Regexp
|
||||
}
|
||||
|
||||
var (
|
||||
reGOCSPX = regexp.MustCompile(`GOCSPX-[0-9A-Za-z_-]{24,}`)
|
||||
reAIza = regexp.MustCompile(`AIza[0-9A-Za-z_-]{35}`)
|
||||
|
||||
defaultTextRedactPatterns = compileTextRedactPatterns(nil)
|
||||
extraTextPatternCache sync.Map // map[string]*textRedactPatterns
|
||||
)
|
||||
|
||||
func RedactMap(input map[string]any, extraKeys ...string) map[string]any {
|
||||
@@ -83,23 +94,71 @@ func RedactText(input string, extraKeys ...string) string {
|
||||
return RedactJSON(raw, extraKeys...)
|
||||
}
|
||||
|
||||
keyAlt := buildKeyAlternation(extraKeys)
|
||||
// JSON-like: "access_token":"..."
|
||||
reJSONLike := regexp.MustCompile(`(?i)("(?:` + keyAlt + `)"\s*:\s*")([^"]*)(")`)
|
||||
// Query-like: access_token=...
|
||||
reQueryLike := regexp.MustCompile(`(?i)\b((?:` + keyAlt + `))=([^&\s]+)`)
|
||||
// Plain: access_token: ... / access_token = ...
|
||||
rePlain := regexp.MustCompile(`(?i)\b((?:` + keyAlt + `))\b(\s*[:=]\s*)([^,\s]+)`)
|
||||
patterns := getTextRedactPatterns(extraKeys)
|
||||
|
||||
out := input
|
||||
out = reGOCSPX.ReplaceAllString(out, "GOCSPX-***")
|
||||
out = reAIza.ReplaceAllString(out, "AIza***")
|
||||
out = reJSONLike.ReplaceAllString(out, `$1***$3`)
|
||||
out = reQueryLike.ReplaceAllString(out, `$1=***`)
|
||||
out = rePlain.ReplaceAllString(out, `$1$2***`)
|
||||
out = patterns.reJSONLike.ReplaceAllString(out, `$1***$3`)
|
||||
out = patterns.reQueryLike.ReplaceAllString(out, `$1=***`)
|
||||
out = patterns.rePlain.ReplaceAllString(out, `$1$2***`)
|
||||
return out
|
||||
}
|
||||
|
||||
func compileTextRedactPatterns(extraKeys []string) *textRedactPatterns {
|
||||
keyAlt := buildKeyAlternation(extraKeys)
|
||||
return &textRedactPatterns{
|
||||
// JSON-like: "access_token":"..."
|
||||
reJSONLike: regexp.MustCompile(`(?i)("(?:` + keyAlt + `)"\s*:\s*")([^"]*)(")`),
|
||||
// Query-like: access_token=...
|
||||
reQueryLike: regexp.MustCompile(`(?i)\b((?:` + keyAlt + `))=([^&\s]+)`),
|
||||
// Plain: access_token: ... / access_token = ...
|
||||
rePlain: regexp.MustCompile(`(?i)\b((?:` + keyAlt + `))\b(\s*[:=]\s*)([^,\s]+)`),
|
||||
}
|
||||
}
|
||||
|
||||
func getTextRedactPatterns(extraKeys []string) *textRedactPatterns {
|
||||
normalizedExtraKeys := normalizeAndSortExtraKeys(extraKeys)
|
||||
if len(normalizedExtraKeys) == 0 {
|
||||
return defaultTextRedactPatterns
|
||||
}
|
||||
|
||||
cacheKey := strings.Join(normalizedExtraKeys, ",")
|
||||
if cached, ok := extraTextPatternCache.Load(cacheKey); ok {
|
||||
if patterns, ok := cached.(*textRedactPatterns); ok {
|
||||
return patterns
|
||||
}
|
||||
}
|
||||
|
||||
compiled := compileTextRedactPatterns(normalizedExtraKeys)
|
||||
actual, _ := extraTextPatternCache.LoadOrStore(cacheKey, compiled)
|
||||
if patterns, ok := actual.(*textRedactPatterns); ok {
|
||||
return patterns
|
||||
}
|
||||
return compiled
|
||||
}
|
||||
|
||||
func normalizeAndSortExtraKeys(extraKeys []string) []string {
|
||||
if len(extraKeys) == 0 {
|
||||
return nil
|
||||
}
|
||||
seen := make(map[string]struct{}, len(extraKeys))
|
||||
keys := make([]string, 0, len(extraKeys))
|
||||
for _, key := range extraKeys {
|
||||
normalized := normalizeKey(key)
|
||||
if normalized == "" {
|
||||
continue
|
||||
}
|
||||
if _, ok := seen[normalized]; ok {
|
||||
continue
|
||||
}
|
||||
seen[normalized] = struct{}{}
|
||||
keys = append(keys, normalized)
|
||||
}
|
||||
sort.Strings(keys)
|
||||
return keys
|
||||
}
|
||||
|
||||
func buildKeyAlternation(extraKeys []string) string {
|
||||
seen := make(map[string]struct{}, len(defaultSensitiveKeyList)+len(extraKeys))
|
||||
keys := make([]string, 0, len(defaultSensitiveKeyList)+len(extraKeys))
|
||||
|
||||
@@ -37,3 +37,48 @@ func TestRedactText_GOCSPX(t *testing.T) {
|
||||
t.Fatalf("expected key redacted, got %q", out)
|
||||
}
|
||||
}
|
||||
|
||||
func TestRedactText_ExtraKeyCacheUsesNormalizedSortedKey(t *testing.T) {
|
||||
clearExtraTextPatternCache()
|
||||
|
||||
out1 := RedactText("custom_secret=abc", "Custom_Secret", " custom_secret ")
|
||||
out2 := RedactText("custom_secret=xyz", "custom_secret")
|
||||
if !strings.Contains(out1, "custom_secret=***") {
|
||||
t.Fatalf("expected custom key redacted in first call, got %q", out1)
|
||||
}
|
||||
if !strings.Contains(out2, "custom_secret=***") {
|
||||
t.Fatalf("expected custom key redacted in second call, got %q", out2)
|
||||
}
|
||||
|
||||
if got := countExtraTextPatternCacheEntries(); got != 1 {
|
||||
t.Fatalf("expected 1 cached pattern set, got %d", got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestRedactText_DefaultPathDoesNotUseExtraCache(t *testing.T) {
|
||||
clearExtraTextPatternCache()
|
||||
|
||||
out := RedactText("access_token=abc")
|
||||
if !strings.Contains(out, "access_token=***") {
|
||||
t.Fatalf("expected default key redacted, got %q", out)
|
||||
}
|
||||
if got := countExtraTextPatternCacheEntries(); got != 0 {
|
||||
t.Fatalf("expected extra cache to remain empty, got %d", got)
|
||||
}
|
||||
}
|
||||
|
||||
func clearExtraTextPatternCache() {
|
||||
extraTextPatternCache.Range(func(key, value any) bool {
|
||||
extraTextPatternCache.Delete(key)
|
||||
return true
|
||||
})
|
||||
}
|
||||
|
||||
func countExtraTextPatternCacheEntries() int {
|
||||
count := 0
|
||||
extraTextPatternCache.Range(func(key, value any) bool {
|
||||
count++
|
||||
return true
|
||||
})
|
||||
return count
|
||||
}
|
||||
|
||||
@@ -41,7 +41,14 @@ var hopByHopHeaders = map[string]struct{}{
|
||||
"connection": {},
|
||||
}
|
||||
|
||||
func FilterHeaders(src http.Header, cfg config.ResponseHeaderConfig) http.Header {
|
||||
type CompiledHeaderFilter struct {
|
||||
allowed map[string]struct{}
|
||||
forceRemove map[string]struct{}
|
||||
}
|
||||
|
||||
var defaultCompiledHeaderFilter = CompileHeaderFilter(config.ResponseHeaderConfig{})
|
||||
|
||||
func CompileHeaderFilter(cfg config.ResponseHeaderConfig) *CompiledHeaderFilter {
|
||||
allowed := make(map[string]struct{}, len(defaultAllowed)+len(cfg.AdditionalAllowed))
|
||||
for key := range defaultAllowed {
|
||||
allowed[key] = struct{}{}
|
||||
@@ -69,13 +76,24 @@ func FilterHeaders(src http.Header, cfg config.ResponseHeaderConfig) http.Header
|
||||
}
|
||||
}
|
||||
|
||||
return &CompiledHeaderFilter{
|
||||
allowed: allowed,
|
||||
forceRemove: forceRemove,
|
||||
}
|
||||
}
|
||||
|
||||
func FilterHeaders(src http.Header, filter *CompiledHeaderFilter) http.Header {
|
||||
if filter == nil {
|
||||
filter = defaultCompiledHeaderFilter
|
||||
}
|
||||
|
||||
filtered := make(http.Header, len(src))
|
||||
for key, values := range src {
|
||||
lower := strings.ToLower(key)
|
||||
if _, blocked := forceRemove[lower]; blocked {
|
||||
if _, blocked := filter.forceRemove[lower]; blocked {
|
||||
continue
|
||||
}
|
||||
if _, ok := allowed[lower]; !ok {
|
||||
if _, ok := filter.allowed[lower]; !ok {
|
||||
continue
|
||||
}
|
||||
// 跳过 hop-by-hop 头部,这些由 HTTP 库自动处理
|
||||
@@ -89,8 +107,8 @@ func FilterHeaders(src http.Header, cfg config.ResponseHeaderConfig) http.Header
|
||||
return filtered
|
||||
}
|
||||
|
||||
func WriteFilteredHeaders(dst http.Header, src http.Header, cfg config.ResponseHeaderConfig) {
|
||||
filtered := FilterHeaders(src, cfg)
|
||||
func WriteFilteredHeaders(dst http.Header, src http.Header, filter *CompiledHeaderFilter) {
|
||||
filtered := FilterHeaders(src, filter)
|
||||
for key, values := range filtered {
|
||||
for _, value := range values {
|
||||
dst.Add(key, value)
|
||||
|
||||
@@ -20,7 +20,7 @@ func TestFilterHeadersDisabledUsesDefaultAllowlist(t *testing.T) {
|
||||
ForceRemove: []string{"x-request-id"},
|
||||
}
|
||||
|
||||
filtered := FilterHeaders(src, cfg)
|
||||
filtered := FilterHeaders(src, CompileHeaderFilter(cfg))
|
||||
if filtered.Get("Content-Type") != "application/json" {
|
||||
t.Fatalf("expected Content-Type passthrough, got %q", filtered.Get("Content-Type"))
|
||||
}
|
||||
@@ -51,7 +51,7 @@ func TestFilterHeadersEnabledUsesAllowlist(t *testing.T) {
|
||||
ForceRemove: []string{"x-remove"},
|
||||
}
|
||||
|
||||
filtered := FilterHeaders(src, cfg)
|
||||
filtered := FilterHeaders(src, CompileHeaderFilter(cfg))
|
||||
if filtered.Get("Content-Type") != "application/json" {
|
||||
t.Fatalf("expected Content-Type allowed, got %q", filtered.Get("Content-Type"))
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user