Skip to content

Commit

Permalink
Add DNS client
Browse files Browse the repository at this point in the history
  • Loading branch information
ammario committed Aug 12, 2016
2 parents 86f4b3f + 4b2b245 commit c5a6bd3
Show file tree
Hide file tree
Showing 6 changed files with 415 additions and 234 deletions.
4 changes: 2 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ IPISP wraps [Team Cymru's](http://www.team-cymru.org/IP-ASN-mapping.html) IP to

Features
- Programmatically resolve an IP address's AS number, ISP name, range, allocation time, registry, and country of registration.
- Safe for bulk usage by using Cymru's netcat interface
- Safe for bulk usage by using Cymru's DNS interface
- Concurrent safe


Expand All @@ -15,7 +15,7 @@ Features
A more thorough example is in the examples/ folder.

```go
client, _ := ipisp.NewClient()
client, _ := ipisp.NewDnsClient()
resp, err := client.LookupIP(net.ParseIP("4.2.2.2"))

fmt.Printf("IP: %v\n", resp.IP)
Expand Down
233 changes: 6 additions & 227 deletions client.go
Original file line number Diff line number Diff line change
@@ -1,234 +1,13 @@
//Package ipisp provides a wrapper to team-cymru.com IP to ASN service.
//ipisp uses Cymru's netcat interface
package ipisp

import (
"bufio"
"bytes"
"errors"
"net"
"strconv"
"sync"
"time"
)

var ncEOL = []byte("\r\n")

//Timeout is the TCP connection timeout
var Timeout = time.Second * 10

//Common errors
var (
ErrUnexpectedTokens = errors.New("Unexpected tokens while reading Cymru response.")
)

const (
netcatIPTokensLength = 7
netcatASNTokensLength = 5
)

//Network addresses
const (
cymruNetcatAddress = "whois.cymru.com:43"
)

//Client wraps the team-cyru services
type Client struct {
conn net.Conn
w *bufio.Writer
sc *bufio.Scanner
ncmu *sync.Mutex
}

//NewClient returns a pointer to a new connected IPISP client
func NewClient() (client *Client, err error) {
client = &Client{}
client.conn, err = net.DialTimeout("tcp", cymruNetcatAddress, Timeout)
client.ncmu = &sync.Mutex{}
if err != nil {
return
}
client.w = bufio.NewWriter(client.conn)
client.sc = bufio.NewScanner(client.conn)

client.w.Write([]byte("begin"))
client.w.Write(ncEOL)
client.w.Write([]byte("verbose"))
client.w.Write(ncEOL)

err = client.w.Flush()
if err != nil {
return
}

//Discard first hello line
client.sc.Scan()
client.sc.Bytes()
err = client.sc.Err()
return
}

//Close closes a client.
func (c *Client) Close() error {
c.w.Write([]byte("end"))
c.w.Write(ncEOL)
return c.conn.Close()
}

//LookupIPs looks up IPs and returns a slice of responses the same size as the input slice of IPs
//The response slice will be in the same order as the input IPs
func (c *Client) LookupIPs(ips []net.IP) (resp []Response, err error) {
resp = make([]Response, 0, len(ips))

c.ncmu.Lock()
defer c.ncmu.Unlock()
for _, ip := range ips {
c.w.WriteString(ip.String())
c.w.Write(ncEOL)
if err = c.w.Flush(); err != nil {
return resp, err
}
}
//Raw response
var raw []byte
var tokens [][]byte
var asn int

var finished bool

//Read results
for !finished && c.sc.Scan() {

raw = c.sc.Bytes()
if bytes.HasPrefix(raw, []byte("Error: ")) {
return resp, errors.New(string(bytes.TrimSpace(bytes.TrimLeft(raw, "Error: "))))
}
tokens = bytes.Split(raw, []byte{'|'})

if len(tokens) != netcatIPTokensLength {
return resp, ErrUnexpectedTokens
}

//Trim excess whitespace from tokens
for i := range tokens {
tokens[i] = bytes.TrimSpace(tokens[i])
}

re := Response{}

//Read ASN
if asn, err = strconv.Atoi(string(tokens[0])); err != nil {
return
}
re.ASN = ASN(asn)

//Read IP
re.IP = net.ParseIP(string(tokens[1]))

//Read range
if _, re.Range, err = net.ParseCIDR(string(tokens[2])); err != nil {
return
}

//Read country
re.Country = NewCountryFromCode(string(tokens[3]))

//Read registry
re.Registry = string(tokens[4])

//Read allocated. Ignore error as a lot of entries don't have an allocated value.
re.Allocated, _ = time.Parse("2006-01-02", string(tokens[5]))

//Read name
re.Name = NewName(string(tokens[6]))

//Add to response slice
resp = append(resp, re)
if len(resp) == cap(resp) {
finished = true
}
}
return resp, err
}

//LookupIP is a single IP convenience proxy of LookupIPs
func (c *Client) LookupIP(ip net.IP) (*Response, error) {
resp, err := c.LookupIPs([]net.IP{ip})
if len(resp) == 0 {
return nil, err
}
return &resp[0], err
}

//LookupASNs looks up ASNs. Response IP and Range fields are zeroed
func (c *Client) LookupASNs(asns []ASN) (resp []Response, err error) {
resp = make([]Response, 0, len(asns))

c.ncmu.Lock()
defer c.ncmu.Unlock()
for _, asn := range asns {
c.w.WriteString(asn.String())
c.w.Write(ncEOL)
if err = c.w.Flush(); err != nil {
return resp, err
}
}

//Raw response
var raw []byte
var tokens [][]byte
var asn int

var finished bool

//Read results
for !finished && c.sc.Scan() {
raw = c.sc.Bytes()
if bytes.HasPrefix(raw, []byte("Error: ")) {
return resp, errors.New(string(bytes.TrimSpace(bytes.TrimLeft(raw, "Error: "))))
}
tokens = bytes.Split(raw, []byte{'|'})

if len(tokens) != netcatASNTokensLength {
return resp, ErrUnexpectedTokens
}

//Trim excess whitespace from tokens
for i := range tokens {
tokens[i] = bytes.TrimSpace(tokens[i])
}

re := Response{}

//Read ASN
if asn, err = strconv.Atoi(string(tokens[0])); err != nil {
return
}
re.ASN = ASN(asn)

//Read country
re.Country = NewCountryFromCode(string(tokens[1]))

//Read registry
re.Registry = string(tokens[2])

//Read allocated. Ignore error as a lot of entries don't have an allocated value.
re.Allocated, _ = time.Parse("2006-01-02", string(tokens[3]))

//Read name
re.Name = NewName(string(tokens[4]))

//Add to response slice
resp = append(resp, re)
if len(resp) == cap(resp) {
finished = true
}
}
return resp, err
}

//LookupASN is a single ASN convenience proxy of LookupASNs
func (c *Client) LookupASN(asn ASN) (Response, error) {
resp, err := c.LookupASNs([]ASN{asn})
return resp[0], err
type Client interface {
LookupIPs([]net.IP) ([]Response, error)
LookupIP(net.IP) (*Response, error)
LookupASNs([]ASN) ([]Response, error)
LookupASN(ASN) (*Response, error)
Close() error
}
8 changes: 4 additions & 4 deletions country.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,8 @@ type Country struct {
}

//NewCountryFromCode returns a country from a country code
func NewCountryFromCode(code string) *Country {
country := &Country{}
country.Name, _ = langreg.RegionName(code)
return country
func NewCountryFromCode(code string) (country *Country, err error) {
country = &Country{}
country.Name, err = langreg.RegionName(code)
return country, err
}
Loading

0 comments on commit c5a6bd3

Please sign in to comment.