mirror of
https://github.com/h44z/wg-portal.git
synced 2026-05-28 08:56:17 +00:00
152 lines
3.7 KiB
Go
152 lines
3.7 KiB
Go
|
|
package domain
|
||
|
|
|
||
|
|
import (
|
||
|
|
"log/slog"
|
||
|
|
"net/mail"
|
||
|
|
"strings"
|
||
|
|
"unicode"
|
||
|
|
"unicode/utf8"
|
||
|
|
|
||
|
|
"golang.org/x/text/unicode/norm"
|
||
|
|
)
|
||
|
|
|
||
|
|
// LogSanitizeChange applies sanitizeFn to raw, logs when the value changes, and writes
|
||
|
|
// the sanitized value to dest. Raw and sanitized values are intentionally omitted.
|
||
|
|
func LogSanitizeChange(
|
||
|
|
providerType string,
|
||
|
|
providerName string,
|
||
|
|
field string,
|
||
|
|
raw string,
|
||
|
|
sanitizeFn func() string,
|
||
|
|
dest *string,
|
||
|
|
) {
|
||
|
|
sanitized := sanitizeFn()
|
||
|
|
if sanitized != raw {
|
||
|
|
message := "sanitization modified field value from external provider"
|
||
|
|
if sanitized == "" {
|
||
|
|
message = "sanitization cleared field value from external provider"
|
||
|
|
}
|
||
|
|
slog.Warn(message,
|
||
|
|
"provider_type", SanitizeString(providerType, 64),
|
||
|
|
"provider", SanitizeString(providerName, 128),
|
||
|
|
"field", SanitizeString(field, 64),
|
||
|
|
)
|
||
|
|
}
|
||
|
|
*dest = sanitized
|
||
|
|
}
|
||
|
|
|
||
|
|
var reservedUserIdentifiers = map[string]struct{}{
|
||
|
|
"all": {},
|
||
|
|
"new": {},
|
||
|
|
"id": {},
|
||
|
|
CtxSystemAdminId: {},
|
||
|
|
CtxUnknownUserId: {},
|
||
|
|
CtxSystemLdapSyncer: {},
|
||
|
|
CtxSystemWgImporter: {},
|
||
|
|
CtxSystemV1Migrator: {},
|
||
|
|
CtxSystemDBMigrator: {},
|
||
|
|
}
|
||
|
|
|
||
|
|
// SanitizeString normalizes to NFC, trims leading and trailing whitespace, strips Unicode
|
||
|
|
// control and format characters, drops invalid UTF-8 bytes, and truncates the result to
|
||
|
|
// maxLen runes. If maxLen <= 0, returns "".
|
||
|
|
func SanitizeString(s string, maxLen int) string {
|
||
|
|
if maxLen <= 0 {
|
||
|
|
return ""
|
||
|
|
}
|
||
|
|
|
||
|
|
s = norm.NFC.String(strings.TrimSpace(s))
|
||
|
|
|
||
|
|
var b strings.Builder
|
||
|
|
b.Grow(len(s))
|
||
|
|
for len(s) > 0 {
|
||
|
|
r, size := utf8.DecodeRuneInString(s)
|
||
|
|
s = s[size:]
|
||
|
|
if r == utf8.RuneError && size == 1 {
|
||
|
|
continue
|
||
|
|
}
|
||
|
|
if !unicode.IsControl(r) && !unicode.Is(unicode.Cf, r) {
|
||
|
|
b.WriteRune(r)
|
||
|
|
}
|
||
|
|
}
|
||
|
|
s = b.String()
|
||
|
|
|
||
|
|
if utf8.RuneCountInString(s) > maxLen {
|
||
|
|
runes := []rune(s)
|
||
|
|
s = string(runes[:maxLen])
|
||
|
|
}
|
||
|
|
|
||
|
|
return strings.TrimSpace(s)
|
||
|
|
}
|
||
|
|
|
||
|
|
// SanitizeEmail applies SanitizeString first, then returns "" if the original s
|
||
|
|
// contains CR/LF or if the sanitized result is not a plain email address.
|
||
|
|
func SanitizeEmail(s string, maxLen int) string {
|
||
|
|
if strings.ContainsRune(s, '\r') || strings.ContainsRune(s, '\n') {
|
||
|
|
return ""
|
||
|
|
}
|
||
|
|
|
||
|
|
sanitized := SanitizeString(s, maxLen)
|
||
|
|
|
||
|
|
if sanitized == "" || strings.Count(sanitized, "@") != 1 {
|
||
|
|
return ""
|
||
|
|
}
|
||
|
|
addr, err := mail.ParseAddress(sanitized)
|
||
|
|
if err != nil || addr.Name != "" || addr.Address != sanitized {
|
||
|
|
return ""
|
||
|
|
}
|
||
|
|
|
||
|
|
return sanitized
|
||
|
|
}
|
||
|
|
|
||
|
|
// SanitizePhone applies SanitizeString first, then removes all characters not in the
|
||
|
|
// set [0-9+\-() .]. Returns "" if the result after filtering is empty.
|
||
|
|
func SanitizePhone(s string, maxLen int) string {
|
||
|
|
sanitized := SanitizeString(s, maxLen)
|
||
|
|
|
||
|
|
// Remove all characters not in [0-9+\-() .]
|
||
|
|
var b strings.Builder
|
||
|
|
b.Grow(len(sanitized))
|
||
|
|
for _, r := range sanitized {
|
||
|
|
if isAllowedPhoneRune(r) {
|
||
|
|
b.WriteRune(r)
|
||
|
|
}
|
||
|
|
}
|
||
|
|
result := strings.TrimSpace(b.String())
|
||
|
|
|
||
|
|
if result == "" {
|
||
|
|
return ""
|
||
|
|
}
|
||
|
|
|
||
|
|
return result
|
||
|
|
}
|
||
|
|
|
||
|
|
// isAllowedPhoneRune reports whether r is in the allowed phone character set [0-9+\-() .].
|
||
|
|
func isAllowedPhoneRune(r rune) bool {
|
||
|
|
switch {
|
||
|
|
case r >= '0' && r <= '9':
|
||
|
|
return true
|
||
|
|
case r == '+', r == '-', r == '(', r == ')', r == ' ', r == '.':
|
||
|
|
return true
|
||
|
|
default:
|
||
|
|
return false
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
// SanitizeIdentifier applies SanitizeString first, then returns "" if the result equals
|
||
|
|
// a reserved user identifier (case-sensitive, exact match).
|
||
|
|
func SanitizeIdentifier(s string, maxLen int) string {
|
||
|
|
sanitized := SanitizeString(s, maxLen)
|
||
|
|
|
||
|
|
if IsReservedUserIdentifier(UserIdentifier(sanitized)) {
|
||
|
|
return ""
|
||
|
|
}
|
||
|
|
|
||
|
|
return sanitized
|
||
|
|
}
|
||
|
|
|
||
|
|
func IsReservedUserIdentifier(identifier UserIdentifier) bool {
|
||
|
|
_, reserved := reservedUserIdentifiers[string(identifier)]
|
||
|
|
return reserved
|
||
|
|
}
|