118 lines
2.8 KiB
Go
118 lines
2.8 KiB
Go
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
|
|
}
|