From 8d4919022dfedf99bdd4cdc1f87de97d72e1c083 Mon Sep 17 00:00:00 2001 From: Shulhan Date: Mon, 15 Nov 2021 03:15:31 +0700 Subject: lib/dns: refactoring ZoneFile into Zone Reason: A Zone is not always represented by file, it just that in this package, it is. This changes rename the type ZoneFile into Zone. --- lib/dns/zone.go | 385 ++++++++++++++++++++++ lib/dns/zone_file.go | 387 ---------------------- lib/dns/zone_file_test.go | 815 ---------------------------------------------- lib/dns/zone_parser.go | 6 +- lib/dns/zone_test.go | 815 ++++++++++++++++++++++++++++++++++++++++++++++ 5 files changed, 1203 insertions(+), 1205 deletions(-) create mode 100644 lib/dns/zone.go delete mode 100644 lib/dns/zone_file.go delete mode 100644 lib/dns/zone_file_test.go create mode 100644 lib/dns/zone_test.go diff --git a/lib/dns/zone.go b/lib/dns/zone.go new file mode 100644 index 00000000..679a75dd --- /dev/null +++ b/lib/dns/zone.go @@ -0,0 +1,385 @@ +// Copyright 2020, Shulhan . All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +package dns + +import ( + "errors" + "fmt" + "os" + "path/filepath" + "sort" + "strings" + + libio "github.com/shuLhan/share/lib/io" +) + +// +// Zone represent a group of domain names shared a single root domain. +// A Zone contains at least one SOA record. +// +type Zone struct { + Records zoneRecords + Path string `json:"-"` + Name string + messages []*Message + SOA ResourceRecord +} + +// +// NewZone create and initialize new zone. +// +func NewZone(file, name string) *Zone { + return &Zone{ + Path: file, + Name: name, + SOA: ResourceRecord{ + Type: RecordTypeSOA, + Value: &RDataSOA{ + MName: name, + }, + }, + Records: make(zoneRecords), + } +} + +// +// LoadZoneDir load DNS record from zone formatted files in +// directory "dir". +// On success, it will return map of file name and Zone content as list +// of Message. +// On fail, it will return possible partially parse zone file and an error. +// +func LoadZoneDir(dir string) (zoneFiles map[string]*Zone, err error) { + if len(dir) == 0 { + return nil, nil + } + + d, err := os.Open(dir) + if err != nil { + return nil, fmt.Errorf("LoadZoneDir: %w", err) + } + + fis, err := d.Readdir(0) + if err != nil { + err = d.Close() + if err != nil { + return nil, fmt.Errorf("LoadZoneDir: %w", err) + } + return nil, fmt.Errorf("LoadZoneDir: %w", err) + } + + zoneFiles = make(map[string]*Zone) + + for x := 0; x < len(fis); x++ { + if fis[x].IsDir() { + continue + } + + // Ignore file that start with "." . + name := fis[x].Name() + if name[0] == '.' { + continue + } + + zoneFilePath := filepath.Join(dir, name) + + zoneFile, err := ParseZoneFile(zoneFilePath, "", 0) + if err != nil { + return zoneFiles, fmt.Errorf("LoadZoneDir %q: %w", dir, err) + } + + zoneFiles[name] = zoneFile + } + + err = d.Close() + if err != nil { + return zoneFiles, fmt.Errorf(" LoadZoneDir %q: %w", dir, err) + } + + return zoneFiles, nil +} + +// +// ParseZoneFile parse zone file. +// The file name will be assumed as origin if parameter origin or $ORIGIN is +// not set. +// +func ParseZoneFile(file, origin string, ttl uint32) (*Zone, error) { + var err error + + m := newZoneParser(file) + m.ttl = ttl + + if len(origin) > 0 { + m.origin = origin + } else { + m.origin = filepath.Base(file) + } + + m.origin = strings.ToLower(m.origin) + + m.reader, err = libio.NewReader(file) + if err != nil { + return nil, fmt.Errorf("ParseZone %q: %w", file, err) + } + + err = m.parse() + if err != nil { + return nil, fmt.Errorf("ParseZone %q: %w", file, err) + } + + m.zone.Name = m.origin + + zone := m.zone + m.zone = nil + return zone, nil +} + +// +// Add add new ResourceRecord to Zone. +// +func (zone *Zone) Add(rr *ResourceRecord) (err error) { + if rr.Type == RecordTypeSOA { + zone.SOA = *rr + } else { + zone.Records.add(rr) + } + + for _, msg := range zone.messages { + if msg.Question.Name != rr.Name { + continue + } + if msg.Question.Type != rr.Type { + continue + } + if msg.Question.Class != rr.Class { + continue + } + return msg.AddAnswer(rr) + } + + msg := &Message{ + Header: MessageHeader{ + IsAA: true, + QDCount: 1, + ANCount: 1, + }, + Question: MessageQuestion{ + Name: rr.Name, + Type: rr.Type, + Class: rr.Class, + }, + Answer: []ResourceRecord{*rr}, + } + zone.messages = append(zone.messages, msg) + return nil +} + +// +// Delete the zone file from storage. +// +func (zone *Zone) Delete() (err error) { + return os.Remove(zone.Path) +} + +// +// Messages return all pre-generated DNS messages. +// +func (zone *Zone) Messages() []*Message { + return zone.messages +} + +// +// Remove a ResourceRecord from zone file. +// +func (zone *Zone) Remove(rr *ResourceRecord) (err error) { + if rr.Type == RecordTypeSOA { + zone.SOA = ResourceRecord{ + Type: RecordTypeSOA, + Value: &RDataSOA{}, + } + } else { + if zone.Records.remove(rr) { + err = zone.Save() + } + } + return err +} + +// +// Save the content of zone records to file defined by Path. +// +func (zone *Zone) Save() (err error) { + out, err := os.OpenFile(zone.Path, os.O_RDWR|os.O_CREATE|os.O_TRUNC, + 0600) + if err != nil { + return err + } + + var ( + names []string + listRR []*ResourceRecord + ) + + fmt.Fprintf(out, "$ORIGIN %s.\n", zone.Name) + + soa, ok := zone.SOA.Value.(*RDataSOA) + if ok && len(soa.MName) > 0 { + _, err = fmt.Fprintf(out, + "@ SOA %s. %s. %d %d %d %d %d\n", + soa.MName, soa.RName, soa.Serial, soa.Refresh, + soa.Retry, soa.Expire, soa.Minimum) + if err != nil { + goto out + } + } + + // Save the origin records first. + listRR = zone.Records[zone.Name] + if len(listRR) > 0 { + err = zone.saveListRR(out, "@", listRR) + if err != nil { + goto out + } + } + + // Save the records ordered by name. + names = make([]string, 0, len(zone.Records)) + for dname := range zone.Records { + if dname == zone.Name { + continue + } + names = append(names, dname) + } + sort.Strings(names) + + for _, dname := range names { + listRR := zone.Records[dname] + dname = strings.TrimSuffix(dname, "."+zone.Name) + err = zone.saveListRR(out, dname, listRR) + if err != nil { + break + } + } +out: + errc := out.Close() + if errc != nil { + if err == nil { + err = errc + } + } + return err +} + +func (zone *Zone) saveListRR(out *os.File, dname string, listRR []*ResourceRecord) (err error) { + for x, rr := range listRR { + if x > 0 { + dname = "\t" + } + switch rr.Type { + case RecordTypeA, RecordTypeNULL, RecordTypeAAAA: + _, err = fmt.Fprintf(out, "%s %d %s %s %s\n", + dname, rr.TTL, RecordClassName[rr.Class], + RecordTypeNames[rr.Type], rr.Value.(string)) + + case RecordTypeTXT: + _, err = fmt.Fprintf(out, "%s %d %s %s %q\n", + dname, rr.TTL, RecordClassName[rr.Class], + RecordTypeNames[rr.Type], rr.Value.(string)) + + case RecordTypeNS, RecordTypeCNAME, RecordTypeMB, + RecordTypeMG, RecordTypeMR: + v, ok := rr.Value.(string) + if !ok { + err = errors.New("invalid record value for " + + RecordTypeNames[rr.Type]) + break + } + if strings.HasSuffix(v, zone.Name) { + v = strings.TrimSuffix(v, "."+zone.Name) + } else { + v += "." + } + _, err = fmt.Fprintf(out, "%s %d %s %s %s\n", + dname, rr.TTL, RecordClassName[rr.Class], + RecordTypeNames[rr.Type], v) + + case RecordTypePTR: + v, ok := rr.Value.(string) + if !ok { + err = errors.New("invalid record value for " + + RecordTypeNames[rr.Type]) + break + } + if strings.HasSuffix(v, zone.Name) { + v = strings.TrimSuffix(v, "."+zone.Name) + } else { + v += "." + } + _, err = fmt.Fprintf(out, "%s. %d IN PTR %s\n", + rr.Name, rr.TTL, v) + + case RecordTypeWKS: + wks, ok := rr.Value.(*RDataWKS) + if !ok { + err = errors.New("invalid record value for WKS") + break + } + _, err = fmt.Fprintf(out, + "%s %d %s WKS %s %d %s\n", + dname, rr.TTL, RecordClassName[rr.Class], + wks.Address, wks.Protocol, wks.BitMap) + + case RecordTypeHINFO: + hinfo, ok := rr.Value.(*RDataHINFO) + if !ok { + err = errors.New("invalid record value for HINFO") + break + } + _, err = fmt.Fprintf(out, + "%s %d %s HINFO %s %s\n", + dname, rr.TTL, RecordClassName[rr.Class], + hinfo.CPU, hinfo.OS) + + case RecordTypeMINFO: + minfo, ok := rr.Value.(*RDataMINFO) + if !ok { + err = errors.New("invalid record value for MINFO") + break + } + _, err = fmt.Fprintf(out, + "%s %d %s MINFO %s %s\n", + dname, rr.TTL, RecordClassName[rr.Class], + minfo.RMailBox, minfo.EmailBox) + + case RecordTypeMX: + mx, ok := rr.Value.(*RDataMX) + if !ok { + err = errors.New("invalid record value for MX") + break + } + _, err = fmt.Fprintf(out, + "%s %d %s MX %d %s.\n", + dname, rr.TTL, RecordClassName[rr.Class], + mx.Preference, mx.Exchange) + + case RecordTypeSRV: + srv, ok := rr.Value.(*RDataSRV) + if !ok { + err = errors.New("invalid record value for SRV") + break + } + _, err = fmt.Fprintf(out, + "%s %d %s SRV %d %d %d %s.\n", + dname, rr.TTL, RecordClassName[rr.Class], + srv.Priority, srv.Weight, + srv.Port, srv.Target) + } + if err != nil { + return err + } + } + return nil +} diff --git a/lib/dns/zone_file.go b/lib/dns/zone_file.go deleted file mode 100644 index 4e9d33cf..00000000 --- a/lib/dns/zone_file.go +++ /dev/null @@ -1,387 +0,0 @@ -// Copyright 2020, Shulhan . All rights reserved. -// Use of this source code is governed by a BSD-style -// license that can be found in the LICENSE file. - -package dns - -import ( - "errors" - "fmt" - "os" - "path/filepath" - "sort" - "strings" - - libio "github.com/shuLhan/share/lib/io" -) - -// -// ZoneFile represent content of single zone file. -// A zone file contains at least one SOA record. -// -type ZoneFile struct { - Records zoneRecords - Path string `json:"-"` - Name string - messages []*Message - SOA ResourceRecord -} - -// -// NewZoneFile create and initialize new zone file. -// -func NewZoneFile(file, name string) *ZoneFile { - return &ZoneFile{ - Path: file, - Name: name, - SOA: ResourceRecord{ - Type: RecordTypeSOA, - Value: &RDataSOA{ - MName: name, - }, - }, - Records: make(zoneRecords), - } -} - -// -// LoadZoneDir load DNS record from zone formatted files in -// directory "dir". -// On success, it will return map of file name and ZoneFile content as list -// of Message. -// On fail, it will return possible partially parse zone file and an error. -// -func LoadZoneDir(dir string) (zoneFiles map[string]*ZoneFile, err error) { - if len(dir) == 0 { - return nil, nil - } - - d, err := os.Open(dir) - if err != nil { - return nil, fmt.Errorf("LoadZoneDir: %w", err) - } - - fis, err := d.Readdir(0) - if err != nil { - err = d.Close() - if err != nil { - return nil, fmt.Errorf("LoadZoneDir: %w", err) - } - return nil, fmt.Errorf("LoadZoneDir: %w", err) - } - - zoneFiles = make(map[string]*ZoneFile) - - for x := 0; x < len(fis); x++ { - if fis[x].IsDir() { - continue - } - - // Ignore file that start with "." . - name := fis[x].Name() - if name[0] == '.' { - continue - } - - zoneFilePath := filepath.Join(dir, name) - - zoneFile, err := ParseZoneFile(zoneFilePath, "", 0) - if err != nil { - return zoneFiles, fmt.Errorf("LoadZoneDir %q: %w", dir, err) - } - - zoneFiles[name] = zoneFile - } - - err = d.Close() - if err != nil { - return zoneFiles, fmt.Errorf(" LoadZoneDir %q: %w", dir, err) - } - - return zoneFiles, nil -} - -// -// ParseZoneFile parse zone file and return it as list of Message. -// The file name will be assumed as origin if parameter origin or $ORIGIN is -// not set. -// -func ParseZoneFile(file, origin string, ttl uint32) (*ZoneFile, error) { - var err error - - m := newZoneParser(file) - m.ttl = ttl - - if len(origin) > 0 { - m.origin = origin - } else { - m.origin = filepath.Base(file) - } - - m.origin = strings.ToLower(m.origin) - - m.reader, err = libio.NewReader(file) - if err != nil { - return nil, fmt.Errorf("ParseZoneFile %q: %w", file, err) - } - - err = m.parse() - if err != nil { - return nil, fmt.Errorf("ParseZoneFile %q: %w", file, err) - } - - m.zone.Name = m.origin - - zone := m.zone - m.zone = nil - return zone, nil -} - -// -// Add add new ResourceRecord to ZoneFile. -// -func (zone *ZoneFile) Add(rr *ResourceRecord) (err error) { - if rr.Type == RecordTypeSOA { - zone.SOA = *rr - } else { - zone.Records.add(rr) - } - - for _, msg := range zone.messages { - if msg.Question.Name != rr.Name { - continue - } - if msg.Question.Type != rr.Type { - continue - } - if msg.Question.Class != rr.Class { - continue - } - return msg.AddAnswer(rr) - } - - msg := &Message{ - Header: MessageHeader{ - IsAA: true, - QDCount: 1, - ANCount: 1, - }, - Question: MessageQuestion{ - Name: rr.Name, - Type: rr.Type, - Class: rr.Class, - }, - Answer: []ResourceRecord{*rr}, - } - zone.messages = append(zone.messages, msg) - return nil -} - -// -// Delete the zone file from storage. -// -func (zone *ZoneFile) Delete() (err error) { - return os.Remove(zone.Path) -} - -// -// Messages return all pre-generated DNS messages. -// -func (zone *ZoneFile) Messages() []*Message { - return zone.messages -} - -// -// Remove a ResourceRecord from zone file. -// -func (zone *ZoneFile) Remove(rr *ResourceRecord) (err error) { - if rr.Type == RecordTypeSOA { - zone.SOA = ResourceRecord{ - Type: RecordTypeSOA, - Value: &RDataSOA{}, - } - } else { - if zone.Records.remove(rr) { - err = zone.Save() - } - } - return err -} - -// -// Save the content of zone records to file defined by path. -// -func (zone *ZoneFile) Save() (err error) { - out, err := os.OpenFile(zone.Path, os.O_RDWR|os.O_CREATE|os.O_TRUNC, - 0600) - if err != nil { - return err - } - - var ( - names []string - listRR []*ResourceRecord - ) - - fmt.Fprintf(out, "$ORIGIN %s.\n", zone.Name) - - soa, ok := zone.SOA.Value.(*RDataSOA) - if ok && len(soa.MName) > 0 { - _, err = fmt.Fprintf(out, - "@ SOA %s. %s. %d %d %d %d %d\n", - soa.MName, soa.RName, soa.Serial, soa.Refresh, - soa.Retry, soa.Expire, soa.Minimum) - if err != nil { - goto out - } - } - - // Save the origin records first. - listRR = zone.Records[zone.Name] - if len(listRR) > 0 { - err = zone.saveListRR(out, "@", listRR) - if err != nil { - goto out - } - } - - // Save the records ordered by name. - names = make([]string, 0, len(zone.Records)) - for dname := range zone.Records { - if dname == zone.Name { - continue - } - names = append(names, dname) - } - sort.Strings(names) - - for _, dname := range names { - listRR := zone.Records[dname] - dname = strings.TrimSuffix(dname, "."+zone.Name) - err = zone.saveListRR(out, dname, listRR) - if err != nil { - break - } - } -out: - errc := out.Close() - if errc != nil { - if err == nil { - err = errc - } - } - return err -} - -func (zone *ZoneFile) saveListRR( - out *os.File, dname string, listRR []*ResourceRecord, -) (err error) { - for x, rr := range listRR { - if x > 0 { - dname = "\t" - } - switch rr.Type { - case RecordTypeA, RecordTypeNULL, RecordTypeAAAA: - _, err = fmt.Fprintf(out, "%s %d %s %s %s\n", - dname, rr.TTL, RecordClassName[rr.Class], - RecordTypeNames[rr.Type], rr.Value.(string)) - - case RecordTypeTXT: - _, err = fmt.Fprintf(out, "%s %d %s %s %q\n", - dname, rr.TTL, RecordClassName[rr.Class], - RecordTypeNames[rr.Type], rr.Value.(string)) - - case RecordTypeNS, RecordTypeCNAME, RecordTypeMB, - RecordTypeMG, RecordTypeMR: - v, ok := rr.Value.(string) - if !ok { - err = errors.New("invalid record value for " + - RecordTypeNames[rr.Type]) - break - } - if strings.HasSuffix(v, zone.Name) { - v = strings.TrimSuffix(v, "."+zone.Name) - } else { - v += "." - } - _, err = fmt.Fprintf(out, "%s %d %s %s %s\n", - dname, rr.TTL, RecordClassName[rr.Class], - RecordTypeNames[rr.Type], v) - - case RecordTypePTR: - v, ok := rr.Value.(string) - if !ok { - err = errors.New("invalid record value for " + - RecordTypeNames[rr.Type]) - break - } - if strings.HasSuffix(v, zone.Name) { - v = strings.TrimSuffix(v, "."+zone.Name) - } else { - v += "." - } - _, err = fmt.Fprintf(out, "%s. %d IN PTR %s\n", - rr.Name, rr.TTL, v) - - case RecordTypeWKS: - wks, ok := rr.Value.(*RDataWKS) - if !ok { - err = errors.New("invalid record value for WKS") - break - } - _, err = fmt.Fprintf(out, - "%s %d %s WKS %s %d %s\n", - dname, rr.TTL, RecordClassName[rr.Class], - wks.Address, wks.Protocol, wks.BitMap) - - case RecordTypeHINFO: - hinfo, ok := rr.Value.(*RDataHINFO) - if !ok { - err = errors.New("invalid record value for HINFO") - break - } - _, err = fmt.Fprintf(out, - "%s %d %s HINFO %s %s\n", - dname, rr.TTL, RecordClassName[rr.Class], - hinfo.CPU, hinfo.OS) - - case RecordTypeMINFO: - minfo, ok := rr.Value.(*RDataMINFO) - if !ok { - err = errors.New("invalid record value for MINFO") - break - } - _, err = fmt.Fprintf(out, - "%s %d %s MINFO %s %s\n", - dname, rr.TTL, RecordClassName[rr.Class], - minfo.RMailBox, minfo.EmailBox) - - case RecordTypeMX: - mx, ok := rr.Value.(*RDataMX) - if !ok { - err = errors.New("invalid record value for MX") - break - } - _, err = fmt.Fprintf(out, - "%s %d %s MX %d %s.\n", - dname, rr.TTL, RecordClassName[rr.Class], - mx.Preference, mx.Exchange) - - case RecordTypeSRV: - srv, ok := rr.Value.(*RDataSRV) - if !ok { - err = errors.New("invalid record value for SRV") - break - } - _, err = fmt.Fprintf(out, - "%s %d %s SRV %d %d %d %s.\n", - dname, rr.TTL, RecordClassName[rr.Class], - srv.Priority, srv.Weight, - srv.Port, srv.Target) - } - if err != nil { - return err - } - } - return nil -} diff --git a/lib/dns/zone_file_test.go b/lib/dns/zone_file_test.go deleted file mode 100644 index 38f1d142..00000000 --- a/lib/dns/zone_file_test.go +++ /dev/null @@ -1,815 +0,0 @@ -// Copyright 2018, Shulhan . All rights reserved. -// Use of this source code is governed by a BSD-style -// license that can be found in the LICENSE file. - -package dns - -import ( - "testing" - - "github.com/shuLhan/share/lib/test" -) - -func TestZoneParseDirectiveOrigin(t *testing.T) { - cases := []struct { - desc string - in string - expErr string - exp string - }{{ - desc: "Without value", - in: `$origin`, - expErr: "line 1: empty $origin directive", - }, { - desc: "Without value and with comment", - in: `$origin ; comment`, - expErr: "line 1: empty $origin directive", - }, { - desc: "With value", - in: `$origin x`, - exp: "x", - }, { - desc: "With value and comment", - in: `$origin x ;comment`, - exp: "x", - }} - - m := newZoneParser("") - - for _, c := range cases { - t.Log(c.desc) - - m.Init(c.in, "", 0) - - err := m.parse() - if err != nil { - test.Assert(t, "err", c.expErr, err.Error()) - continue - } - - test.Assert(t, "origin", c.exp, m.origin) - } -} - -func TestZoneParseDirectiveInclude(t *testing.T) { - cases := []struct { - desc string - in string - expErr string - }{{ - desc: "Without value", - in: `$include`, - expErr: "line 1: empty $include directive", - }, { - desc: "Without value and with comment", - in: `$include ; comment`, - expErr: "line 1: empty $include directive", - }, { - desc: "With value", - in: `$include testdata/sub.domain`, - }, { - desc: "With value and comment", - in: `$origin testdata/sub.domain ;comment`, - }} - - m := newZoneParser("") - - for _, c := range cases { - t.Log(c.desc) - - m.Init(c.in, "", 0) - - err := m.parse() - if err != nil { - test.Assert(t, "err", c.expErr, err.Error()) - continue - } - } -} - -func TestZoneParseDirectiveTTL(t *testing.T) { - cases := []struct { - desc string - in string - expErr string - exp uint32 - }{{ - desc: "Without value", - in: `$ttl`, - expErr: "line 1: empty $TTL directive", - }, { - desc: "Without value and with comment", - in: `$ttl ; comment`, - expErr: "line 1: empty $TTL directive", - }, { - desc: "With value", - in: `$ttl 1`, - exp: 1, - }, { - desc: "With value and comment", - in: `$ttl 1 ;comment`, - exp: 1, - }} - - m := newZoneParser("") - - for _, c := range cases { - t.Log(c.desc) - - m.Init(c.in, "", 0) - - err := m.parse() - if err != nil { - test.Assert(t, "err", c.expErr, err.Error()) - continue - } - - test.Assert(t, "ttl", c.exp, m.ttl) - } -} - -func TestZoneInitRFC1035(t *testing.T) { - type caseZoneInit struct { - expErr error - desc string - origin string - in string - exp []*Message - ttl uint32 - } - - cases := []caseZoneInit{{ - desc: "RFC1035 section 5.3", - origin: "ISI.EDU", - ttl: 3600, - in: ` -@ IN SOA VENERA Action\.domains ( - 20 ; SERIAL - 7200 ; REFRESH - 600 ; RETRY - 3600000; EXPIRE - 60) ; MINIMUM - - NS A.ISI.EDU. - NS VENERA - NS VAXA - MX 10 VENERA - MX 20 VAXA - -A A 26.3.0.103 - -VENERA A 10.1.0.52 - A 128.9.0.32 - -VAXA A 10.2.0.27 - A 128.9.0.33 - -`, - exp: []*Message{{ - Header: MessageHeader{ - IsAA: true, - QDCount: 1, - ANCount: 1, - }, - Question: MessageQuestion{ - Name: "isi.edu", - Type: RecordTypeSOA, - Class: RecordClassIN, - }, - Answer: []ResourceRecord{{ - Name: "isi.edu", - Type: RecordTypeSOA, - Class: RecordClassIN, - TTL: 3600, - Value: &RDataSOA{ - MName: "venera.isi.edu", - RName: "action\\.domains.isi.edu", - Serial: 20, - Refresh: 7200, - Retry: 600, - Expire: 3600000, - Minimum: 60, - }, - }}, - }, { - Header: MessageHeader{ - IsAA: true, - QDCount: 1, - ANCount: 3, - }, - Question: MessageQuestion{ - Name: "isi.edu", - Type: RecordTypeNS, - Class: RecordClassIN, - }, - Answer: []ResourceRecord{{ - Name: "isi.edu", - Type: RecordTypeNS, - Class: RecordClassIN, - TTL: 3600, - Value: "a.isi.edu", - }, { - Name: "isi.edu", - Type: RecordTypeNS, - Class: RecordClassIN, - TTL: 3600, - Value: "venera.isi.edu", - }, { - Name: "isi.edu", - Type: RecordTypeNS, - Class: RecordClassIN, - TTL: 3600, - Value: "vaxa.isi.edu", - }}, - }, { - Header: MessageHeader{ - IsAA: true, - QDCount: 1, - ANCount: 2, - }, - Question: MessageQuestion{ - Name: "isi.edu", - Type: RecordTypeMX, - Class: RecordClassIN, - }, - Answer: []ResourceRecord{{ - Name: "isi.edu", - Type: RecordTypeMX, - Class: RecordClassIN, - TTL: 3600, - Value: &RDataMX{ - Preference: 10, - Exchange: "venera.isi.edu", - }, - }, { - Name: "isi.edu", - Type: RecordTypeMX, - Class: RecordClassIN, - TTL: 3600, - Value: &RDataMX{ - Preference: 20, - Exchange: "vaxa.isi.edu", - }, - }}, - }, { - Header: MessageHeader{ - IsAA: true, - QDCount: 1, - ANCount: 1, - }, - Question: MessageQuestion{ - Name: "a.isi.edu", - Type: RecordTypeA, - Class: RecordClassIN, - }, - Answer: []ResourceRecord{{ - Name: "a.isi.edu", - Type: RecordTypeA, - Class: RecordClassIN, - TTL: 3600, - Value: "26.3.0.103", - }}, - }, { - Header: MessageHeader{ - IsAA: true, - QDCount: 1, - ANCount: 2, - }, - Question: MessageQuestion{ - Name: "venera.isi.edu", - Type: RecordTypeA, - Class: RecordClassIN, - }, - Answer: []ResourceRecord{{ - Name: "venera.isi.edu", - Type: RecordTypeA, - Class: RecordClassIN, - TTL: 3600, - Value: "10.1.0.52", - }, { - Name: "venera.isi.edu", - Type: RecordTypeA, - Class: RecordClassIN, - TTL: 3600, - Value: "128.9.0.32", - }}, - }, { - Header: MessageHeader{ - IsAA: true, - QDCount: 1, - ANCount: 2, - }, - Question: MessageQuestion{ - Name: "vaxa.isi.edu", - Type: RecordTypeA, - Class: RecordClassIN, - }, - Answer: []ResourceRecord{{ - Name: "vaxa.isi.edu", - Type: RecordTypeA, - Class: RecordClassIN, - TTL: 3600, - Value: "10.2.0.27", - }, { - Name: "vaxa.isi.edu", - Type: RecordTypeA, - Class: RecordClassIN, - TTL: 3600, - Value: "128.9.0.33", - }}, - }}, - }} - - m := newZoneParser("") - - for _, c := range cases { - t.Log(c.desc) - - m.Init(c.in, c.origin, c.ttl) - - err := m.parse() - if err != nil { - test.Assert(t, "err", c.expErr, err.Error()) - continue - } - - test.Assert(t, "messages length:", - len(c.exp), len(m.zone.messages)) - - for x, msg := range m.zone.messages { - test.Assert(t, "Message.Header", c.exp[x].Header, msg.Header) - test.Assert(t, "Message.Question", c.exp[x].Question, msg.Question) - - for y, answer := range msg.Answer { - test.Assert(t, "Answer.Name", c.exp[x].Answer[y].Name, answer.Name) - test.Assert(t, "Answer.Type", c.exp[x].Answer[y].Type, answer.Type) - test.Assert(t, "Answer.Class", c.exp[x].Answer[y].Class, answer.Class) - test.Assert(t, "Answer.TTL", c.exp[x].Answer[y].TTL, answer.TTL) - test.Assert(t, "Answer.Value", c.exp[x].Answer[y].Value, answer.Value) - } - for y, auth := range msg.Authority { - test.Assert(t, "Authority.Name", c.exp[x].Authority[y].Name, auth.Name) - test.Assert(t, "Authority.Type", c.exp[x].Authority[y].Type, auth.Type) - test.Assert(t, "Authority.Class", c.exp[x].Authority[y].Class, auth.Class) - test.Assert(t, "Authority.TTL", c.exp[x].Authority[y].TTL, auth.TTL) - test.Assert(t, "Authority.Value", c.exp[x].Authority[y].Value, auth.Value) - } - for y, add := range msg.Additional { - test.Assert(t, "Additional.Name", c.exp[x].Additional[y].Name, add.Name) - test.Assert(t, "Additional.Type", c.exp[x].Additional[y].Type, add.Type) - test.Assert(t, "Additional.Class", c.exp[x].Additional[y].Class, add.Class) - test.Assert(t, "Additional.TTL", c.exp[x].Additional[y].TTL, add.TTL) - test.Assert(t, "Additional.Value", c.exp[x].Additional[y].Value, add.Value) - } - } - } -} - -func TestZoneInit2(t *testing.T) { - cases := []struct { - expErr error - desc string - origin string - in string - exp []*Message - ttl uint32 - }{{ - desc: "From http://www.tcpipguide.com/free/t_DNSZoneFileFormat-4.htm", - in: ` -$ORIGIN pcguide.com. -@ IN SOA ns23.pair.com. root.pair.com. ( -2001072300 ; Serial -3600 ; Refresh -300 ; Retry -604800 ; Expire -3600 ) ; Minimum - -@ IN NS ns23.pair.com. -@ IN NS ns0.ns0.com. - -localhost IN A 127.0.0.1 -@ IN A 209.68.14.80 - IN MX 50 qs939.pair.com. - -www IN CNAME @ -ftp IN CNAME @ -mail IN CNAME @ -relay IN CNAME relay.pair.com. -`, - exp: []*Message{{ - Header: MessageHeader{ - IsAA: true, - QDCount: 1, - ANCount: 1, - }, - Question: MessageQuestion{ - Name: "pcguide.com", - Type: RecordTypeSOA, - Class: RecordClassIN, - }, - Answer: []ResourceRecord{{ - Name: "pcguide.com", - Type: RecordTypeSOA, - Class: RecordClassIN, - TTL: 3600, - Value: &RDataSOA{ - MName: "ns23.pair.com", - RName: "root.pair.com", - Serial: 2001072300, - Refresh: 3600, - Retry: 300, - Expire: 604800, - Minimum: 3600, - }, - }}, - }, { - Header: MessageHeader{ - IsAA: true, - QDCount: 1, - ANCount: 2, - }, - Question: MessageQuestion{ - Name: "pcguide.com", - Type: RecordTypeNS, - Class: RecordClassIN, - }, - Answer: []ResourceRecord{{ - Name: "pcguide.com", - Type: RecordTypeNS, - Class: RecordClassIN, - TTL: 3600, - Value: "ns23.pair.com", - }, { - Name: "pcguide.com", - Type: RecordTypeNS, - Class: RecordClassIN, - TTL: 3600, - Value: "ns0.ns0.com", - }}, - }, { - Header: MessageHeader{ - IsAA: true, - QDCount: 1, - ANCount: 1, - }, - Question: MessageQuestion{ - Name: "localhost.pcguide.com", - Type: RecordTypeA, - Class: RecordClassIN, - }, - Answer: []ResourceRecord{{ - Name: "localhost.pcguide.com", - Type: RecordTypeA, - Class: RecordClassIN, - TTL: 3600, - Value: "127.0.0.1", - }}, - }, { - Header: MessageHeader{ - IsAA: true, - QDCount: 1, - ANCount: 1, - }, - Question: MessageQuestion{ - Name: "pcguide.com", - Type: RecordTypeA, - Class: RecordClassIN, - }, - Answer: []ResourceRecord{{ - Name: "pcguide.com", - Type: RecordTypeA, - Class: RecordClassIN, - TTL: 3600, - Value: "209.68.14.80", - }}, - }, { - Header: MessageHeader{ - IsAA: true, - QDCount: 1, - ANCount: 1, - }, - Question: MessageQuestion{ - Name: "pcguide.com", - Type: RecordTypeMX, - Class: RecordClassIN, - }, - Answer: []ResourceRecord{{ - Name: "pcguide.com", - Type: RecordTypeMX, - Class: RecordClassIN, - TTL: 3600, - Value: &RDataMX{ - Preference: 50, - Exchange: "qs939.pair.com", - }, - }}, - }, { - Header: MessageHeader{ - IsAA: true, - QDCount: 1, - ANCount: 1, - }, - Question: MessageQuestion{ - Name: "www.pcguide.com", - Type: RecordTypeCNAME, - Class: RecordClassIN, - }, - Answer: []ResourceRecord{{ - Name: "www.pcguide.com", - Type: RecordTypeCNAME, - Class: RecordClassIN, - TTL: 3600, - Value: "pcguide.com", - }}, - }, { - Header: MessageHeader{ - IsAA: true, - QDCount: 1, - ANCount: 1, - }, - Question: MessageQuestion{ - Name: "ftp.pcguide.com", - Type: RecordTypeCNAME, - Class: RecordClassIN, - }, - Answer: []ResourceRecord{{ - Name: "ftp.pcguide.com", - Type: RecordTypeCNAME, - Class: RecordClassIN, - TTL: 3600, - Value: "pcguide.com", - }}, - }, { - Header: MessageHeader{ - IsAA: true, - QDCount: 1, - ANCount: 1, - }, - Question: MessageQuestion{ - Name: "mail.pcguide.com", - Type: RecordTypeCNAME, - Class: RecordClassIN, - }, - Answer: []ResourceRecord{{ - Name: "mail.pcguide.com", - Type: RecordTypeCNAME, - Class: RecordClassIN, - TTL: 3600, - Value: "pcguide.com", - }}, - }, { - Header: MessageHeader{ - IsAA: true, - QDCount: 1, - ANCount: 1, - }, - Question: MessageQuestion{ - Name: "relay.pcguide.com", - Type: RecordTypeCNAME, - Class: RecordClassIN, - }, - Answer: []ResourceRecord{{ - Name: "relay.pcguide.com", - Type: RecordTypeCNAME, - Class: RecordClassIN, - TTL: 3600, - Value: "relay.pair.com", - }}, - }}, - }} - - m := newZoneParser("") - - for _, c := range cases { - t.Log(c.desc) - - m.Init(c.in, c.origin, c.ttl) - - err := m.parse() - if err != nil { - test.Assert(t, "err", c.expErr, err.Error()) - continue - } - - test.Assert(t, "messages length:", len(c.exp), - len(m.zone.messages)) - - for x, msg := range m.zone.messages { - test.Assert(t, "Message.Header", c.exp[x].Header, msg.Header) - test.Assert(t, "Message.Question", c.exp[x].Question, msg.Question) - - for y, answer := range msg.Answer { - test.Assert(t, "Answer.Name", c.exp[x].Answer[y].Name, answer.Name) - test.Assert(t, "Answer.Type", c.exp[x].Answer[y].Type, answer.Type) - test.Assert(t, "Answer.Class", c.exp[x].Answer[y].Class, answer.Class) - test.Assert(t, "Answer.TTL", c.exp[x].Answer[y].TTL, answer.TTL) - test.Assert(t, "Answer.Value", c.exp[x].Answer[y].Value, answer.Value) - } - for y, auth := range msg.Authority { - test.Assert(t, "Authority.Name", c.exp[x].Authority[y].Name, auth.Name) - test.Assert(t, "Authority.Type", c.exp[x].Authority[y].Type, auth.Type) - test.Assert(t, "Authority.Class", c.exp[x].Authority[y].Class, auth.Class) - test.Assert(t, "Authority.TTL", c.exp[x].Authority[y].TTL, auth.TTL) - test.Assert(t, "Authority.Value", c.exp[x].Authority[y].Value, auth.Value) - } - for y, add := range msg.Additional { - test.Assert(t, "Additional.Name", c.exp[x].Additional[y].Name, add.Name) - test.Assert(t, "Additional.Type", c.exp[x].Additional[y].Type, add.Type) - test.Assert(t, "Additional.Class", c.exp[x].Additional[y].Class, add.Class) - test.Assert(t, "Additional.TTL", c.exp[x].Additional[y].TTL, add.TTL) - test.Assert(t, "Additional.Value", c.exp[x].Additional[y].Value, add.Value) - } - } - } -} - -func TestZoneInit3(t *testing.T) { - cases := []struct { - expErr error - desc string - origin string - in string - exp []*Message - ttl uint32 - }{{ - desc: "From http://www.tcpipguide.com/free/t_DNSZoneFileFormat-4.htm", - origin: "localdomain", - in: ` -; Applications. -dev.kilabit.info. A 127.0.0.1 -dev.kilabit.com. A 127.0.0.1 - -; Documentations. -angularjs.doc A 127.0.0.1 -`, - exp: []*Message{{ - Header: MessageHeader{ - IsAA: true, - QDCount: 1, - ANCount: 1, - }, - Question: MessageQuestion{ - Name: "dev.kilabit.info", - Type: RecordTypeA, - Class: RecordClassIN, - }, - Answer: []ResourceRecord{{ - Name: "dev.kilabit.info", - Type: RecordTypeA, - Class: RecordClassIN, - TTL: 3600, - Value: "127.0.0.1", - }}, - }, { - Header: MessageHeader{ - IsAA: true, - QDCount: 1, - ANCount: 1, - }, - Question: MessageQuestion{ - Name: "dev.kilabit.com", - Type: RecordTypeA, - Class: RecordClassIN, - }, - Answer: []ResourceRecord{{ - Name: "dev.kilabit.com", - Type: RecordTypeA, - Class: RecordClassIN, - TTL: 3600, - Value: "127.0.0.1", - }}, - }, { - Header: MessageHeader{ - IsAA: true, - QDCount: 1, - ANCount: 1, - }, - Question: MessageQuestion{ - Name: "angularjs.doc.localdomain", - Type: RecordTypeA, - Class: RecordClassIN, - }, - Answer: []ResourceRecord{{ - Name: "angularjs.doc.localdomain", - Type: RecordTypeA, - Class: RecordClassIN, - TTL: 3600, - Value: "127.0.0.1", - }}, - }}, - }} - - m := newZoneParser("") - - for _, c := range cases { - t.Log(c.desc) - - m.Init(c.in, c.origin, c.ttl) - - err := m.parse() - if err != nil { - test.Assert(t, "err", c.expErr, err.Error()) - continue - } - - test.Assert(t, "messages length:", len(c.exp), - len(m.zone.messages)) - - for x, msg := range m.zone.messages { - test.Assert(t, "Message.Header", c.exp[x].Header, msg.Header) - test.Assert(t, "Message.Question", c.exp[x].Question, msg.Question) - - for y, answer := range msg.Answer { - test.Assert(t, "Answer.Name", c.exp[x].Answer[y].Name, answer.Name) - test.Assert(t, "Answer.Type", c.exp[x].Answer[y].Type, answer.Type) - test.Assert(t, "Answer.Class", c.exp[x].Answer[y].Class, answer.Class) - test.Assert(t, "Answer.TTL", c.exp[x].Answer[y].TTL, answer.TTL) - test.Assert(t, "Answer.Value", c.exp[x].Answer[y].Value, answer.Value) - } - for y, auth := range msg.Authority { - test.Assert(t, "Authority.Name", c.exp[x].Authority[y].Name, auth.Name) - test.Assert(t, "Authority.Type", c.exp[x].Authority[y].Type, auth.Type) - test.Assert(t, "Authority.Class", c.exp[x].Authority[y].Class, auth.Class) - test.Assert(t, "Authority.TTL", c.exp[x].Authority[y].TTL, auth.TTL) - test.Assert(t, "Authority.Value", c.exp[x].Authority[y].Value, auth.Value) - } - for y, add := range msg.Additional { - test.Assert(t, "Additional.Name", c.exp[x].Additional[y].Name, add.Name) - test.Assert(t, "Additional.Type", c.exp[x].Additional[y].Type, add.Type) - test.Assert(t, "Additional.Class", c.exp[x].Additional[y].Class, add.Class) - test.Assert(t, "Additional.TTL", c.exp[x].Additional[y].TTL, add.TTL) - test.Assert(t, "Additional.Value", c.exp[x].Additional[y].Value, add.Value) - } - } - } -} - -func TestZoneParseTXT(t *testing.T) { - cases := []struct { - in string - expError string - exp []*Message - }{{ - in: `@ IN TXT "This is a test"`, - exp: []*Message{{ - Header: MessageHeader{ - IsAA: true, - QDCount: 1, - ANCount: 1, - }, - Question: MessageQuestion{ - Name: "kilabit.local", - Type: RecordTypeTXT, - Class: RecordClassIN, - }, - Answer: []ResourceRecord{{ - Name: "kilabit.local", - Type: RecordTypeTXT, - Class: RecordClassIN, - TTL: 3600, - Value: "This is a test", - }}, - }}, - }} - - m := newZoneParser("") - - for _, c := range cases { - m.Init(c.in, "kilabit.local", 3600) - - err := m.parse() - if err != nil { - test.Assert(t, "error", c.expError, err.Error()) - continue - } - - test.Assert(t, "messages length:", len(c.exp), len(m.zone.messages)) - - for x, msg := range m.zone.messages { - test.Assert(t, "Message.Header", c.exp[x].Header, msg.Header) - test.Assert(t, "Message.Question", c.exp[x].Question, msg.Question) - - for y, answer := range msg.Answer { - test.Assert(t, "Answer.Name", c.exp[x].Answer[y].Name, answer.Name) - test.Assert(t, "Answer.Type", c.exp[x].Answer[y].Type, answer.Type) - test.Assert(t, "Answer.Class", c.exp[x].Answer[y].Class, answer.Class) - test.Assert(t, "Answer.TTL", c.exp[x].Answer[y].TTL, answer.TTL) - test.Assert(t, "Answer.Value", c.exp[x].Answer[y].Value, answer.Value) - } - for y, auth := range msg.Authority { - test.Assert(t, "Authority.Name", c.exp[x].Authority[y].Name, auth.Name) - test.Assert(t, "Authority.Type", c.exp[x].Authority[y].Type, auth.Type) - test.Assert(t, "Authority.Class", c.exp[x].Authority[y].Class, auth.Class) - test.Assert(t, "Authority.TTL", c.exp[x].Authority[y].TTL, auth.TTL) - test.Assert(t, "Authority.Value", c.exp[x].Authority[y].Value, auth.Value) - } - for y, add := range msg.Additional { - test.Assert(t, "Additional.Name", c.exp[x].Additional[y].Name, add.Name) - test.Assert(t, "Additional.Type", c.exp[x].Additional[y].Type, add.Type) - test.Assert(t, "Additional.Class", c.exp[x].Additional[y].Class, add.Class) - test.Assert(t, "Additional.TTL", c.exp[x].Additional[y].TTL, add.TTL) - test.Assert(t, "Additional.Value", c.exp[x].Additional[y].Value, add.Value) - } - } - } -} diff --git a/lib/dns/zone_parser.go b/lib/dns/zone_parser.go index 92bcb5d4..f9c4b9c5 100644 --- a/lib/dns/zone_parser.go +++ b/lib/dns/zone_parser.go @@ -51,7 +51,7 @@ const ( ) type zoneParser struct { - zone *ZoneFile + zone *Zone reader *libio.Reader lastRR *ResourceRecord origin string @@ -64,7 +64,7 @@ type zoneParser struct { func newZoneParser(file string) *zoneParser { return &zoneParser{ - zone: NewZoneFile(file, ""), + zone: NewZone(file, ""), lineno: 1, seps: []byte{' ', '\t'}, terms: []byte{';', '\n'}, @@ -75,7 +75,7 @@ func newZoneParser(file string) *zoneParser { // Init parse zoneParser file from string. // func (m *zoneParser) Init(data, origin string, ttl uint32) { - m.zone = NewZoneFile("(data)", "") + m.zone = NewZone("(data)", "") m.lineno = 1 m.origin = strings.ToLower(origin) m.ttl = ttl diff --git a/lib/dns/zone_test.go b/lib/dns/zone_test.go new file mode 100644 index 00000000..38f1d142 --- /dev/null +++ b/lib/dns/zone_test.go @@ -0,0 +1,815 @@ +// Copyright 2018, Shulhan . All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +package dns + +import ( + "testing" + + "github.com/shuLhan/share/lib/test" +) + +func TestZoneParseDirectiveOrigin(t *testing.T) { + cases := []struct { + desc string + in string + expErr string + exp string + }{{ + desc: "Without value", + in: `$origin`, + expErr: "line 1: empty $origin directive", + }, { + desc: "Without value and with comment", + in: `$origin ; comment`, + expErr: "line 1: empty $origin directive", + }, { + desc: "With value", + in: `$origin x`, + exp: "x", + }, { + desc: "With value and comment", + in: `$origin x ;comment`, + exp: "x", + }} + + m := newZoneParser("") + + for _, c := range cases { + t.Log(c.desc) + + m.Init(c.in, "", 0) + + err := m.parse() + if err != nil { + test.Assert(t, "err", c.expErr, err.Error()) + continue + } + + test.Assert(t, "origin", c.exp, m.origin) + } +} + +func TestZoneParseDirectiveInclude(t *testing.T) { + cases := []struct { + desc string + in string + expErr string + }{{ + desc: "Without value", + in: `$include`, + expErr: "line 1: empty $include directive", + }, { + desc: "Without value and with comment", + in: `$include ; comment`, + expErr: "line 1: empty $include directive", + }, { + desc: "With value", + in: `$include testdata/sub.domain`, + }, { + desc: "With value and comment", + in: `$origin testdata/sub.domain ;comment`, + }} + + m := newZoneParser("") + + for _, c := range cases { + t.Log(c.desc) + + m.Init(c.in, "", 0) + + err := m.parse() + if err != nil { + test.Assert(t, "err", c.expErr, err.Error()) + continue + } + } +} + +func TestZoneParseDirectiveTTL(t *testing.T) { + cases := []struct { + desc string + in string + expErr string + exp uint32 + }{{ + desc: "Without value", + in: `$ttl`, + expErr: "line 1: empty $TTL directive", + }, { + desc: "Without value and with comment", + in: `$ttl ; comment`, + expErr: "line 1: empty $TTL directive", + }, { + desc: "With value", + in: `$ttl 1`, + exp: 1, + }, { + desc: "With value and comment", + in: `$ttl 1 ;comment`, + exp: 1, + }} + + m := newZoneParser("") + + for _, c := range cases { + t.Log(c.desc) + + m.Init(c.in, "", 0) + + err := m.parse() + if err != nil { + test.Assert(t, "err", c.expErr, err.Error()) + continue + } + + test.Assert(t, "ttl", c.exp, m.ttl) + } +} + +func TestZoneInitRFC1035(t *testing.T) { + type caseZoneInit struct { + expErr error + desc string + origin string + in string + exp []*Message + ttl uint32 + } + + cases := []caseZoneInit{{ + desc: "RFC1035 section 5.3", + origin: "ISI.EDU", + ttl: 3600, + in: ` +@ IN SOA VENERA Action\.domains ( + 20 ; SERIAL + 7200 ; REFRESH + 600 ; RETRY + 3600000; EXPIRE + 60) ; MINIMUM + + NS A.ISI.EDU. + NS VENERA + NS VAXA + MX 10 VENERA + MX 20 VAXA + +A A 26.3.0.103 + +VENERA A 10.1.0.52 + A 128.9.0.32 + +VAXA A 10.2.0.27 + A 128.9.0.33 + +`, + exp: []*Message{{ + Header: MessageHeader{ + IsAA: true, + QDCount: 1, + ANCount: 1, + }, + Question: MessageQuestion{ + Name: "isi.edu", + Type: RecordTypeSOA, + Class: RecordClassIN, + }, + Answer: []ResourceRecord{{ + Name: "isi.edu", + Type: RecordTypeSOA, + Class: RecordClassIN, + TTL: 3600, + Value: &RDataSOA{ + MName: "venera.isi.edu", + RName: "action\\.domains.isi.edu", + Serial: 20, + Refresh: 7200, + Retry: 600, + Expire: 3600000, + Minimum: 60, + }, + }}, + }, { + Header: MessageHeader{ + IsAA: true, + QDCount: 1, + ANCount: 3, + }, + Question: MessageQuestion{ + Name: "isi.edu", + Type: RecordTypeNS, + Class: RecordClassIN, + }, + Answer: []ResourceRecord{{ + Name: "isi.edu", + Type: RecordTypeNS, + Class: RecordClassIN, + TTL: 3600, + Value: "a.isi.edu", + }, { + Name: "isi.edu", + Type: RecordTypeNS, + Class: RecordClassIN, + TTL: 3600, + Value: "venera.isi.edu", + }, { + Name: "isi.edu", + Type: RecordTypeNS, + Class: RecordClassIN, + TTL: 3600, + Value: "vaxa.isi.edu", + }}, + }, { + Header: MessageHeader{ + IsAA: true, + QDCount: 1, + ANCount: 2, + }, + Question: MessageQuestion{ + Name: "isi.edu", + Type: RecordTypeMX, + Class: RecordClassIN, + }, + Answer: []ResourceRecord{{ + Name: "isi.edu", + Type: RecordTypeMX, + Class: RecordClassIN, + TTL: 3600, + Value: &RDataMX{ + Preference: 10, + Exchange: "venera.isi.edu", + }, + }, { + Name: "isi.edu", + Type: RecordTypeMX, + Class: RecordClassIN, + TTL: 3600, + Value: &RDataMX{ + Preference: 20, + Exchange: "vaxa.isi.edu", + }, + }}, + }, { + Header: MessageHeader{ + IsAA: true, + QDCount: 1, + ANCount: 1, + }, + Question: MessageQuestion{ + Name: "a.isi.edu", + Type: RecordTypeA, + Class: RecordClassIN, + }, + Answer: []ResourceRecord{{ + Name: "a.isi.edu", + Type: RecordTypeA, + Class: RecordClassIN, + TTL: 3600, + Value: "26.3.0.103", + }}, + }, { + Header: MessageHeader{ + IsAA: true, + QDCount: 1, + ANCount: 2, + }, + Question: MessageQuestion{ + Name: "venera.isi.edu", + Type: RecordTypeA, + Class: RecordClassIN, + }, + Answer: []ResourceRecord{{ + Name: "venera.isi.edu", + Type: RecordTypeA, + Class: RecordClassIN, + TTL: 3600, + Value: "10.1.0.52", + }, { + Name: "venera.isi.edu", + Type: RecordTypeA, + Class: RecordClassIN, + TTL: 3600, + Value: "128.9.0.32", + }}, + }, { + Header: MessageHeader{ + IsAA: true, + QDCount: 1, + ANCount: 2, + }, + Question: MessageQuestion{ + Name: "vaxa.isi.edu", + Type: RecordTypeA, + Class: RecordClassIN, + }, + Answer: []ResourceRecord{{ + Name: "vaxa.isi.edu", + Type: RecordTypeA, + Class: RecordClassIN, + TTL: 3600, + Value: "10.2.0.27", + }, { + Name: "vaxa.isi.edu", + Type: RecordTypeA, + Class: RecordClassIN, + TTL: 3600, + Value: "128.9.0.33", + }}, + }}, + }} + + m := newZoneParser("") + + for _, c := range cases { + t.Log(c.desc) + + m.Init(c.in, c.origin, c.ttl) + + err := m.parse() + if err != nil { + test.Assert(t, "err", c.expErr, err.Error()) + continue + } + + test.Assert(t, "messages length:", + len(c.exp), len(m.zone.messages)) + + for x, msg := range m.zone.messages { + test.Assert(t, "Message.Header", c.exp[x].Header, msg.Header) + test.Assert(t, "Message.Question", c.exp[x].Question, msg.Question) + + for y, answer := range msg.Answer { + test.Assert(t, "Answer.Name", c.exp[x].Answer[y].Name, answer.Name) + test.Assert(t, "Answer.Type", c.exp[x].Answer[y].Type, answer.Type) + test.Assert(t, "Answer.Class", c.exp[x].Answer[y].Class, answer.Class) + test.Assert(t, "Answer.TTL", c.exp[x].Answer[y].TTL, answer.TTL) + test.Assert(t, "Answer.Value", c.exp[x].Answer[y].Value, answer.Value) + } + for y, auth := range msg.Authority { + test.Assert(t, "Authority.Name", c.exp[x].Authority[y].Name, auth.Name) + test.Assert(t, "Authority.Type", c.exp[x].Authority[y].Type, auth.Type) + test.Assert(t, "Authority.Class", c.exp[x].Authority[y].Class, auth.Class) + test.Assert(t, "Authority.TTL", c.exp[x].Authority[y].TTL, auth.TTL) + test.Assert(t, "Authority.Value", c.exp[x].Authority[y].Value, auth.Value) + } + for y, add := range msg.Additional { + test.Assert(t, "Additional.Name", c.exp[x].Additional[y].Name, add.Name) + test.Assert(t, "Additional.Type", c.exp[x].Additional[y].Type, add.Type) + test.Assert(t, "Additional.Class", c.exp[x].Additional[y].Class, add.Class) + test.Assert(t, "Additional.TTL", c.exp[x].Additional[y].TTL, add.TTL) + test.Assert(t, "Additional.Value", c.exp[x].Additional[y].Value, add.Value) + } + } + } +} + +func TestZoneInit2(t *testing.T) { + cases := []struct { + expErr error + desc string + origin string + in string + exp []*Message + ttl uint32 + }{{ + desc: "From http://www.tcpipguide.com/free/t_DNSZoneFileFormat-4.htm", + in: ` +$ORIGIN pcguide.com. +@ IN SOA ns23.pair.com. root.pair.com. ( +2001072300 ; Serial +3600 ; Refresh +300 ; Retry +604800 ; Expire +3600 ) ; Minimum + +@ IN NS ns23.pair.com. +@ IN NS ns0.ns0.com. + +localhost IN A 127.0.0.1 +@ IN A 209.68.14.80 + IN MX 50 qs939.pair.com. + +www IN CNAME @ +ftp IN CNAME @ +mail IN CNAME @ +relay IN CNAME relay.pair.com. +`, + exp: []*Message{{ + Header: MessageHeader{ + IsAA: true, + QDCount: 1, + ANCount: 1, + }, + Question: MessageQuestion{ + Name: "pcguide.com", + Type: RecordTypeSOA, + Class: RecordClassIN, + }, + Answer: []ResourceRecord{{ + Name: "pcguide.com", + Type: RecordTypeSOA, + Class: RecordClassIN, + TTL: 3600, + Value: &RDataSOA{ + MName: "ns23.pair.com", + RName: "root.pair.com", + Serial: 2001072300, + Refresh: 3600, + Retry: 300, + Expire: 604800, + Minimum: 3600, + }, + }}, + }, { + Header: MessageHeader{ + IsAA: true, + QDCount: 1, + ANCount: 2, + }, + Question: MessageQuestion{ + Name: "pcguide.com", + Type: RecordTypeNS, + Class: RecordClassIN, + }, + Answer: []ResourceRecord{{ + Name: "pcguide.com", + Type: RecordTypeNS, + Class: RecordClassIN, + TTL: 3600, + Value: "ns23.pair.com", + }, { + Name: "pcguide.com", + Type: RecordTypeNS, + Class: RecordClassIN, + TTL: 3600, + Value: "ns0.ns0.com", + }}, + }, { + Header: MessageHeader{ + IsAA: true, + QDCount: 1, + ANCount: 1, + }, + Question: MessageQuestion{ + Name: "localhost.pcguide.com", + Type: RecordTypeA, + Class: RecordClassIN, + }, + Answer: []ResourceRecord{{ + Name: "localhost.pcguide.com", + Type: RecordTypeA, + Class: RecordClassIN, + TTL: 3600, + Value: "127.0.0.1", + }}, + }, { + Header: MessageHeader{ + IsAA: true, + QDCount: 1, + ANCount: 1, + }, + Question: MessageQuestion{ + Name: "pcguide.com", + Type: RecordTypeA, + Class: RecordClassIN, + }, + Answer: []ResourceRecord{{ + Name: "pcguide.com", + Type: RecordTypeA, + Class: RecordClassIN, + TTL: 3600, + Value: "209.68.14.80", + }}, + }, { + Header: MessageHeader{ + IsAA: true, + QDCount: 1, + ANCount: 1, + }, + Question: MessageQuestion{ + Name: "pcguide.com", + Type: RecordTypeMX, + Class: RecordClassIN, + }, + Answer: []ResourceRecord{{ + Name: "pcguide.com", + Type: RecordTypeMX, + Class: RecordClassIN, + TTL: 3600, + Value: &RDataMX{ + Preference: 50, + Exchange: "qs939.pair.com", + }, + }}, + }, { + Header: MessageHeader{ + IsAA: true, + QDCount: 1, + ANCount: 1, + }, + Question: MessageQuestion{ + Name: "www.pcguide.com", + Type: RecordTypeCNAME, + Class: RecordClassIN, + }, + Answer: []ResourceRecord{{ + Name: "www.pcguide.com", + Type: RecordTypeCNAME, + Class: RecordClassIN, + TTL: 3600, + Value: "pcguide.com", + }}, + }, { + Header: MessageHeader{ + IsAA: true, + QDCount: 1, + ANCount: 1, + }, + Question: MessageQuestion{ + Name: "ftp.pcguide.com", + Type: RecordTypeCNAME, + Class: RecordClassIN, + }, + Answer: []ResourceRecord{{ + Name: "ftp.pcguide.com", + Type: RecordTypeCNAME, + Class: RecordClassIN, + TTL: 3600, + Value: "pcguide.com", + }}, + }, { + Header: MessageHeader{ + IsAA: true, + QDCount: 1, + ANCount: 1, + }, + Question: MessageQuestion{ + Name: "mail.pcguide.com", + Type: RecordTypeCNAME, + Class: RecordClassIN, + }, + Answer: []ResourceRecord{{ + Name: "mail.pcguide.com", + Type: RecordTypeCNAME, + Class: RecordClassIN, + TTL: 3600, + Value: "pcguide.com", + }}, + }, { + Header: MessageHeader{ + IsAA: true, + QDCount: 1, + ANCount: 1, + }, + Question: MessageQuestion{ + Name: "relay.pcguide.com", + Type: RecordTypeCNAME, + Class: RecordClassIN, + }, + Answer: []ResourceRecord{{ + Name: "relay.pcguide.com", + Type: RecordTypeCNAME, + Class: RecordClassIN, + TTL: 3600, + Value: "relay.pair.com", + }}, + }}, + }} + + m := newZoneParser("") + + for _, c := range cases { + t.Log(c.desc) + + m.Init(c.in, c.origin, c.ttl) + + err := m.parse() + if err != nil { + test.Assert(t, "err", c.expErr, err.Error()) + continue + } + + test.Assert(t, "messages length:", len(c.exp), + len(m.zone.messages)) + + for x, msg := range m.zone.messages { + test.Assert(t, "Message.Header", c.exp[x].Header, msg.Header) + test.Assert(t, "Message.Question", c.exp[x].Question, msg.Question) + + for y, answer := range msg.Answer { + test.Assert(t, "Answer.Name", c.exp[x].Answer[y].Name, answer.Name) + test.Assert(t, "Answer.Type", c.exp[x].Answer[y].Type, answer.Type) + test.Assert(t, "Answer.Class", c.exp[x].Answer[y].Class, answer.Class) + test.Assert(t, "Answer.TTL", c.exp[x].Answer[y].TTL, answer.TTL) + test.Assert(t, "Answer.Value", c.exp[x].Answer[y].Value, answer.Value) + } + for y, auth := range msg.Authority { + test.Assert(t, "Authority.Name", c.exp[x].Authority[y].Name, auth.Name) + test.Assert(t, "Authority.Type", c.exp[x].Authority[y].Type, auth.Type) + test.Assert(t, "Authority.Class", c.exp[x].Authority[y].Class, auth.Class) + test.Assert(t, "Authority.TTL", c.exp[x].Authority[y].TTL, auth.TTL) + test.Assert(t, "Authority.Value", c.exp[x].Authority[y].Value, auth.Value) + } + for y, add := range msg.Additional { + test.Assert(t, "Additional.Name", c.exp[x].Additional[y].Name, add.Name) + test.Assert(t, "Additional.Type", c.exp[x].Additional[y].Type, add.Type) + test.Assert(t, "Additional.Class", c.exp[x].Additional[y].Class, add.Class) + test.Assert(t, "Additional.TTL", c.exp[x].Additional[y].TTL, add.TTL) + test.Assert(t, "Additional.Value", c.exp[x].Additional[y].Value, add.Value) + } + } + } +} + +func TestZoneInit3(t *testing.T) { + cases := []struct { + expErr error + desc string + origin string + in string + exp []*Message + ttl uint32 + }{{ + desc: "From http://www.tcpipguide.com/free/t_DNSZoneFileFormat-4.htm", + origin: "localdomain", + in: ` +; Applications. +dev.kilabit.info. A 127.0.0.1 +dev.kilabit.com. A 127.0.0.1 + +; Documentations. +angularjs.doc A 127.0.0.1 +`, + exp: []*Message{{ + Header: MessageHeader{ + IsAA: true, + QDCount: 1, + ANCount: 1, + }, + Question: MessageQuestion{ + Name: "dev.kilabit.info", + Type: RecordTypeA, + Class: RecordClassIN, + }, + Answer: []ResourceRecord{{ + Name: "dev.kilabit.info", + Type: RecordTypeA, + Class: RecordClassIN, + TTL: 3600, + Value: "127.0.0.1", + }}, + }, { + Header: MessageHeader{ + IsAA: true, + QDCount: 1, + ANCount: 1, + }, + Question: MessageQuestion{ + Name: "dev.kilabit.com", + Type: RecordTypeA, + Class: RecordClassIN, + }, + Answer: []ResourceRecord{{ + Name: "dev.kilabit.com", + Type: RecordTypeA, + Class: RecordClassIN, + TTL: 3600, + Value: "127.0.0.1", + }}, + }, { + Header: MessageHeader{ + IsAA: true, + QDCount: 1, + ANCount: 1, + }, + Question: MessageQuestion{ + Name: "angularjs.doc.localdomain", + Type: RecordTypeA, + Class: RecordClassIN, + }, + Answer: []ResourceRecord{{ + Name: "angularjs.doc.localdomain", + Type: RecordTypeA, + Class: RecordClassIN, + TTL: 3600, + Value: "127.0.0.1", + }}, + }}, + }} + + m := newZoneParser("") + + for _, c := range cases { + t.Log(c.desc) + + m.Init(c.in, c.origin, c.ttl) + + err := m.parse() + if err != nil { + test.Assert(t, "err", c.expErr, err.Error()) + continue + } + + test.Assert(t, "messages length:", len(c.exp), + len(m.zone.messages)) + + for x, msg := range m.zone.messages { + test.Assert(t, "Message.Header", c.exp[x].Header, msg.Header) + test.Assert(t, "Message.Question", c.exp[x].Question, msg.Question) + + for y, answer := range msg.Answer { + test.Assert(t, "Answer.Name", c.exp[x].Answer[y].Name, answer.Name) + test.Assert(t, "Answer.Type", c.exp[x].Answer[y].Type, answer.Type) + test.Assert(t, "Answer.Class", c.exp[x].Answer[y].Class, answer.Class) + test.Assert(t, "Answer.TTL", c.exp[x].Answer[y].TTL, answer.TTL) + test.Assert(t, "Answer.Value", c.exp[x].Answer[y].Value, answer.Value) + } + for y, auth := range msg.Authority { + test.Assert(t, "Authority.Name", c.exp[x].Authority[y].Name, auth.Name) + test.Assert(t, "Authority.Type", c.exp[x].Authority[y].Type, auth.Type) + test.Assert(t, "Authority.Class", c.exp[x].Authority[y].Class, auth.Class) + test.Assert(t, "Authority.TTL", c.exp[x].Authority[y].TTL, auth.TTL) + test.Assert(t, "Authority.Value", c.exp[x].Authority[y].Value, auth.Value) + } + for y, add := range msg.Additional { + test.Assert(t, "Additional.Name", c.exp[x].Additional[y].Name, add.Name) + test.Assert(t, "Additional.Type", c.exp[x].Additional[y].Type, add.Type) + test.Assert(t, "Additional.Class", c.exp[x].Additional[y].Class, add.Class) + test.Assert(t, "Additional.TTL", c.exp[x].Additional[y].TTL, add.TTL) + test.Assert(t, "Additional.Value", c.exp[x].Additional[y].Value, add.Value) + } + } + } +} + +func TestZoneParseTXT(t *testing.T) { + cases := []struct { + in string + expError string + exp []*Message + }{{ + in: `@ IN TXT "This is a test"`, + exp: []*Message{{ + Header: MessageHeader{ + IsAA: true, + QDCount: 1, + ANCount: 1, + }, + Question: MessageQuestion{ + Name: "kilabit.local", + Type: RecordTypeTXT, + Class: RecordClassIN, + }, + Answer: []ResourceRecord{{ + Name: "kilabit.local", + Type: RecordTypeTXT, + Class: RecordClassIN, + TTL: 3600, + Value: "This is a test", + }}, + }}, + }} + + m := newZoneParser("") + + for _, c := range cases { + m.Init(c.in, "kilabit.local", 3600) + + err := m.parse() + if err != nil { + test.Assert(t, "error", c.expError, err.Error()) + continue + } + + test.Assert(t, "messages length:", len(c.exp), len(m.zone.messages)) + + for x, msg := range m.zone.messages { + test.Assert(t, "Message.Header", c.exp[x].Header, msg.Header) + test.Assert(t, "Message.Question", c.exp[x].Question, msg.Question) + + for y, answer := range msg.Answer { + test.Assert(t, "Answer.Name", c.exp[x].Answer[y].Name, answer.Name) + test.Assert(t, "Answer.Type", c.exp[x].Answer[y].Type, answer.Type) + test.Assert(t, "Answer.Class", c.exp[x].Answer[y].Class, answer.Class) + test.Assert(t, "Answer.TTL", c.exp[x].Answer[y].TTL, answer.TTL) + test.Assert(t, "Answer.Value", c.exp[x].Answer[y].Value, answer.Value) + } + for y, auth := range msg.Authority { + test.Assert(t, "Authority.Name", c.exp[x].Authority[y].Name, auth.Name) + test.Assert(t, "Authority.Type", c.exp[x].Authority[y].Type, auth.Type) + test.Assert(t, "Authority.Class", c.exp[x].Authority[y].Class, auth.Class) + test.Assert(t, "Authority.TTL", c.exp[x].Authority[y].TTL, auth.TTL) + test.Assert(t, "Authority.Value", c.exp[x].Authority[y].Value, auth.Value) + } + for y, add := range msg.Additional { + test.Assert(t, "Additional.Name", c.exp[x].Additional[y].Name, add.Name) + test.Assert(t, "Additional.Type", c.exp[x].Additional[y].Type, add.Type) + test.Assert(t, "Additional.Class", c.exp[x].Additional[y].Class, add.Class) + test.Assert(t, "Additional.TTL", c.exp[x].Additional[y].TTL, add.TTL) + test.Assert(t, "Additional.Value", c.exp[x].Additional[y].Value, add.Value) + } + } + } +} -- cgit v1.3