Home | History | Annotate | Download | only in net
      1 // Copyright 2009 The Go Authors. All rights reserved.
      2 // Use of this source code is governed by a BSD-style
      3 // license that can be found in the LICENSE file.
      4 
      5 package net
      6 
      7 import (
      8 	"bytes"
      9 	"encoding/json"
     10 	"errors"
     11 	"os/exec"
     12 	"reflect"
     13 	"regexp"
     14 	"sort"
     15 	"strconv"
     16 	"strings"
     17 	"testing"
     18 )
     19 
     20 var nslookupTestServers = []string{"mail.golang.com", "gmail.com"}
     21 
     22 func toJson(v interface{}) string {
     23 	data, _ := json.Marshal(v)
     24 	return string(data)
     25 }
     26 
     27 func TestLookupMX(t *testing.T) {
     28 	if testing.Short() || !*testExternal {
     29 		t.Skip("avoid external network")
     30 	}
     31 
     32 	for _, server := range nslookupTestServers {
     33 		mx, err := LookupMX(server)
     34 		if err != nil {
     35 			t.Error(err)
     36 			continue
     37 		}
     38 		if len(mx) == 0 {
     39 			t.Errorf("no results")
     40 			continue
     41 		}
     42 		expected, err := nslookupMX(server)
     43 		if err != nil {
     44 			t.Logf("skipping failed nslookup %s test: %s", server, err)
     45 		}
     46 		sort.Sort(byPrefAndHost(expected))
     47 		sort.Sort(byPrefAndHost(mx))
     48 		if !reflect.DeepEqual(expected, mx) {
     49 			t.Errorf("different results %s:\texp:%v\tgot:%v", server, toJson(expected), toJson(mx))
     50 		}
     51 	}
     52 }
     53 
     54 func TestLookupCNAME(t *testing.T) {
     55 	if testing.Short() || !*testExternal {
     56 		t.Skip("avoid external network")
     57 	}
     58 
     59 	for _, server := range nslookupTestServers {
     60 		cname, err := LookupCNAME(server)
     61 		if err != nil {
     62 			t.Errorf("failed %s: %s", server, err)
     63 			continue
     64 		}
     65 		if cname == "" {
     66 			t.Errorf("no result %s", server)
     67 		}
     68 		expected, err := nslookupCNAME(server)
     69 		if err != nil {
     70 			t.Logf("skipping failed nslookup %s test: %s", server, err)
     71 			continue
     72 		}
     73 		if expected != cname {
     74 			t.Errorf("different results %s:\texp:%v\tgot:%v", server, expected, cname)
     75 		}
     76 	}
     77 }
     78 
     79 func TestLookupNS(t *testing.T) {
     80 	if testing.Short() || !*testExternal {
     81 		t.Skip("avoid external network")
     82 	}
     83 
     84 	for _, server := range nslookupTestServers {
     85 		ns, err := LookupNS(server)
     86 		if err != nil {
     87 			t.Errorf("failed %s: %s", server, err)
     88 			continue
     89 		}
     90 		if len(ns) == 0 {
     91 			t.Errorf("no results")
     92 			continue
     93 		}
     94 		expected, err := nslookupNS(server)
     95 		if err != nil {
     96 			t.Logf("skipping failed nslookup %s test: %s", server, err)
     97 			continue
     98 		}
     99 		sort.Sort(byHost(expected))
    100 		sort.Sort(byHost(ns))
    101 		if !reflect.DeepEqual(expected, ns) {
    102 			t.Errorf("different results %s:\texp:%v\tgot:%v", toJson(server), toJson(expected), ns)
    103 		}
    104 	}
    105 }
    106 
    107 func TestLookupTXT(t *testing.T) {
    108 	if testing.Short() || !*testExternal {
    109 		t.Skip("avoid external network")
    110 	}
    111 
    112 	for _, server := range nslookupTestServers {
    113 		txt, err := LookupTXT(server)
    114 		if err != nil {
    115 			t.Errorf("failed %s: %s", server, err)
    116 			continue
    117 		}
    118 		if len(txt) == 0 {
    119 			t.Errorf("no results")
    120 			continue
    121 		}
    122 		expected, err := nslookupTXT(server)
    123 		if err != nil {
    124 			t.Logf("skipping failed nslookup %s test: %s", server, err)
    125 			continue
    126 		}
    127 		sort.Strings(expected)
    128 		sort.Strings(txt)
    129 		if !reflect.DeepEqual(expected, txt) {
    130 			t.Errorf("different results %s:\texp:%v\tgot:%v", server, toJson(expected), toJson(txt))
    131 		}
    132 	}
    133 }
    134 
    135 type byPrefAndHost []*MX
    136 
    137 func (s byPrefAndHost) Len() int { return len(s) }
    138 func (s byPrefAndHost) Less(i, j int) bool {
    139 	if s[i].Pref != s[j].Pref {
    140 		return s[i].Pref < s[j].Pref
    141 	}
    142 	return s[i].Host < s[j].Host
    143 }
    144 func (s byPrefAndHost) Swap(i, j int) { s[i], s[j] = s[j], s[i] }
    145 
    146 type byHost []*NS
    147 
    148 func (s byHost) Len() int           { return len(s) }
    149 func (s byHost) Less(i, j int) bool { return s[i].Host < s[j].Host }
    150 func (s byHost) Swap(i, j int)      { s[i], s[j] = s[j], s[i] }
    151 
    152 func fqdn(s string) string {
    153 	if len(s) == 0 || s[len(s)-1] != '.' {
    154 		return s + "."
    155 	}
    156 	return s
    157 }
    158 
    159 func nslookup(qtype, name string) (string, error) {
    160 	var out bytes.Buffer
    161 	var err bytes.Buffer
    162 	cmd := exec.Command("nslookup", "-querytype="+qtype, name)
    163 	cmd.Stdout = &out
    164 	cmd.Stderr = &err
    165 	if err := cmd.Run(); err != nil {
    166 		return "", err
    167 	}
    168 	r := strings.Replace(out.String(), "\r\n", "\n", -1)
    169 	// nslookup stderr output contains also debug information such as
    170 	// "Non-authoritative answer" and it doesn't return the correct errcode
    171 	if strings.Contains(err.String(), "can't find") {
    172 		return r, errors.New(err.String())
    173 	}
    174 	return r, nil
    175 }
    176 
    177 func nslookupMX(name string) (mx []*MX, err error) {
    178 	var r string
    179 	if r, err = nslookup("mx", name); err != nil {
    180 		return
    181 	}
    182 	mx = make([]*MX, 0, 10)
    183 	// linux nslookup syntax
    184 	// golang.org      mail exchanger = 2 alt1.aspmx.l.google.com.
    185 	rx := regexp.MustCompile(`(?m)^([a-z0-9.\-]+)\s+mail exchanger\s*=\s*([0-9]+)\s*([a-z0-9.\-]+)$`)
    186 	for _, ans := range rx.FindAllStringSubmatch(r, -1) {
    187 		pref, _ := strconv.Atoi(ans[2])
    188 		mx = append(mx, &MX{fqdn(ans[3]), uint16(pref)})
    189 	}
    190 	// windows nslookup syntax
    191 	// gmail.com       MX preference = 30, mail exchanger = alt3.gmail-smtp-in.l.google.com
    192 	rx = regexp.MustCompile(`(?m)^([a-z0-9.\-]+)\s+MX preference\s*=\s*([0-9]+)\s*,\s*mail exchanger\s*=\s*([a-z0-9.\-]+)$`)
    193 	for _, ans := range rx.FindAllStringSubmatch(r, -1) {
    194 		pref, _ := strconv.Atoi(ans[2])
    195 		mx = append(mx, &MX{fqdn(ans[3]), uint16(pref)})
    196 	}
    197 	return
    198 }
    199 
    200 func nslookupNS(name string) (ns []*NS, err error) {
    201 	var r string
    202 	if r, err = nslookup("ns", name); err != nil {
    203 		return
    204 	}
    205 	ns = make([]*NS, 0, 10)
    206 	// golang.org      nameserver = ns1.google.com.
    207 	rx := regexp.MustCompile(`(?m)^([a-z0-9.\-]+)\s+nameserver\s*=\s*([a-z0-9.\-]+)$`)
    208 	for _, ans := range rx.FindAllStringSubmatch(r, -1) {
    209 		ns = append(ns, &NS{fqdn(ans[2])})
    210 	}
    211 	return
    212 }
    213 
    214 func nslookupCNAME(name string) (cname string, err error) {
    215 	var r string
    216 	if r, err = nslookup("cname", name); err != nil {
    217 		return
    218 	}
    219 	// mail.golang.com canonical name = golang.org.
    220 	rx := regexp.MustCompile(`(?m)^([a-z0-9.\-]+)\s+canonical name\s*=\s*([a-z0-9.\-]+)$`)
    221 	// assumes the last CNAME is the correct one
    222 	last := name
    223 	for _, ans := range rx.FindAllStringSubmatch(r, -1) {
    224 		last = ans[2]
    225 	}
    226 	return fqdn(last), nil
    227 }
    228 
    229 func nslookupTXT(name string) (txt []string, err error) {
    230 	var r string
    231 	if r, err = nslookup("txt", name); err != nil {
    232 		return
    233 	}
    234 	txt = make([]string, 0, 10)
    235 	// linux
    236 	// golang.org      text = "v=spf1 redirect=_spf.google.com"
    237 
    238 	// windows
    239 	// golang.org      text =
    240 	//
    241 	//    "v=spf1 redirect=_spf.google.com"
    242 	rx := regexp.MustCompile(`(?m)^([a-z0-9.\-]+)\s+text\s*=\s*"(.*)"$`)
    243 	for _, ans := range rx.FindAllStringSubmatch(r, -1) {
    244 		txt = append(txt, ans[2])
    245 	}
    246 	return
    247 }
    248