Files
2026-07-13 18:01:39 -03:00

111 lines
3.3 KiB
Go

package main
import (
"encoding/base32"
"fmt"
"regexp"
"strings"
"time"
)
var accountUsernamePattern = regexp.MustCompile(`^[A-Za-z0-9][A-Za-z0-9._@-]{0,63}$`)
func validateAccountUsername(username string) error {
if !accountUsernamePattern.MatchString(username) {
return fmt.Errorf("username must be 1-64 characters using letters, numbers, dot, underscore, @, or hyphen")
}
return nil
}
func hasAccountControlCharacters(value string) bool {
return strings.IndexFunc(value, func(r rune) bool { return r < 0x20 || r == 0x7f }) >= 0
}
func validateOptionalAccountExpiry(raw string) error {
raw = strings.TrimSpace(raw)
if raw == "" {
return nil
}
for _, layout := range []string{time.RFC3339, "2006-01-02T15:04", "2006-01-02"} {
if _, err := time.Parse(layout, raw); err == nil {
return nil
}
}
return fmt.Errorf("invalid expiration date")
}
func validateSSHUserPayload(p *UserPayload) error {
p.Username = strings.TrimSpace(p.Username)
p.OwnerUsername = strings.TrimSpace(p.OwnerUsername)
p.ServerID = strings.TrimSpace(p.ServerID)
p.TOTPSecret = normalizeBase32Secret(p.TOTPSecret)
if err := validateAccountUsername(p.Username); err != nil {
return err
}
if p.Password != nil && len(*p.Password) > 4096 {
return fmt.Errorf("password is too long")
}
if p.MaxConnections < 0 || p.MaxConnections > 1000 {
return fmt.Errorf("max_connections must be between 0 and 1000")
}
if p.LimitUpMbps < 0 || p.LimitUpMbps > 100000 || p.LimitDownMbps < 0 || p.LimitDownMbps > 100000 {
return fmt.Errorf("speed limits must be between 0 and 100000 Mbps")
}
if err := validateOptionalAccountExpiry(p.ExpiresAt); err != nil {
return err
}
if p.OwnerUsername != "" {
if err := validateAdminUsername(p.OwnerUsername); err != nil {
return fmt.Errorf("invalid owner username")
}
}
if len(p.ServerID) > 32 || hasAccountControlCharacters(p.ServerID) {
return fmt.Errorf("invalid server id")
}
if p.TOTPSecret != "" {
if len(p.TOTPSecret) > 256 {
return fmt.Errorf("TOTP secret is too long")
}
if _, err := base32.StdEncoding.WithPadding(base32.NoPadding).DecodeString(p.TOTPSecret); err != nil {
return fmt.Errorf("invalid TOTP secret")
}
}
if p.TOTPPeriod != 0 && (p.TOTPPeriod < 15 || p.TOTPPeriod > 300) {
return fmt.Errorf("TOTP period must be between 15 and 300 seconds")
}
if p.TOTPWindow < 0 || p.TOTPWindow > 10 {
return fmt.Errorf("TOTP window must be between 0 and 10")
}
if p.TOTPDigits != 0 && (p.TOTPDigits < 6 || p.TOTPDigits > 8) {
return fmt.Errorf("TOTP digits must be between 6 and 8")
}
return nil
}
func validateXrayClientFields(uuid, inboundTag, email, name, expiresAt string, maxConnections int, requireInbound bool) error {
uuid = strings.TrimSpace(uuid)
if _, err := parseUUID(uuid); err != nil {
return fmt.Errorf("invalid UUID")
}
if requireInbound && strings.TrimSpace(inboundTag) == "" {
return fmt.Errorf("inbound_tag required")
}
for field, value := range map[string]string{
"inbound_tag": inboundTag,
"email": email,
"name": name,
} {
limit := 256
if field == "inbound_tag" {
limit = 128
}
if len(value) > limit || hasAccountControlCharacters(value) {
return fmt.Errorf("invalid %s", field)
}
}
if maxConnections < 0 || maxConnections > 1000 {
return fmt.Errorf("max_connections must be between 0 and 1000")
}
return validateOptionalAccountExpiry(expiresAt)
}