whoissdk/parse_common.go
2026-03-19 11:53:07 +08:00

431 lines
8.8 KiB
Go

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 ""
}