package whois import ( "strings" ) func commonParser(domain, data string) (Result, error) { p := newCommonParsePipeline(domain, data) p.run() return p.result, nil } type commonParsePipeline struct { queryDomain string result Result register PersonalInfo admin PersonalInfo tech PersonalInfo statusSet map[string]struct{} pendingKey string terminated bool } func newCommonParsePipeline(domain, data string) *commonParsePipeline { return &commonParsePipeline{ queryDomain: strings.ToLower(strings.Trim(strings.TrimSpace(domain), ".")), result: Result{ domain: strings.TrimSpace(domain), rawData: data, }, statusSet: make(map[string]struct{}), } } func (p *commonParsePipeline) run() { for _, rawLine := range splitParseLines(p.result.rawData) { if p.terminated { break } p.consumeLine(rawLine) } p.finalize() } func splitParseLines(raw string) []string { raw = strings.ReplaceAll(raw, "\r\n", "\n") raw = strings.ReplaceAll(raw, "\r", "\n") return strings.Split(raw, "\n") } func (p *commonParsePipeline) consumeLine(rawLine string) { line := strings.TrimSpace(rawLine) if line == "" { p.onBlankLine() return } if p.handleTerminalLine(line) { return } if p.handlePendingLine(line) { return } if p.handleKVLine(line) { return } p.handleInlineLine(line) } func (p *commonParsePipeline) onBlankLine() { if p.pendingKey == "status" || p.pendingKey == "domain" { p.pendingKey = "" } } func (p *commonParsePipeline) handleTerminalLine(line string) bool { if !isNotFoundLine(line) { return false } if p.result.exists && p.result.hasRegisterDate { return false } p.result.exists = false p.pendingKey = "" p.terminated = true return true } func (p *commonParsePipeline) handlePendingLine(line string) bool { if p.pendingKey == "" { return false } switch p.pendingKey { case "domain": if d := extractDomainToken(line); d != "" { p.result.exists = true p.result.domain = d p.pendingKey = "" return true } p.pendingKey = "" return false case "nameserver": if ns, ok := parseNameServerCandidate(line); ok { p.result.nsServers = appendUnique(p.result.nsServers, ns) return true } p.pendingKey = "" return false case "status": if s := firstToken(line); s != "" { p.statusSet[s] = struct{}{} return true } p.pendingKey = "" return false default: p.pendingKey = "" return false } } func (p *commonParsePipeline) handleKVLine(line string) bool { key, val, ok := splitKV(line) if !ok { return false } canonical := canonicalKey(key) if val == "" { switch canonical { case "domain", "nameserver", "status": p.pendingKey = canonical return true } } if applyCanonicalKV(&p.result, canonical, val, p.statusSet) { return true } applyContactAlias(key, val, &p.register, &p.admin, &p.tech) return true } func (p *commonParsePipeline) handleInlineLine(line string) { if d, ok := parseInlineDomain(line); ok { p.result.exists = true p.result.domain = d } } func (p *commonParsePipeline) finalize() { if p.result.exists && strings.TrimSpace(p.result.domain) == "" { if d := extractDomainFromRaw(p.result.rawData, p.queryDomain); d != "" { p.result.domain = d } } if p.result.exists && strings.TrimSpace(p.result.registrar) == "" { if v := extractDomainManagersName(p.result.rawData); v != "" { p.result.registrar = v } } for s := range p.statusSet { p.result.statusRaw = append(p.result.statusRaw, s) } p.result.registerInfo = p.register p.result.adminInfo = p.admin p.result.techInfo = p.tech } func applyCanonicalKV(res *Result, canonical, val string, statusSet map[string]struct{}) bool { switch canonical { case "domain": res.exists = true if d := extractDomainToken(val); d != "" { res.domain = d } return true case "domain_id": res.domainID = strings.TrimSpace(val) return true case "updated_at": if d := parseDateAuto(val); !d.IsZero() { res.hasUpdateDate = true res.updateDate = d } return true case "created_at": if d := parseDateAuto(val); !d.IsZero() { res.hasRegisterDate = true res.registerDate = d } return true case "expired_at": if d := parseDateAuto(val); !d.IsZero() { res.hasExpireDate = true res.expireDate = d } return true case "registrar": res.registrar = strings.TrimSpace(val) return true case "status": if s := firstToken(val); s != "" { statusSet[s] = struct{}{} } return true case "nameserver": if ns, ok := parseNameServerCandidate(val); ok { res.nsServers = appendUnique(res.nsServers, ns) } else if v := strings.TrimSpace(val); v != "" { res.nsServers = appendUnique(res.nsServers, v) } return true case "dnssec": res.dnssec = strings.TrimSpace(val) return true default: return false } } func applyContactAlias(key, val string, r, a, t *PersonalInfo) { k := normalizeKey(key) switch k { case "registrant name": r.Name = val case "registrant organization": r.Org = val case "registrant street": r.Addr = val case "registrant city": r.City = val case "registrant state province": r.State = val case "registrant postal code": r.Zip = val case "registrant country": r.Country = val case "registrant phone": r.Phone = val case "registrant phone ext": r.PhoneExt = val case "registrant fax": r.Fax = val case "registrant fax ext": r.FaxExt = val case "registrant email", "registrant contact email": r.Email = val case "admin name": a.Name = val case "admin organization": a.Org = val case "admin street": a.Addr = val case "admin city": a.City = val case "admin state province": a.State = val case "admin postal code": a.Zip = val case "admin country": a.Country = val case "admin phone": a.Phone = val case "admin phone ext": a.PhoneExt = val case "admin fax": a.Fax = val case "admin fax ext": a.FaxExt = val case "admin email": a.Email = val case "tech name", "tech contact name": t.Name = val case "tech organization", "tech contact organisation", "tech contact organization": t.Org = val case "tech street": t.Addr = val case "tech city": t.City = val case "tech state province": t.State = val case "tech postal code": t.Zip = val case "tech country": t.Country = val case "tech phone": t.Phone = val case "tech phone ext": t.PhoneExt = val case "tech fax": t.Fax = val case "tech fax ext": t.FaxExt = val case "tech email", "tech contact email": t.Email = val } } func firstToken(s string) string { fields := strings.Fields(strings.TrimSpace(s)) if len(fields) == 0 { return "" } return fields[0] } func parseNameServerCandidate(s string) (string, bool) { d := extractDomainToken(s) if d == "" { return "", false } if isIPv4Token(d) { return "", false } return d, true } func parseInlineDomain(line string) (string, bool) { l := strings.ToLower(strings.TrimSpace(line)) if !strings.HasPrefix(l, "domain ") { return "", false } if strings.HasPrefix(l, "domain name") || strings.HasPrefix(l, "domain status") || strings.HasPrefix(l, "domain information") || strings.HasPrefix(l, "domain managers") { return "", false } d := extractDomainToken(line) if d == "" { return "", false } return d, true } func extractDomainToken(s string) string { for _, field := range strings.Fields(strings.TrimSpace(s)) { token := strings.ToLower(strings.Trim(field, "[]()<>\"'`,;")) token = strings.Trim(token, ".") if token == "" { continue } if strings.HasPrefix(token, "http://") || strings.HasPrefix(token, "https://") { continue } if strings.Contains(token, "@") || strings.Contains(token, "/") { continue } if strings.Count(token, ".") < 1 { continue } return token } return "" } func isIPv4Token(s string) bool { parts := strings.Split(s, ".") if len(parts) != 4 { return false } for _, p := range parts { if p == "" { return false } for _, ch := range p { if ch < '0' || ch > '9' { return false } } } return true } func appendUnique(list []string, v string) []string { v = strings.TrimSpace(v) if v == "" { return list } for _, cur := range list { if strings.EqualFold(cur, v) { return list } } return append(list, v) } func extractDomainManagersName(raw string) string { lines := splitParseLines(raw) inManagers := false waitingName := false for _, rawLine := range lines { line := strings.TrimSpace(rawLine) if line == "" || strings.HasPrefix(line, "%") { continue } key := normalizeKey(line) if key == "domain managers" { inManagers = true waitingName = false continue } if !inManagers { continue } if key == "name" { waitingName = true continue } if waitingName { return strings.TrimSpace(line) } } return "" } func extractDomainFromRaw(raw, query string) string { q := strings.ToLower(strings.TrimSpace(query)) if q == "" { return "" } if strings.Contains(strings.ToLower(raw), q) { return q } return "" }