Files
yunzerwebsiteallinone/go/services/domain_verify.go
T

160 lines
4.6 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
package services
import (
"fmt"
"net"
"regexp"
"strings"
beego "github.com/beego/beego/v2/server/web"
)
// hostnameRe 合法主机名:多段标签,每段字母数字或连字符,末段为字母(顶级域)
var hostnameRe = regexp.MustCompile(`^([a-z0-9]([a-z0-9-]{0,61}[a-z0-9])?\.)+[a-z]{2,}$`)
// NormalizeHost 归一化用户输入的域名:去协议、去路径、去端口、去尾点、转小写。
// 返回空字符串表示输入不是合法主机名。
func NormalizeHost(raw string) string {
h := strings.TrimSpace(strings.ToLower(raw))
if h == "" {
return ""
}
// 去掉协议前缀
if idx := strings.Index(h, "://"); idx >= 0 {
h = h[idx+3:]
}
// 去掉路径与查询
if idx := strings.IndexAny(h, "/?#"); idx >= 0 {
h = h[:idx]
}
// 去掉可能的 user@ 前缀
if idx := strings.LastIndex(h, "@"); idx >= 0 {
h = h[idx+1:]
}
// 去掉端口
if idx := strings.Index(h, ":"); idx >= 0 {
h = h[:idx]
}
// 去掉 FQDN 末尾的点
h = strings.TrimSuffix(h, ".")
// 去掉通配符写法
h = strings.TrimPrefix(h, "*.")
if len(h) > 253 || !hostnameRe.MatchString(h) {
return ""
}
return h
}
// CustomDomainGuide 自有域名的解析配置指引,供前端展示。
type CustomDomainGuide struct {
CnameTarget string `json:"cname_target"`
AIPs []string `json:"a_ips"`
}
// GetCustomDomainGuide 读取配置中的 CNAME 目标与 A 记录 IP。
func GetCustomDomainGuide() CustomDomainGuide {
target, _ := beego.AppConfig.String("customdomain_cname_target")
target = NormalizeHost(target)
ipsRaw, _ := beego.AppConfig.String("customdomain_a_ips")
ips := make([]string, 0, 2)
for _, item := range strings.Split(ipsRaw, ",") {
item = strings.TrimSpace(item)
if item == "" {
continue
}
if net.ParseIP(item) == nil {
continue
}
ips = append(ips, item)
}
return CustomDomainGuide{CnameTarget: target, AIPs: ips}
}
// VerifyCustomDomainDNS 检测域名是否已解析到本平台。
// 命中任一条件即通过:
// 1. CNAME 链最终指向配置的 cname_target(或其本身);
// 2. A/AAAA 记录中出现配置的任一 IP;
// 3. cname_target 自身解析出的 IP 与域名解析出的 IP 有交集(兼容 CNAME 被 DNS 服务商拍平的情况)。
//
// 返回 (是否通过, 说明文案)。说明文案会写入 verify_msg 展示给租户。
func VerifyCustomDomainDNS(host string) (bool, string) {
host = NormalizeHost(host)
if host == "" {
return false, "域名格式不正确"
}
guide := GetCustomDomainGuide()
if guide.CnameTarget == "" && len(guide.AIPs) == 0 {
return false, "平台尚未配置解析目标(customdomain_cname_target / customdomain_a_ips),请联系平台管理员"
}
// 1. CNAME 校验
if guide.CnameTarget != "" {
if cname, err := net.LookupCNAME(host); err == nil {
actual := NormalizeHost(cname)
if actual != "" && actual != host && actual == guide.CnameTarget {
return true, "CNAME 已指向 " + guide.CnameTarget
}
}
}
// 2/3. 解析出的 IP 与允许列表或 cname_target 的 IP 比对
addrs, err := net.LookupHost(host)
if err != nil {
return false, "域名无法解析,请确认解析记录已添加并等待生效:" + trimDNSError(err)
}
if len(addrs) == 0 {
return false, "域名未解析到任何 IP,请确认解析记录已添加并等待生效"
}
got := make(map[string]bool, len(addrs))
for _, a := range addrs {
got[a] = true
}
for _, want := range guide.AIPs {
if got[want] {
return true, "解析已指向 " + want
}
}
if guide.CnameTarget != "" {
if targetAddrs, err := net.LookupHost(guide.CnameTarget); err == nil {
for _, want := range targetAddrs {
if got[want] {
return true, "解析已指向平台服务器(" + want + ")"
}
}
}
}
expect := describeExpectedTarget(guide)
return false, fmt.Sprintf("当前解析到 %s,与平台地址不一致。%s", strings.Join(addrs, ", "), expect)
}
// describeExpectedTarget 生成「应该怎么配」的提示文案
func describeExpectedTarget(guide CustomDomainGuide) string {
parts := make([]string, 0, 2)
if guide.CnameTarget != "" {
parts = append(parts, "子域名请添加 CNAME 记录指向 "+guide.CnameTarget)
}
if len(guide.AIPs) > 0 {
parts = append(parts, "根域名请添加 A 记录指向 "+strings.Join(guide.AIPs, " 或 "))
}
if len(parts) == 0 {
return ""
}
return strings.Join(parts, ";") + "。"
}
// trimDNSError 精简 DNS 错误信息,避免把内部解析器地址写进给租户看的文案
func trimDNSError(err error) string {
msg := err.Error()
if idx := strings.LastIndex(msg, ": "); idx >= 0 && idx+2 < len(msg) {
msg = msg[idx+2:]
}
return msg
}