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