package site import ( "net/url" "strings" "unicode/utf8" ) const ( MaxPublicURLPatternRunes = 512 LangPlaceholder = "{lang}" SlugPlaceholder = "{slug}" placeholderFiller = "x" allowedURLSymbols = "-._~:/?#[]@!$&'()*+,;=%" ) func NormalizePublicURLPattern(pattern *string) (*string, error) { if pattern == nil { return nil, nil } trimmed := strings.TrimSpace(*pattern) if trimmed == "" { return nil, nil } if utf8.RuneCountInString(trimmed) > MaxPublicURLPatternRunes { return nil, ErrInvalidPublicURLPattern } if err := checkPublicURLScheme(trimmed); err != nil { return nil, err } if err := checkPlaceholders(trimmed); err != nil { return nil, err } filled := fillPlaceholders(trimmed) if err := checkEncoding(filled); err != nil { return nil, err } if !strings.Contains(trimmed, SlugPlaceholder) { return nil, ErrPublicURLMissingSlug } if err := checkPublicURLHost(filled); err != nil { return nil, err } return &trimmed, nil } func checkPublicURLScheme(pattern string) error { lowered := strings.ToLower(pattern) if !strings.HasPrefix(lowered, "https://") && !strings.HasPrefix(lowered, "http://") { return ErrPublicURLScheme } return nil } func checkPlaceholders(pattern string) error { remaining := pattern for { open := strings.IndexAny(remaining, "{}") if open < 0 { return nil } if remaining[open] == '}' { return ErrPublicURLPlaceholder } rest := remaining[open:] switch { case strings.HasPrefix(rest, LangPlaceholder): remaining = rest[len(LangPlaceholder):] case strings.HasPrefix(rest, SlugPlaceholder): remaining = rest[len(SlugPlaceholder):] default: return ErrPublicURLPlaceholder } } } func fillPlaceholders(pattern string) string { return strings.NewReplacer(LangPlaceholder, placeholderFiller, SlugPlaceholder, placeholderFiller).Replace(pattern) } func checkEncoding(filled string) error { for index := 0; index < len(filled); index++ { character := filled[index] if character == '%' { if index+2 >= len(filled) || !isHex(filled[index+1]) || !isHex(filled[index+2]) { return ErrPublicURLNotEncoded } index += 2 continue } if !isAllowedURLByte(character) { return ErrPublicURLNotEncoded } } return nil } func isHex(character byte) bool { return character >= '0' && character <= '9' || character >= 'a' && character <= 'f' || character >= 'A' && character <= 'F' } func isAllowedURLByte(character byte) bool { if character >= '0' && character <= '9' || character >= 'a' && character <= 'z' || character >= 'A' && character <= 'Z' { return true } return strings.IndexByte(allowedURLSymbols, character) >= 0 } func checkPublicURLHost(filled string) error { parsed, err := url.Parse(filled) if err != nil || parsed.Hostname() == "" { return ErrPublicURLHost } return nil }