commit 6a8915039e2be4b6f95984102dd608c4fe36a9c7 Author: Starainrt Date: Thu Aug 15 08:38:28 2024 +0800 init diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..723ef36 --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +.idea \ No newline at end of file diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..507c8a4 --- /dev/null +++ b/go.mod @@ -0,0 +1,5 @@ +module b612.me/sdk/whois + +go 1.21.2 + +require golang.org/x/net v0.24.0 diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..4a8ba20 --- /dev/null +++ b/go.sum @@ -0,0 +1,2 @@ +golang.org/x/net v0.24.0 h1:1PcaxkF854Fu3+lvBIx5SYn9wRlBzzcnHZSiaFFAb0w= +golang.org/x/net v0.24.0/go.mod h1:2Q7sJY5mzlzWjKtYUEXSlBWCdyaioyXzRB2RtU8KVE8= diff --git a/parse.go b/parse.go new file mode 100644 index 0000000..fdd6c41 --- /dev/null +++ b/parse.go @@ -0,0 +1,415 @@ +package whois + +import ( + "fmt" + "os" + "strings" + "time" +) + +func parse(domain string, result string) { + os.WriteFile(domain+".txt", []byte(result), 0644) + ext := getExtension(domain) + secExt := getExtension(strings.TrimSuffix(domain, "."+ext)) + _ = secExt + var data Result + var err error + switch ext { + case "cn": + data, err = dotCNParser(domain, result) + case "jp": + data, err = dotJPParser(domain, result) + case "tw": + data, err = dotTWParser(domain, result) + default: + data, err = commonParser(domain, result) + } + fmt.Println(err) + fmt.Println(data.domain, data.registerDate, data.expireDate, data.nsServers) + fmt.Println(data.exists, data.statusRaw, data.nsServers) + fmt.Printf("%+v\n%+v\n", data.adminInfo, data.registerInfo) +} + +func commonParser(domain, data string) (Result, error) { + var res = Result{ + domain: domain, + rawData: data, + } + var r, a, t PersonalInfo + + split := strings.Split(data, "\n") + for _, line := range split { + line = strings.TrimSpace(line) + if !res.exists { + for _, token := range []string{"Domain not found.", "No match for", "No match", "No Data Found", "No entries found", "No match for domain", "No matching record", "No Found", "No Object"} { + if strings.HasPrefix(token, line) { + res.exists = false + return res, nil + } + } + } + if strings.HasPrefix(line, "Domain Name:") { + res.exists = true + res.nsServers = []string{} + res.domain = strings.TrimSpace(strings.TrimPrefix(line, "Domain Name:")) + } + if strings.HasPrefix(line, "Registry Domain ID:") { + res.domainID = strings.TrimSpace(strings.TrimPrefix(line, "Registry Domain ID:")) + } + if strings.HasPrefix(line, "Updated Date:") { + res.updateDate = parseDate(strings.TrimSpace(strings.TrimPrefix(line, "Updated Date:"))) + } + if strings.HasPrefix(line, "Creation Date:") { + res.registerDate = parseDate(strings.TrimSpace(strings.TrimPrefix(line, "Creation Date:"))) + } + if strings.HasPrefix(line, "Registry Expiry Date:") { + res.expireDate = parseDate(strings.TrimSpace(strings.TrimPrefix(line, "Registry Expiry Date:"))) + } + if strings.HasPrefix(line, "Registrar:") { + res.registar = strings.TrimSpace(strings.TrimPrefix(line, "Registrar:")) + } + if strings.HasPrefix(line, "Status:") { + res.statusRaw = strings.TrimSpace(strings.TrimPrefix(line, "Status:")) + } + if strings.HasPrefix(line, "Name Server:") { + res.nsServers = append(res.nsServers, strings.TrimSpace(strings.TrimPrefix(line, "Name Server:"))) + } + if strings.HasPrefix(line, "DNSSEC:") { + res.dnssec = strings.TrimSpace(strings.TrimPrefix(line, "DNSSEC:")) + } + if strings.HasPrefix(line, "Registrar Registration Expiration Date:") { + res.expireDate = parseDate(strings.TrimSpace(strings.TrimPrefix(line, "Registrar Registration Expiration Date:"))) + } + if strings.HasPrefix(line, "Registrant Name:") { + r.Name = strings.TrimSpace(strings.TrimPrefix(line, "Registrant Name:")) + } + if strings.HasPrefix(line, "Registrant Organization:") { + r.Org = strings.TrimSpace(strings.TrimPrefix(line, "Registrant Organization:")) + } + if strings.HasPrefix(line, "Registrant Street:") { + r.Addr = strings.TrimSpace(strings.TrimPrefix(line, "Registrant Street:")) + } + if strings.HasPrefix(line, "Registrant City:") { + r.City = strings.TrimSpace(strings.TrimPrefix(line, "Registrant City:")) + } + if strings.HasPrefix(line, "Registrant State/Province:") { + r.State = strings.TrimSpace(strings.TrimPrefix(line, "Registrant State/Province:")) + } + if strings.HasPrefix(line, "Registrant Postal Code:") { + r.Zip = strings.TrimSpace(strings.TrimPrefix(line, "Registrant Postal Code:")) + } + if strings.HasPrefix(line, "Registrant Country:") { + r.Country = strings.TrimSpace(strings.TrimPrefix(line, "Registrant Country:")) + } + if strings.HasPrefix(line, "Registrant Phone:") { + r.Phone = strings.TrimSpace(strings.TrimPrefix(line, "Registrant Phone:")) + } + if strings.HasPrefix(line, "Registrant Phone Ext:") { + r.PhoneExt = strings.TrimSpace(strings.TrimPrefix(line, "Registrant Phone Ext:")) + } + if strings.HasPrefix(line, "Registrant Fax:") { + r.Fax = strings.TrimSpace(strings.TrimPrefix(line, "Registrant Fax:")) + } + if strings.HasPrefix(line, "Registrant Fax Ext:") { + r.FaxExt = strings.TrimSpace(strings.TrimPrefix(line, "Registrant Fax Ext:")) + } + if strings.HasPrefix(line, "Registrant Email:") { + r.Email = strings.TrimSpace(strings.TrimPrefix(line, "Registrant Email:")) + } + if strings.HasPrefix(line, "Admin Name:") { + a.Name = strings.TrimSpace(strings.TrimPrefix(line, "Admin Name:")) + } + if strings.HasPrefix(line, "Admin Organization:") { + a.Org = strings.TrimSpace(strings.TrimPrefix(line, "Admin Organization:")) + } + if strings.HasPrefix(line, "Admin Street:") { + a.Addr = strings.TrimSpace(strings.TrimPrefix(line, "Admin Street:")) + } + if strings.HasPrefix(line, "Admin City:") { + a.City = strings.TrimSpace(strings.TrimPrefix(line, "Admin City:")) + } + if strings.HasPrefix(line, "Admin State/Province:") { + a.State = strings.TrimSpace(strings.TrimPrefix(line, "Admin State/Province:")) + } + if strings.HasPrefix(line, "Admin Postal Code:") { + a.Zip = strings.TrimSpace(strings.TrimPrefix(line, "Admin Postal Code:")) + } + if strings.HasPrefix(line, "Admin Country:") { + a.Country = strings.TrimSpace(strings.TrimPrefix(line, "Admin Country:")) + } + if strings.HasPrefix(line, "Admin Phone:") { + a.Phone = strings.TrimSpace(strings.TrimPrefix(line, "Admin Phone:")) + } + if strings.HasPrefix(line, "Admin Phone Ext:") { + a.PhoneExt = strings.TrimSpace(strings.TrimPrefix(line, "Admin Phone Ext:")) + } + if strings.HasPrefix(line, "Admin Fax:") { + a.Fax = strings.TrimSpace(strings.TrimPrefix(line, "Admin Fax:")) + } + if strings.HasPrefix(line, "Admin Fax Ext:") { + a.FaxExt = strings.TrimSpace(strings.TrimPrefix(line, "Admin Fax Ext:")) + } + if strings.HasPrefix(line, "Admin Email:") { + a.Email = strings.TrimSpace(strings.TrimPrefix(line, "Admin Email:")) + } + if strings.HasPrefix(line, "Tech Name:") { + t.Name = strings.TrimSpace(strings.TrimPrefix(line, "Tech Name:")) + } + if strings.HasPrefix(line, "Tech Organization:") { + t.Org = strings.TrimSpace(strings.TrimPrefix(line, "Tech Organization:")) + } + if strings.HasPrefix(line, "Tech Street:") { + t.Addr = strings.TrimSpace(strings.TrimPrefix(line, "Tech Street:")) + } + if strings.HasPrefix(line, "Tech City:") { + t.City = strings.TrimSpace(strings.TrimPrefix(line, "Tech City:")) + } + if strings.HasPrefix(line, "Tech State/Province:") { + t.State = strings.TrimSpace(strings.TrimPrefix(line, "Tech State/Province:")) + } + if strings.HasPrefix(line, "Tech Postal Code:") { + t.Zip = strings.TrimSpace(strings.TrimPrefix(line, "Tech Postal Code:")) + } + if strings.HasPrefix(line, "Tech Country:") { + t.Country = strings.TrimSpace(strings.TrimPrefix(line, "Tech Country:")) + } + if strings.HasPrefix(line, "Tech Phone:") { + t.Phone = strings.TrimSpace(strings.TrimPrefix(line, "Tech Phone:")) + } + if strings.HasPrefix(line, "Tech Phone Ext:") { + t.PhoneExt = strings.TrimSpace(strings.TrimPrefix(line, "Tech Phone Ext:")) + } + if strings.HasPrefix(line, "Tech Fax:") { + t.Fax = strings.TrimSpace(strings.TrimPrefix(line, "Tech Fax:")) + } + if strings.HasPrefix(line, "Tech Fax Ext:") { + t.FaxExt = strings.TrimSpace(strings.TrimPrefix(line, "Tech Fax Ext:")) + } + if strings.HasPrefix(line, "Tech Email:") { + t.Email = strings.TrimSpace(strings.TrimPrefix(line, "Tech Email:")) + } + } + res.registerInfo = r + res.adminInfo = a + res.techInfo = t + return res, nil +} + +func dotCNParser(domain, data string) (Result, error) { + var res = Result{ + domain: domain, + rawData: data, + } + var r PersonalInfo + if strings.HasPrefix("No matching record.", strings.TrimSpace(data)) { + res.exists = false + return res, nil + } + res.exists = true + split := strings.Split(data, "\n") + for _, line := range split { + line = strings.TrimSpace(line) + if strings.HasPrefix(line, "Domain Name:") { + res.nsServers = []string{} + res.domain = strings.TrimSpace(strings.TrimPrefix(line, "Domain Name:")) + } + if strings.HasPrefix(line, "ROID:") { + res.domainID = strings.TrimSpace(strings.TrimPrefix(line, "ROID:")) + } + if strings.HasPrefix(line, "Registration Time:") { + res.registerDate = parseCNDate(strings.TrimSpace(strings.TrimPrefix(line, "Registration Time:"))) + } + if strings.HasPrefix(line, "Expiration Time:") { + res.expireDate = parseCNDate(strings.TrimSpace(strings.TrimPrefix(line, "Expiration Time:"))) + } + if strings.HasPrefix(line, "Sponsoring Registrar:") { + res.registar = strings.TrimSpace(strings.TrimPrefix(line, "Sponsoring Registrar:")) + } + if strings.HasPrefix(line, "Status:") { + res.statusRaw = strings.TrimSpace(strings.TrimPrefix(line, "Status:")) + } + if strings.HasPrefix(line, "Name Server:") { + res.nsServers = append(res.nsServers, strings.TrimSpace(strings.TrimPrefix(line, "Name Server:"))) + } + if strings.HasPrefix(line, "DNSSEC:") { + res.dnssec = strings.TrimSpace(strings.TrimPrefix(line, "DNSSEC:")) + } + if strings.HasPrefix(line, "Registrant:") { + r.Name = strings.TrimSpace(strings.TrimPrefix(line, "Registrant:")) + } + if strings.HasPrefix(line, "Registrant Contact Email:") { + r.Email = strings.TrimSpace(strings.TrimPrefix(line, "Registrant Contact Email:")) + } + } + res.registerInfo = r + return res, nil +} + +func dotJPParser(domain, data string) (Result, error) { + var res = Result{ + domain: domain, + rawData: data, + } + var r PersonalInfo + split := strings.Split(data, "\n") + startAddress := false + for _, line := range split { + if strings.HasPrefix(line, "No match!!") { + res.exists = false + return res, nil + } + line = strings.TrimSpace(line) + if strings.HasPrefix(line, "[Domain Name]") { + res.nsServers = []string{} + res.domain = strings.TrimSpace(strings.TrimPrefix(line, "[Domain Name]")) + } + if strings.HasPrefix(line, "[登録者名]") { + r.Name = strings.TrimSpace(strings.TrimPrefix(line, "[登録者名]")) + } + if strings.HasPrefix(line, "[Name Server]") { + res.nsServers = append(res.nsServers, strings.TrimSpace(strings.TrimPrefix(line, "[Name Server]"))) + } + if strings.HasPrefix(line, "[状態]") { + res.statusRaw += strings.TrimSpace(strings.TrimPrefix(line, "[状態]")) + } + if strings.HasPrefix(line, "[ロック状態]") { + res.statusRaw += strings.TrimSpace(strings.TrimPrefix(line, "[ロック状態]")) + } + if strings.HasPrefix(line, "[登録年月日]") { + res.registerDate = parseJPDate(strings.TrimSpace(strings.TrimPrefix(line, "[登録年月日]")), true) + } + if strings.HasPrefix(line, "[最終更新]") { + res.updateDate = parseJPDate(strings.TrimSpace(strings.TrimPrefix(line, "[最終更新]")), false) + } + if strings.HasPrefix(line, "[有効期限]") { + res.expireDate = parseJPDate(strings.TrimSpace(strings.TrimPrefix(line, "[有効期限]")), true) + } + if strings.HasPrefix(line, "[名前]") { + r.Name = strings.TrimSpace(strings.TrimPrefix(line, "[名前]")) + } + if strings.HasPrefix(line, "[組織名]") { + r.Org = strings.TrimSpace(strings.TrimPrefix(line, "[組織名]")) + } + if strings.HasPrefix(line, "[Email]") { + r.Email = strings.TrimSpace(strings.TrimPrefix(line, "[Email]")) + } + if strings.HasPrefix(line, "[住所]") { + r.Addr = strings.TrimSpace(strings.TrimPrefix(line, "[住所]")) + continue + } + if strings.HasPrefix(line, "[Postal Address]") { + startAddress = false + continue + } + if startAddress { + r.Addr += "\n" + line + } + if strings.HasPrefix(line, "[電話番号]") { + r.Phone = strings.TrimSpace(strings.TrimPrefix(line, "[電話番号]")) + } + if strings.HasPrefix(line, "[FAX番号]") { + r.Fax = strings.TrimSpace(strings.TrimPrefix(line, "[FAX番号]")) + break + } + } + res.registerInfo = r + return res, nil +} + +func dotTWParser(domain, data string) (Result, error) { + var res = Result{ + domain: domain, + rawData: data, + } + split := strings.Split(data, "\n") + startNs := false + var currentStatus uint8 = 0 + var startIdx int + var r, a, t, p PersonalInfo + for idx, line := range split { + line = strings.TrimSpace(line) + if strings.HasPrefix(line, "No Found") || strings.HasPrefix(line, "網域名稱不合規定") { + res.exists = false + return res, nil + } + if strings.HasPrefix(line, "Domain Name:") { + res.exists = true + res.nsServers = []string{} + res.domain = strings.TrimSpace(strings.TrimPrefix(line, "Domain Name:")) + } + if strings.HasPrefix(line, "Registration Service Provider:") { + startNs = false + res.registar = strings.TrimSpace(strings.TrimPrefix(line, "Registration Service Provider:")) + } + if strings.HasPrefix(line, "Record created on") { + res.registerDate = parseCNDate(strings.TrimSpace(strings.TrimSuffix(strings.TrimPrefix(line, "Record created on"), "(UTC+8)"))) + } + if strings.HasPrefix(line, "Record expires on") { + res.expireDate = parseCNDate(strings.TrimSpace(strings.TrimSuffix(strings.TrimPrefix(line, "Record expires on"), "(UTC+8)"))) + } + if strings.HasPrefix(line, "Domain Status:") { + res.statusRaw = strings.TrimSpace(strings.TrimPrefix(line, "Domain Status:")) + } + if strings.HasPrefix(line, "Domain servers in listed order:") { + startNs = true + continue + } + if strings.HasPrefix(line, "Registrant:") { + currentStatus = 1 + startIdx = idx + p = PersonalInfo{} + continue + } + if currentStatus > 0 { + if line == "(Redacted for privacy)" { + switch currentStatus { + case 1: + r = p + case 2: + a = p + case 3: + t = p + } + continue + } + switch idx - startIdx { + case 1: + p.Name = line + case 3: + p.Email = line + case 4: + p.Country = line + } + } + if startNs { + res.nsServers = append(res.nsServers, line) + } + } + res.registerInfo = r + res.adminInfo = a + res.techInfo = t + return res, nil +} + +func parseDate(date string) time.Time { + t, _ := time.Parse("2006-01-02T15:04:05Z", date) + t = t.In(time.Local) + return t +} + +func parseCNDate(date string) time.Time { + t, _ := time.ParseInLocation("2006-01-02 15:04:05", date, time.FixedZone("CST", 8*3600)) + t = t.In(time.Local) + return t +} + +func parseJPDate(date string, onlyMD bool) time.Time { + if onlyMD { + t, _ := time.ParseInLocation("2006/01/02", date, time.FixedZone("JST", 9*3600)) + t = t.In(time.Local) + return t + } + t, _ := time.ParseInLocation("2006/01/02 15:04:05 (JST)", date, time.FixedZone("JST", 9*3600)) + t = t.In(time.Local) + return t +} diff --git a/parse_test.go b/parse_test.go new file mode 100644 index 0000000..1785c53 --- /dev/null +++ b/parse_test.go @@ -0,0 +1,11 @@ +package whois + +import ( + "fmt" + "testing" +) + +func TestWhois(t *testing.T) { + c := NewClient() + fmt.Println(c.Whois("b612.com")) +} diff --git a/whois.go b/whois.go new file mode 100644 index 0000000..46cabc2 --- /dev/null +++ b/whois.go @@ -0,0 +1,258 @@ +package whois + +import ( + "errors" + "fmt" + "io" + "net" + "strconv" + "strings" + "time" + + "golang.org/x/net/proxy" +) + +const ( + defWhoisServer = "whois.iana.org" + defWhoisPort = "43" + defTimeout = 30 * time.Second + asnPrefix = "AS" +) + +// DefaultClient is default whois client +var DefaultClient = NewClient() + +// Client is whois client +type Client struct { + dialer proxy.Dialer + timeout time.Duration + elapsed time.Duration + disableReferral bool +} +type PersonalInfo struct { + FirstName string + LastName string + Name string + Org string + Fax string + FaxExt string + Addr string + City string + State string + Country string + Zip string + Phone string + PhoneExt string + Email string +} + +type Result struct { + exists bool + domain string + domainID string + rawData string + registar string + registerDate time.Time + updateDate time.Time + expireDate time.Time + statusRaw string + nsServers []string + dnssec string + whoisSer string + ianaID string + registerInfo PersonalInfo + adminInfo PersonalInfo + techInfo PersonalInfo +} + +// NewClient returns new whois client +func NewClient() *Client { + return &Client{ + dialer: &net.Dialer{ + Timeout: defTimeout, + }, + timeout: defTimeout, + } +} + +func (c *Client) Whois(domain string, servers ...string) (string, error) { + data, err := c.whois(domain) + if err != nil { + return data, err + } + parse(domain, data) + return "", err +} + +func (c *Client) whois(domain string, servers ...string) (result string, err error) { + start := time.Now() + defer func() { + result = strings.TrimSpace(result) + if result != "" { + result = fmt.Sprintf("%s\n\n%% Query time: %d msec\n%% WHEN: %s\n", + result, time.Since(start).Milliseconds(), start.Format("Mon Jan 02 15:04:05 MST 2006"), + ) + } + }() + + domain = strings.Trim(strings.TrimSpace(domain), ".") + if domain == "" { + return "", errors.New("whois: domain is empty") + } + + isASN := IsASN(domain) + if isASN { + if !strings.HasPrefix(strings.ToUpper(domain), asnPrefix) { + domain = asnPrefix + domain + } + } + + if !strings.Contains(domain, ".") && !strings.Contains(domain, ":") && !isASN { + return c.rawQuery(domain, defWhoisServer, defWhoisPort) + } + + var server, port string + if len(servers) > 0 && servers[0] != "" { + server = strings.ToLower(servers[0]) + port = defWhoisPort + } else { + ext := getExtension(domain) + result, err := c.rawQuery(ext, defWhoisServer, defWhoisPort) + if err != nil { + return "", fmt.Errorf("whois: query for whois server failed: %w", err) + } + server, port = getServer(result) + if server == "" { + return "", fmt.Errorf("%w: %s", errors.New("whois server not found"), domain) + } + } + + result, err = c.rawQuery(domain, server, port) + if err != nil { + return + } + + if c.disableReferral { + return + } + + refServer, refPort := getServer(result) + if refServer == "" || refServer == server { + return + } + + data, err := c.rawQuery(domain, refServer, refPort) + if err == nil { + result += data + } + + return +} + +// SetDialer set query net dialer +func (c *Client) SetDialer(dialer proxy.Dialer) *Client { + c.dialer = dialer + return c +} + +// SetTimeout set query timeout +func (c *Client) SetTimeout(timeout time.Duration) *Client { + c.timeout = timeout + return c +} + +// rawQuery do raw query to the server +func (c *Client) rawQuery(domain, server, port string) (string, error) { + c.elapsed = 0 + start := time.Now() + + if server == "whois.arin.net" { + if IsASN(domain) { + domain = "a + " + domain + } else { + domain = "n + " + domain + } + } + + conn, err := c.dialer.Dial("tcp", net.JoinHostPort(server, port)) + if err != nil { + return "", fmt.Errorf("whois: connect to whois server failed: %w", err) + } + + defer conn.Close() + c.elapsed = time.Since(start) + + _ = conn.SetWriteDeadline(time.Now().Add(c.timeout - c.elapsed)) + _, err = conn.Write([]byte(domain + "\r\n")) + if err != nil { + return "", fmt.Errorf("whois: send to whois server failed: %w", err) + } + + c.elapsed = time.Since(start) + + _ = conn.SetReadDeadline(time.Now().Add(c.timeout - c.elapsed)) + buffer, err := io.ReadAll(conn) + if err != nil { + return "", fmt.Errorf("whois: read from whois server failed: %w", err) + } + + c.elapsed = time.Since(start) + + return string(buffer), nil +} + +// getExtension returns extension of domain +func getExtension(domain string) string { + ext := domain + + if net.ParseIP(domain) == nil { + domains := strings.Split(domain, ".") + ext = domains[len(domains)-1] + } + + if strings.Contains(ext, "/") { + ext = strings.Split(ext, "/")[0] + } + + return ext +} + +// getServer returns server from whois data +func getServer(data string) (string, string) { + tokens := []string{ + "Registrar WHOIS Server: ", + "whois: ", + "ReferralServer: ", + "refer: ", + } + for _, token := range tokens { + start := strings.Index(data, token) + if start != -1 { + start += len(token) + end := strings.Index(data[start:], "\n") + server := strings.TrimSpace(data[start : start+end]) + server = strings.TrimPrefix(server, "http:") + server = strings.TrimPrefix(server, "https:") + server = strings.TrimPrefix(server, "whois:") + server = strings.TrimPrefix(server, "rwhois:") + server = strings.Trim(server, "/") + port := defWhoisPort + if strings.Contains(server, ":") { + v := strings.Split(server, ":") + server, port = v[0], v[1] + } + return server, port + } + } + return "", "" +} + +// IsASN returns if s is ASN +func IsASN(s string) bool { + s = strings.ToUpper(s) + + s = strings.TrimPrefix(s, asnPrefix) + _, err := strconv.Atoi(s) + + return err == nil +}