431 lines
8.8 KiB
Go
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 ""
|
|
}
|