package service import ( "regexp" "strings" ) // Санитайз свободного текста логов. // // Отличается от YAML-санитайзера тем, что у него нет структуры: журнал — это // строки, а не документ. Поэтому правил ровно два и оба консервативные: // // 1. всё, что разбирается как URL, прогоняется через тот же sanitizeURLValue, // что и экспорт конфига: host/port/path остаются читаемыми, встроенные // учётные данные и секретные query-параметры вырезаются; // 2. отдельно ловятся пары `имя=значение` с секретоподобным именем, которые // встречаются в тексте вне URL. // // Зачем это журналу Hysteria: HY2_AUTH_URL содержит access_token, и upstream // волен упомянуть его в сообщении об ошибке обращения к auth-backend. Свой // журнал админки мы контролируем, чужой — нет, а ExportLog отдаёт его // оператору тем же файлом. var logURLPattern = regexp.MustCompile(`[a-zA-Z][a-zA-Z0-9+.\-]*://[^\s"'<>` + "`" + `]+`) // Имена подобраны по тем же маркерам, что и secretKeyMarkers, но записаны // явным перечислением: в свободном тексте подстрочный поиск по «token» дал бы // слишком много ложных срабатываний на словах вроде «tokenizer». var logSecretAssignmentPattern = regexp.MustCompile( `(?i)([a-z0-9_\-]*(?:access_token|token|secret|password|passwd|passphrase|credential|api_?key|authorization|signature)[a-z0-9_\-]*)\s*[=:]\s*([^\s&,;"'<>` + "`" + `]+)`, ) // SanitizeLogText вырезает секреты из произвольного текста журнала. func SanitizeLogText(raw string) string { if raw == "" { return raw } withSafeURLs := logURLPattern.ReplaceAllStringFunc(raw, func(match string) string { body, trailing := splitTrailingPunctuation(match) return sanitizeURLValue(body) + trailing }) return logSecretAssignmentPattern.ReplaceAllStringFunc(withSafeURLs, func(match string) string { groups := logSecretAssignmentPattern.FindStringSubmatch(match) if len(groups) != 3 { return match } value, trailing := splitTrailingPunctuation(groups[2]) // Значение уже вырезано URL-санитайзером: percent-кодированный маркер // внутри готового URL трогать нельзя, иначе адрес перестанет быть // валидным URL. if value == RedactedPlaceholder || strings.EqualFold(value, encodedRedactedPlaceholder) { return match } separator := "=" if strings.Contains(match, ":") && !strings.Contains(match, "=") { separator = ": " } return groups[1] + separator + RedactedPlaceholder + trailing }) } // encodedRedactedPlaceholder — то, во что превращается маркер внутри query // после url.Values.Encode(). const encodedRedactedPlaceholder = "%3Credacted%3E" // splitTrailingPunctuation отделяет хвостовую пунктуацию предложения от // значения: в «...access_token=abc.» точка принадлежит тексту, а не токену. func splitTrailingPunctuation(value string) (string, string) { end := len(value) for end > 0 && strings.ContainsRune(".,;:)]}!?", rune(value[end-1])) { end-- } return value[:end], value[end:] }