package webui import ( "context" "crypto/tls" "io" "net" "net/http" "strings" "time" "gopkg.in/ini.v1" ) // getPublicIP mirrors server_web_ui/utils.py's get_public_ip: try ifconfig.me, then // httpbin.org, then the configured SPF_SERVER_IP fallback, then 127.0.0.1. func getPublicIP(cfg *ini.File) string { client := &http.Client{ Timeout: 3 * time.Second, Transport: &http.Transport{TLSClientConfig: &tls.Config{InsecureSkipVerify: true}}, } if ip := fetchBody(client, "http://ifconfig.me/ip"); ip != "" { return strings.TrimSpace(ip) } if ip := fetchBody(client, "http://httpbin.org/ip"); ip != "" { return strings.TrimSpace(ip) } fallback := cfg.Section("DKIM").Key("SPF_SERVER_IP").MustString("") if net.ParseIP(fallback) != nil { return fallback } return "127.0.0.1" } func fetchBody(client *http.Client, url string) string { resp, err := client.Get(url) if err != nil { return "" } defer resp.Body.Close() b, err := io.ReadAll(io.LimitReader(resp.Body, 4096)) if err != nil { return "" } return string(b) } // resolverAt builds a resolver pinned to a specific DNS server, mirroring // utils.check_dns_record's hardcoded Cloudflare resolver (1.1.1.1), 5s timeout. func resolverAt(serverIP string) *net.Resolver { return &net.Resolver{ PreferGo: true, Dial: func(ctx context.Context, network, address string) (net.Conn, error) { d := net.Dialer{Timeout: 5 * time.Second} return d.DialContext(ctx, network, net.JoinHostPort(serverIP, "53")) }, } } var pinnedResolver = resolverAt("1.1.1.1") type dnsCheckResult struct { Success bool Message string Records []string } // checkDNSRecord mirrors utils.check_dns_record for TXT lookups. func checkDNSRecord(domain string) dnsCheckResult { ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) defer cancel() recs, err := pinnedResolver.LookupTXT(ctx, domain) if err != nil { return dnsCheckResult{Success: false, Message: err.Error()} } if len(recs) == 0 { return dnsCheckResult{Success: false, Message: "No TXT records found"} } return dnsCheckResult{Success: true, Records: recs} } // verificationRecordName is the DNS TXT record name a domain's ownership proof lives // at, e.g. "_pymta-verify.example.com". func verificationRecordName(domain string) string { return "_pymta-verify." + domain } func verificationRecordValue(token string) string { return "pymta-verify=" + token } // checkDomainOwnership looks up the verification TXT record via two independent public // resolvers (1.1.1.1 and 8.8.8.8, per the user's requirement) and considers the domain // verified if the expected token shows up via either — a domain that's genuinely been // updated can otherwise show as unverified for a while against whichever resolver has // a stale cache, so requiring both to agree at the same instant would be a flaky check. func checkDomainOwnership(domain, token string) (verified bool, checkedRecords []string, err error) { recordName := verificationRecordName(domain) expected := verificationRecordValue(token) var lastErr error for _, resolverIP := range []string{"1.1.1.1", "8.8.8.8"} { ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) recs, lookupErr := resolverAt(resolverIP).LookupTXT(ctx, recordName) cancel() if lookupErr != nil { lastErr = lookupErr continue } checkedRecords = append(checkedRecords, recs...) for _, r := range recs { if strings.TrimSpace(r) == expected { return true, checkedRecords, nil } } } if len(checkedRecords) == 0 && lastErr != nil { return false, nil, lastErr } return false, checkedRecords, nil } // generateSPFRecord mirrors utils.generate_spf_record. func generateSPFRecord(serverIP, existingSPF string) string { if existingSPF == "" { return "v=spf1 ip4:" + serverIP + " ~all" } if strings.Contains(existingSPF, "ip4:"+serverIP) { return existingSPF } for _, all := range []string{"-all", "~all", "all"} { if idx := strings.LastIndex(existingSPF, all); idx >= 0 { return strings.TrimSpace(existingSPF[:idx]) + " ip4:" + serverIP + " " + all } } return existingSPF + " ip4:" + serverIP + " ~all" }