clone

a self-hosted multitenant nostr relay with sites, file storage, git hosting, and inbox and outbox delivery

feat/tinyagent
tinyrelay / internal / catalog / validate.go
validate.go 2.3K
1package catalog23import (4 "database/sql"5 "encoding/hex"6 "errors"7 "fmt"8 "net"9 "net/url"10 "regexp"11 "strings"12)1314var labelPattern = regexp.MustCompile(`^[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?$`)1516func normalizeName(name string) (string, error) {17 name = strings.TrimSpace(name)18 if name == "" || len(name) > 128 || strings.ContainsAny(name, "/\\") || hasControl(name) {19 return "", errors.New("tenant name must be non-empty, safe, and at most 128 characters")20 }21 return name, nil22}2324func normalizeOwner(owner string) (string, error) {25 if len(owner) != 64 || owner != strings.ToLower(owner) {26 return "", errors.New("owner must be a lowercase 32-byte hex public key")27 }28 decoded, err := hex.DecodeString(owner)29 if err != nil || len(decoded) != 32 {30 return "", errors.New("owner must be a lowercase 32-byte hex public key")31 }32 return owner, nil33}3435func normalizeTemplate(template string) (string, error) {36 template = strings.TrimSpace(template)37 if template == "" {38 return "default", nil39 }40 if len(template) > 64 || strings.ContainsAny(template, "/\\") || hasControl(template) {41 return "", errors.New("tenant template must be safe and at most 64 characters")42 }43 return template, nil44}4546func normalizeSource(source string) (string, error) {47 source = strings.TrimSpace(source)48 if source == "" {49 return "", nil50 }51 u, err := url.Parse(source)52 if err != nil || u.Host == "" || (u.Scheme != "http" && u.Scheme != "https" && u.Scheme != "ws" && u.Scheme != "wss") || u.User != nil {53 return "", errors.New("source must be an http, https, ws, or wss relay URL")54 }55 return source, nil56}5758func normalizeHost(host string) (string, error) {59 host = strings.TrimSuffix(strings.ToLower(strings.TrimSpace(host)), ".")60 if host == "" || len(host) > 253 || strings.ContainsAny(host, "/\\:@?#") || hasControl(host) {61 return "", errors.New("hostname must be a normalized DNS name")62 }63 if net.ParseIP(host) != nil {64 return host, nil65 }66 for _, label := range strings.Split(host, ".") {67 if !labelPattern.MatchString(label) {68 return "", fmt.Errorf("invalid hostname label %q", label)69 }70 }71 return host, nil72}7374func hasControl(value string) bool {75 for _, r := range value {76 if r < 0x20 || r == 0x7f {77 return true78 }79 }80 return false81}8283func isConstraint(err error) bool {84 return strings.Contains(strings.ToLower(err.Error()), "constraint") || errors.Is(err, sql.ErrNoRows)85}