From aa70c40be17611a2f9f16854d6d29ee14362e97b Mon Sep 17 00:00:00 2001 From: Iss M <59224605+twiny@users.noreply.github.com> Date: Sun, 11 Feb 2024 17:17:12 +0100 Subject: [PATCH] break: simplified whois client implementation (#2) * break: simplified whois client implementation - user can specify proxy.Dialer to use for the connection. - add unit tests for the split function. - mod tidy. - make linter happy. --- README.md | 56 ++++++-------- client.go | 197 +++++++++++++++++++------------------------------ client_test.go | 38 +++++++++- db.yaml | 2 +- go.mod | 8 +- go.sum | 35 +++------ 6 files changed, 149 insertions(+), 187 deletions(-) diff --git a/README.md b/README.md index dc6694b..f014e46 100644 --- a/README.md +++ b/README.md @@ -1,15 +1,15 @@ # WHOIS Client -a simple Go WHOIS Client API. +a simple Go WHOIS Client API. It supports custom `proxy.Dialer` for Socks5. ## API + ```go -Lookup(ctx context.Context, domain, server string) (string, error) -WHOISHost(domain string) (string, error) -TLDs() []string +Query(ctx context.Context, domain string) (Response, error) ``` ## Install + `go get github.com/twiny/whois/v2` ## Example @@ -18,44 +18,30 @@ TLDs() []string package main import ( - "context" - "fmt" - "time" + "context" + "fmt" - "github.com/twiny/whois/v2" + "github.com/twiny/whois/v2" ) func main() { - domain := "google.com" - - // to use Socks5 - this format 'socks5://username:password@alpha.hostname.com:1023' - // otherwise use 'whois.Localhost' to use local connection - client, err := whois.NewClient(whois.Localhost) - if err != nil { - fmt.Println(err) - return - } - - ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) - defer cancel() - - host, err := client.WHOISHost(domain) - if err != nil { - fmt.Println(err) - return - } - - resp, err := client.Lookup(ctx, "google.com", host) - if err != nil { - fmt.Println(err) - return - } - - fmt.Println(resp) - fmt.Println("done.") + client, err := whois.NewClient(nil) + if err != nil { + fmt.Printf("err: %s\n", err) + } + + resp, err := client.Query(context.TODO(), "google.com") + if err != nil { + fmt.Printf("err: %s\n", err) + } + + // Print the response + fmt.Printf("Domain: %+v\n", resp) } ``` + ## Tracking + - If you wish to add more WHOIS Server please [create a PR](https://github.com/twiny/whois/pulls). - If you find any issues please [create a new issue](https://github.com/twiny/whois/issues/new). diff --git a/client.go b/client.go index 6df3f00..8342740 100644 --- a/client.go +++ b/client.go @@ -4,11 +4,9 @@ import ( "bytes" "context" _ "embed" - "errors" "fmt" - "io/ioutil" + "io" "net" - "net/url" "regexp" "strings" "time" @@ -19,98 +17,97 @@ import ( ) var ( - testURL = "whois.verisign-grs.com:43" // TestURL - // dialer - forwrder = &net.Dialer{ - Timeout: 15 * time.Second, - } - // - domainRegExp = regexp.MustCompile(`(^(([[:alnum:]]-?)?([[:alnum:]]-?)+\.)+[A-Za-z]{2,20}$)`) - Localhost string = "socks5://localhost" // used for default client //go:embed db.yaml whoisdb []byte -) -// Client -type Client struct { - socks string - db map[string]string - dialer func(network, addr string) (c net.Conn, err error) -} + domainRegExp = regexp.MustCompile(`^(?:(?:[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?\.)+[a-zA-Z]{2,24})$`) -// NewClient: a new WHOIS client using a Socks5 connection -// in this format: socks5://username:password@alpha.hostname.com:1023 -// to use local connection pass `whois.Localhost` as argument. -func NewClient(socks5 string) (*Client, error) { - // whois db - db := map[string]string{} - if err := yaml.Unmarshal(whoisdb, &db); err != nil { - return nil, err + defaultDialer = &net.Dialer{ + Timeout: 15 * time.Second, + KeepAlive: 30 * time.Second, + DualStack: true, } +) - // if using local dialer - if socks5 == Localhost { - var d = &net.Dialer{} - return &Client{ - socks: socks5, - db: db, - dialer: d.Dial, - }, nil +type ( + Client struct { + dialer proxy.Dialer + db map[string]string } - d, err := socks5Dialer(socks5) - if err != nil { + Response struct { + Domain string + Name string + TLD string + WHOISHost string + WHOISRaw string + } +) + +// NewClient: create a new WHOIS client, if dialer is nil, it will use the default dialer +func NewClient(dialer proxy.Dialer) (*Client, error) { + var db = map[string]string{} + if err := yaml.Unmarshal(whoisdb, &db); err != nil { return nil, err } + if dialer == nil { + dialer = defaultDialer + } + return &Client{ - socks: socks5, db: db, - dialer: d.Dial, + dialer: dialer, }, nil } -// Lookup a domain in a whois server db -func (c *Client) Lookup(ctx context.Context, domain, server string) (string, error) { - // check if domain is domain - name, tld, _, err := c.split(domain) +// Query: return raw WHOIS information for a domain +func (c *Client) Query(ctx context.Context, domain string) (Response, error) { + var response Response + name, tld, whoisHost, err := c.split(domain) if err != nil { - return "", err + return response, err } - domain = name + "." + tld + domain = strings.Join([]string{name, tld}, ".") - var text string - result := make(chan string, 1) + var ( + text string + lookupErr error + result = make(chan string, 1) + ) - // lookup go func() { defer close(result) - text, err = c.lookup(domain, server) + text, lookupErr = c.lookup(domain, whoisHost) result <- text }() - // wait for { select { case <-ctx.Done(): - return "", ctx.Err() + return response, ctx.Err() case text := <-result: - if err != nil { - return "", err + if lookupErr != nil { + return response, err } - return text, nil + + response.Domain = domain + response.Name = name + response.TLD = tld + response.WHOISHost = whoisHost + response.WHOISRaw = text + + return response, nil } } } -// lookup -func (c *Client) lookup(domain, server string) (string, error) { - // WHOIS server address at port 43 - addr := net.JoinHostPort(server, "43") +func (c *Client) lookup(domain, whoisHost string) (string, error) { + addr := net.JoinHostPort(whoisHost, "43") // WHOIS server address at port 43 - conn, err := c.dialer("tcp", addr) + conn, err := c.dialer.Dial("tcp", addr) if err != nil { return "", err } @@ -124,7 +121,7 @@ func (c *Client) lookup(domain, server string) (string, error) { return "", err } - resp, err := ioutil.ReadAll(conn) + resp, err := io.ReadAll(conn) if err != nil { return "", err } @@ -136,78 +133,36 @@ func (c *Client) lookup(domain, server string) (string, error) { return strings.Replace(string(resp), "\r", "", -1), nil } -// Split domain name into 3 parts name, tld, server or an error -func (c *Client) WHOISHost(domain string) (string, error) { - _, _, server, err := c.split(domain) - if err != nil { - return "", err +// split: validate and split domain: name, tld, server or error +func (c *Client) split(raw string) (string, string, string, error) { + if !domainRegExp.MatchString(raw) { + return "", "", "", fmt.Errorf("invalid domain: %s", raw) } - return server, nil -} -// split -func (c *Client) split(domain string) (name, tld, server string, err error) { - domain = strings.ToLower(domain) - - // validate - if !domainRegExp.MatchString(domain) { - return "", "", "", fmt.Errorf("domain %s looks invalid", domain) + tld, icann := publicsuffix.PublicSuffix(raw) + if !icann { + return "", "", "", fmt.Errorf("unsupported TLD: %s", tld) } - // extract TLD from domaian - tld = publicsuffix.List.PublicSuffix(domain) - - // find whois host server, found := c.db[tld] if !found { - return "", "", "", fmt.Errorf("could not find corresponded WHOIS server for %s", tld) + return "", "", "", fmt.Errorf("unsupported TLD: %s", tld) } - name = strings.TrimSuffix(domain, "."+tld) + trimmedDomain := strings.TrimSuffix(raw, "."+tld) - return -} - -// TLDs -func (c *Client) TLDs() []string { - var tlds = []string{} - for tld := range c.db { - tlds = append(tlds, tld) - } - return tlds -} - -// socks5Dialer -func socks5Dialer(socks5 string) (proxy.Dialer, error) { - surl, err := url.Parse(socks5) - if err != nil { - return nil, err - } - - if surl.Scheme != "socks5" { - return nil, errors.New("socks must start with socks5://") - } + // Split the raw domain into parts. + // e.g. www.google.com -> www.google -> google + // If no parts, the trimmed domain itself is the name. + // Otherwise, the name is the last part in array. + parts := strings.Split(trimmedDomain, ".") - // check auth - username := surl.User.Username() - password, found := surl.User.Password() - - auth := &proxy.Auth{} - if username+password == "" && !found { - auth = nil - } - - d, err := proxy.SOCKS5("tcp", surl.Host, auth, forwrder) - if err != nil { - return nil, err + var name string + if len(parts) == 0 { + name = trimmedDomain + } else { + name = parts[len(parts)-1] } - // to test connection - conn, err := d.Dial("tcp", testURL) - if err != nil { - return nil, err - } - defer conn.Close() - - return d, nil + return name, tld, server, nil } diff --git a/client_test.go b/client_test.go index dd334ab..177418f 100644 --- a/client_test.go +++ b/client_test.go @@ -1,3 +1,39 @@ package whois -// TODO (twiny) implment tests +import "testing" + +func TestSplit(t *testing.T) { + var cases = []struct { + in string + name string + tld string + err bool + }{ + {"google.com", "google", "com", false}, + {" google.com.", "", "", true}, + {"www.google.com", "google", "com", false}, + {"www.go-ogle.com.", "", "", true}, + {"www.go-ogle.com", "go-ogle", "com", false}, + {"host.alpha.host.www.google.com", "google", "com", false}, + } + + client, err := NewClient(nil) + if err != nil { + t.Errorf("NewClient() == %s, want 'nil'", err) + } + + for _, c := range cases { + name, tld, _, err := client.split(c.in) + if err != nil && !c.err { + t.Errorf("split(%s) == %s, want 'nil'", c.in, err) + } + + if name != c.name { + t.Errorf("split(%q) == %q, want %q", c.in, name, c.name) + } + + if tld != c.tld { + t.Errorf("split(%q) == %q, want %q", c.in, tld, c.tld) + } + } +} diff --git a/db.yaml b/db.yaml index 1a7bfb3..034106f 100644 --- a/db.yaml +++ b/db.yaml @@ -448,4 +448,4 @@ computer: whois.donuts.co pictures: whois.donuts.co gallery: whois.donuts.co gmbh: whois.donuts.co -bar: whois.nic.bar \ No newline at end of file +bar: whois.nic.bar diff --git a/go.mod b/go.mod index d7edefe..32fb03a 100644 --- a/go.mod +++ b/go.mod @@ -1,16 +1,16 @@ module github.com/twiny/whois/v2 -go 1.17 +go 1.21 require ( - github.com/goccy/go-yaml v1.9.4 - golang.org/x/net v0.0.0-20211209124913-491a49abca63 + github.com/goccy/go-yaml v1.11.3 + golang.org/x/net v0.21.0 ) require ( github.com/fatih/color v1.10.0 // indirect github.com/mattn/go-colorable v0.1.8 // indirect github.com/mattn/go-isatty v0.0.12 // indirect - golang.org/x/sys v0.0.0-20210423082822-04245dca01da // indirect + golang.org/x/sys v0.17.0 // indirect golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 // indirect ) diff --git a/go.sum b/go.sum index aa7db33..60d0d41 100644 --- a/go.sum +++ b/go.sum @@ -1,43 +1,28 @@ -github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/fatih/color v1.10.0 h1:s36xzo75JdqLaaWoiEHk767eHiwo0598uUxyfiPkDsg= github.com/fatih/color v1.10.0/go.mod h1:ELkj/draVOlAH/xkhN6mQ50Qd0MPOk5AAr3maGEBuJM= -github.com/go-playground/assert/v2 v2.0.1/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4= github.com/go-playground/locales v0.13.0 h1:HyWk6mgj5qFqCT5fjGBuRArbVDfE4hi8+e8ceBS/t7Q= github.com/go-playground/locales v0.13.0/go.mod h1:taPMhCMXrRLJO55olJkUXHZBHCxTMfnGwq/HNwmWNS8= github.com/go-playground/universal-translator v0.17.0 h1:icxd5fm+REJzpZx7ZfpaD876Lmtgy7VtROAbHHXk8no= github.com/go-playground/universal-translator v0.17.0/go.mod h1:UkSxE5sNxxRwHyU+Scu5vgOQjsIJAF8j9muTVoKLVtA= github.com/go-playground/validator/v10 v10.4.1 h1:pH2c5ADXtd66mxoE0Zm9SUhxE20r7aM3F26W0hOn+GE= github.com/go-playground/validator/v10 v10.4.1/go.mod h1:nlOn6nFhuKACm19sB/8EGNn9GlaMV7XkbRSipzJ0Ii4= -github.com/goccy/go-yaml v1.9.4 h1:S0GCYjwHKVI6IHqio7QWNKNThUl6NLzFd/g8Z65Axw8= -github.com/goccy/go-yaml v1.9.4/go.mod h1:U/jl18uSupI5rdI2jmuCswEA2htH9eXfferR3KfscvA= +github.com/goccy/go-yaml v1.11.3 h1:B3W9IdWbvrUu2OYQGwvU1nZtvMQJPBKgBUuweJjLj6I= +github.com/goccy/go-yaml v1.11.3/go.mod h1:wKnAMd44+9JAAnGQpWVEgBzGt3YuTaQ4uXoHvE4m7WU= +github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38= +github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= github.com/leodido/go-urn v1.2.0 h1:hpXL4XnriNwQ/ABnpepYM/1vCLWNDfUNts8dX3xTG6Y= github.com/leodido/go-urn v1.2.0/go.mod h1:+8+nEpDfqqsY+g338gtMEUOtuK+4dEMhiQEgxpxOKII= github.com/mattn/go-colorable v0.1.8 h1:c1ghPdyEDarC70ftn0y+A/Ee++9zz8ljHG1b13eJ0s8= github.com/mattn/go-colorable v0.1.8/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc= github.com/mattn/go-isatty v0.0.12 h1:wuysRhFDzyxgEmMf5xjvJ2M9dZoWAXNNr5LSBS7uHXY= github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU= -github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= -github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= -github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= -golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= -golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9 h1:psW17arqaxU48Z5kZ0CQnkZWQJsqcURM6tKiBApRjXI= -golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= -golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= -golang.org/x/net v0.0.0-20211209124913-491a49abca63 h1:iocB37TsdFuN6IBRZ+ry36wrkoV51/tl5vOWqkcPGvY= -golang.org/x/net v0.0.0-20211209124913-491a49abca63/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= -golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= -golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/crypto v0.19.0 h1:ENy+Az/9Y1vSrlrvBSyna3PITt4tiZLf7sgCjZBX7Wo= +golang.org/x/crypto v0.19.0/go.mod h1:Iy9bg/ha4yyC70EfRS8jz+B6ybOBKMaSxLj6P6oBDfU= +golang.org/x/net v0.21.0 h1:AQyQV4dYCvJ7vGmJyKki9+PBdyvhkSd8EIx/qb0AYv4= +golang.org/x/net v0.21.0/go.mod h1:bIjVDfnllIU7BJ2DNgfnXvpSvtn8VRwhlsaeUTyUS44= golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20210423082822-04245dca01da h1:b3NXsE2LusjYGGjL5bxEVZZORm/YEFFrWFjR8eFrw/c= -golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= -golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= -golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= -golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= -golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/sys v0.17.0 h1:25cE3gD+tdBA7lp7QfhuV+rJiE9YXTcS3VG1SqssI/Y= +golang.org/x/sys v0.17.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 h1:go1bK/D/BFZV2I8cIQd1NKEZ+0owSTG1fDTci4IqFcE= golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= -gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= -gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=