Files
-Fonu/internal/validate/validate.go
T
mrlee 278f8267b7 chore: 初始提交 Fonu 1.0.2
- 公网反代:Nginx 状态卡片(版本 / 运行状态 / 配置校验 / 端口监听)
- 部署设置第一、二梯队(HTTPS 开关、安全头、限流、连接数、仅中国大陆 IP)
- 隧道检测兼容 frps v0.68 / v0.71(V2 与旧版端点)
- 恢复被清空的 internal/frp/store.go、internal/api/ddns_handler.go、web/src/views/DdnsView.vue
2026-09-23 09:40:59 +08:00

139 lines
3.5 KiB
Go

package validate
import (
"fmt"
"net"
"net/url"
"regexp"
"strconv"
"strings"
)
var domainRe = regexp.MustCompile(`^(?i:[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?)(?:\.(?i:[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?))*$`)
func CertDomain(domain string) error {
domain = strings.TrimSuffix(strings.ToLower(strings.TrimSpace(domain)), ".")
if domain == "" {
return fmt.Errorf("域名不能为空")
}
if strings.HasPrefix(domain, "*.") {
return Domain(strings.TrimPrefix(domain, "*."))
}
return Domain(domain)
}
func Domain(domain string) error {
domain = strings.TrimSpace(domain)
if domain == "" {
return fmt.Errorf("域名不能为空")
}
if len(domain) > 253 {
return fmt.Errorf("域名过长")
}
if strings.Contains(domain, "..") || strings.HasPrefix(domain, ".") || strings.HasSuffix(domain, ".") {
return fmt.Errorf("域名格式无效")
}
if !domainRe.MatchString(domain) {
return fmt.Errorf("域名格式无效")
}
return nil
}
func ListenPort(port int) error {
if port < 1 || port > 65535 {
return fmt.Errorf("监听端口无效")
}
return nil
}
// FrontendAddress parses a frontend binding like "example.com" or "example.com:8011".
// A zero port means the rule-level default should be used.
func FrontendAddress(raw string) (hostname string, port int, err error) {
raw = strings.TrimSpace(raw)
if raw == "" {
return "", 0, fmt.Errorf("前端地址不能为空")
}
host := raw
port = 0
if strings.Count(raw, ":") == 1 && !strings.Contains(raw, "]") {
parts := strings.SplitN(raw, ":", 2)
host = parts[0]
p, parseErr := strconv.Atoi(parts[1])
if parseErr != nil || p < 1 || p > 65535 {
return "", 0, fmt.Errorf("前端地址端口无效")
}
port = p
}
host = strings.ToLower(strings.TrimSpace(host))
if err := Domain(host); err != nil {
return "", 0, err
}
return host, port, nil
}
func Upstream(raw string) (string, error) {
raw = strings.TrimSpace(raw)
if raw == "" {
return "", fmt.Errorf("目标地址不能为空")
}
u, err := url.Parse(raw)
if err != nil {
return "", fmt.Errorf("目标地址格式无效")
}
if u.Scheme != "http" && u.Scheme != "https" {
return "", fmt.Errorf("目标地址仅支持 http:// 或 https://")
}
if u.User != nil {
return "", fmt.Errorf("目标地址不能包含用户名或密码")
}
if u.Host == "" {
return "", fmt.Errorf("目标地址缺少主机名")
}
if u.Path != "" && u.Path != "/" {
return "", fmt.Errorf("目标地址不能包含路径")
}
if u.RawQuery != "" || u.Fragment != "" {
return "", fmt.Errorf("目标地址不能包含查询参数或片段")
}
host := u.Hostname()
port := u.Port()
if host == "" {
return "", fmt.Errorf("目标地址主机名无效")
}
if ip := net.ParseIP(host); ip != nil {
if port == "" {
return "", fmt.Errorf("IP 地址目标必须指定端口")
}
} else if err := Domain(host); err != nil {
return "", fmt.Errorf("目标地址主机名无效")
}
normalized := fmt.Sprintf("%s://%s", u.Scheme, u.Host)
if !strings.HasPrefix(normalized, "http://") && !strings.HasPrefix(normalized, "https://") {
return "", fmt.Errorf("目标地址格式无效")
}
return normalized, nil
}
func Password(password string) error {
if len(password) < 8 {
return fmt.Errorf("密码长度至少 8 位")
}
return nil
}
func Username(username string) error {
username = strings.TrimSpace(username)
if username == "" {
return fmt.Errorf("用户名不能为空")
}
if len(username) > 64 {
return fmt.Errorf("用户名过长")
}
return nil
}