diff --git a/backend/backend.go b/backend/backend.go index 5c6aac7..ea5008f 100644 --- a/backend/backend.go +++ b/backend/backend.go @@ -1,16 +1,14 @@ package backend -import "github.com/golang/groupcache/lru" import "github.com/miekg/dns" -import "github.com/hlandau/degoutils/log" -import "fmt" -import "strings" -import "net" -import "github.com/hlandau/ncdns/namecoin" +import "github.com/golang/groupcache/lru" import "github.com/hlandau/madns/merr" +import "github.com/hlandau/ncdns/namecoin" import "github.com/hlandau/ncdns/util" import "github.com/hlandau/ncdns/ncdomain" import "sync" +import "fmt" +import "net" // Provides an abstract zone file for the Namecoin .bit TLD. type Backend struct { @@ -21,9 +19,7 @@ type Backend struct { cfg Config } -const ( - defaultMaxEntries = 100 -) +const defaultMaxEntries = 100 // Backend configuration. type Config struct { @@ -65,108 +61,21 @@ func New(cfg *Config) (backend *Backend, err error) { b.cache.MaxEntries = defaultMaxEntries } - if b.cfg.FakeNames == nil { - b.cfg.FakeNames = map[string]string{} - } - backend = b return } -// Keep domains in parsed format. -type domain struct { - ncv *ncdomain.Value -} - -func toNamecoinName(basename string) (string, error) { - return "d/" + basename, nil -} - -func (b *Backend) getNamecoinEntry(name string) (*domain, error) { - d := b.getNamecoinEntryCache(name) - if d != nil { - return d, nil - } - - d, err := b.getNamecoinEntryLL(name) - if err != nil { - return nil, err - } - - b.addNamecoinEntryToCache(name, d) - return d, nil -} - -func (b *Backend) getNamecoinEntryCache(name string) *domain { - b.cacheMutex.Lock() - defer b.cacheMutex.Unlock() - - if dd, ok := b.cache.Get(name); ok { - d := dd.(*domain) - return d - } - - return nil -} - -func (b *Backend) addNamecoinEntryToCache(name string, d *domain) { - b.cacheMutex.Lock() - defer b.cacheMutex.Unlock() - - b.cache.Add(name, d) -} - -func (b *Backend) resolveName(name string) (jsonValue string, err error) { - if fv, ok := b.cfg.FakeNames[name]; ok { - if fv == "NX" { - return "", merr.ErrNoSuchDomain - } - return fv, nil - } - - v, err := b.nc.Query(name) - if err != nil { - return "", err - } - - return v, nil -} - -func (b *Backend) getNamecoinEntryLL(name string) (*domain, error) { - v, err := b.resolveName(name) - if err != nil { - return nil, err - } - - log.Info("namecoin query (", name, ") succeeded: ", v) - - d, err := b.jsonToDomain(name, v) - if err != nil { - log.Infoe(err, "cannot convert JSON to domain") - return nil, err - } - - return d, nil -} - -func (b *Backend) jsonToDomain(name, jsonValue string) (*domain, error) { - d := &domain{} - - v, err := ncdomain.ParseValue(name, jsonValue, b.resolveExtraName) - if err != nil { - return nil, err - } - - d.ncv = v - - return d, nil -} - -func (b *Backend) resolveExtraName(name string) (jsonValue string, err error) { - return b.resolveName(name) +// Do low-level queries against an abstract zone file. This is the per-query +// entrypoint from madns. +func (b *Backend) Lookup(qname string) (rrs []dns.RR, err error) { + btx := &btx{} + btx.b = b + btx.qname = qname + return btx.Do() } +// Things to keep track of while processing a query. type btx struct { b *Backend qname string @@ -174,73 +83,47 @@ type btx struct { subname, basename, rootname string } -func (tx *btx) determineDomain() (subname, basename, rootname string, err error) { - qname := tx.qname - qname = strings.TrimRight(qname, ".") - parts := strings.Split(qname, ".") - if len(parts) < 2 { - if parts[0] != "bit" { - err = merr.ErrNotInZone - return - } - - rootname = parts[0] - return - } - - for i := len(parts) - 1; i >= 0; i-- { - v := parts[i] - - // scanning for rootname - if v == "bit" { - if i == 0 { - // i is already zero, so we have something like bit.x.y.z. - rootname = qname - return - } - rootname = strings.Join(parts[i:len(parts)], ".") - basename = parts[i-1] - subname = strings.Join(parts[0:i-1], ".") - return - } - } - - err = merr.ErrNotInZone - return -} - func (tx *btx) Do() (rrs []dns.RR, err error) { + // Split the domain up. 'rootname' is the TLD and everything after it, + // basename is the name directly before that, and subname is every name + // before that. So "a.b.example.bit.suffix.xyz." would have a subname + // of "a.b", a basename of "example" and a rootname of "bit.suffix.xyz". tx.subname, tx.basename, tx.rootname, err = tx.determineDomain() if err != nil { - log.Infoe(err, "couldn't determine domain") + // We get an error if '.bit.' does not appear anywhere. In that case + // we're not authoritative for the query in question and error out. return } - log.Info("domain: sub=", tx.subname, " basename=", tx.basename, " rootname=", tx.rootname) - if tx.rootname == "" { // REFUSED return nil, merr.ErrNotInZone } + // If subname and basename are "", this means the query was for a root name + // such as "bit." or "bit.suffix.xyz." directly. Serve SOA, NS records, etc. + // as requested. if tx.subname == "" && tx.basename == "" { return tx.doRootDomain() } + // Where ncdns has not been configured with a hostname to identify itself by, + // it generates one under a special meta domain "x--nmc". This domain is not + // a valid Namecoin domain name, so it does not confict with the Namecoin + // domain name namespace. if tx.basename == "x--nmc" && tx.b.cfg.SelfName == "" { return tx.doMetaDomain() } + // If we have reached this point the query must be a normal user query. rrs, err = tx.doUserDomain() - - log.Info("USER RECORDS YIELDED:") - for _, rr := range rrs { - log.Info(" ", rr.String()) - } - return } +func (tx *btx) determineDomain() (subname, basename, rootname string, err error) { + return util.SplitDomainByFloatingAnchor(tx.qname, "bit") +} + func (tx *btx) doRootDomain() (rrs []dns.RR, err error) { nsname := tx.b.cfg.SelfName if nsname == "" { @@ -304,27 +187,110 @@ func (tx *btx) doMetaDomain() (rrs []dns.RR, err error) { } func (tx *btx) doUserDomain() (rrs []dns.RR, err error) { - ncname, err := toNamecoinName(tx.basename) + ncname, err := util.BasenameToNamecoinKey(tx.basename) if err != nil { - log.Infoe(err, "cannot determine namecoin name") return } d, err := tx.b.getNamecoinEntry(ncname) if err != nil { - log.Infoe(err, "cannot get namecoin entry") return nil, err } rrs, err = tx.doUnderDomain(d) if err != nil { - log.Infoe(err, "cannot process namecoin entry under domain") return nil, err } return rrs, nil } +// Keep domains in parsed format. +type domain struct { + ncv *ncdomain.Value +} + +func (b *Backend) getNamecoinEntry(name string) (*domain, error) { + d := b.getNamecoinEntryCache(name) + if d != nil { + return d, nil + } + + d, err := b.getNamecoinEntryLL(name) + if err != nil { + return nil, err + } + + b.addNamecoinEntryToCache(name, d) + return d, nil +} + +func (b *Backend) getNamecoinEntryCache(name string) *domain { + b.cacheMutex.Lock() + defer b.cacheMutex.Unlock() + + if dd, ok := b.cache.Get(name); ok { + d := dd.(*domain) + return d + } + + return nil +} + +func (b *Backend) addNamecoinEntryToCache(name string, d *domain) { + b.cacheMutex.Lock() + defer b.cacheMutex.Unlock() + + b.cache.Add(name, d) +} + +func (b *Backend) getNamecoinEntryLL(name string) (*domain, error) { + v, err := b.resolveName(name) + if err != nil { + return nil, err + } + + d, err := b.jsonToDomain(name, v) + if err != nil { + return nil, err + } + + return d, nil +} + +func (b *Backend) resolveName(name string) (jsonValue string, err error) { + if fv, ok := b.cfg.FakeNames[name]; ok { + if fv == "NX" { + return "", merr.ErrNoSuchDomain + } + return fv, nil + } + + v, err := b.nc.Query(name) + if err != nil { + return "", err + } + + return v, nil +} + +func (b *Backend) jsonToDomain(name, jsonValue string) (*domain, error) { + d := &domain{} + + v, err := ncdomain.ParseValue(name, jsonValue, b.resolveExtraName) + if err != nil { + return nil, err + } + + d.ncv = v + + return d, nil +} + +func (b *Backend) resolveExtraName(name string) (jsonValue string, err error) { + return b.resolveName(name) +} + func (tx *btx) doUnderDomain(d *domain) (rrs []dns.RR, err error) { rrs, err = tx.addAnswersUnderNCValue(d.ncv, tx.subname) if err == merr.ErrNoResults { @@ -340,15 +306,9 @@ func (tx *btx) addAnswersUnderNCValue(rncv *ncdomain.Value, subname string) (rrs return } - log.Info("ncv actual: ", sn) return tx.addAnswersUnderNCValueActual(ncv, sn) } -/*func hasNS(ncv *ncdomain.Value) bool { - nss, err := ncv.GetNSs() - return err == nil && len(nss) > 0 -}*/ - func (tx *btx) findNCValue(ncv *ncdomain.Value, subname string, shortCircuitFunc func(curNCV *ncdomain.Value) bool) (xncv *ncdomain.Value, sn string, err error) { return tx._findNCValue(ncv, subname, "", 0, shortCircuitFunc) } @@ -381,7 +341,7 @@ func (tx *btx) _findNCValue(ncv *ncdomain.Value, isubname, subname string, depth } func (tx *btx) addAnswersUnderNCValueActual(ncv *ncdomain.Value, sn string) (rrs []dns.RR, err error) { - rrs, err = ncv.RRs(nil, dns.Fqdn(tx.qname), dns.Fqdn(tx.basename+"."+tx.rootname)) //convertAt(nil, dns.Fqdn(tx.qname), ncv) + rrs, err = ncv.RRs(nil, dns.Fqdn(tx.qname), dns.Fqdn(tx.basename+"."+tx.rootname)) return } @@ -395,12 +355,4 @@ func (tx *btx) addAnswersUnderNCValueActual(ncv *ncdomain.Value, sn string) (rrs // f[b]("a", "b.c.d.e.f.g.zzz.bit") // f[a]("", "a.b.c.d.e.f.g.zzz.bit") -// Do low-level queries against an abstract zone file. -func (b *Backend) Lookup(qname string) (rrs []dns.RR, err error) { - btx := &btx{} - btx.b = b - btx.qname = qname - return btx.Do() -} - // © 2014 Hugo Landau GPLv3 or later diff --git a/ncdomain/convert.go b/ncdomain/convert.go index 745f466..1b03d0b 100644 --- a/ncdomain/convert.go +++ b/ncdomain/convert.go @@ -6,13 +6,12 @@ import "fmt" import "github.com/miekg/dns" import "encoding/base64" import "encoding/hex" -import "regexp" -import "net/mail" import "github.com/hlandau/ncdns/util" import "strings" const depthLimit = 16 const mergeDepthLimit = 4 +const defaultTTL = 600 // Note: Name values in Value (e.g. those in Alias and Target, Services, MXs, // etc.) are not necessarily fully qualified and must be fully qualified before @@ -94,7 +93,7 @@ func (v *Value) appendIPs(out []dns.RR, suffix, apexSuffix string) ([]dns.RR, er Name: suffix, Rrtype: dns.TypeA, Class: dns.ClassINET, - Ttl: 600, + Ttl: defaultTTL, }, A: ip, }) @@ -110,7 +109,7 @@ func (v *Value) appendIP6s(out []dns.RR, suffix, apexSuffix string) ([]dns.RR, e Name: suffix, Rrtype: dns.TypeAAAA, Class: dns.ClassINET, - Ttl: 600, + Ttl: defaultTTL, }, AAAA: ip, }) @@ -131,7 +130,7 @@ func (v *Value) appendNSs(out []dns.RR, suffix, apexSuffix string) ([]dns.RR, er Name: suffix, Rrtype: dns.TypeNS, Class: dns.ClassINET, - Ttl: 600, + Ttl: defaultTTL, }, Ns: qn, }) @@ -147,7 +146,7 @@ func (v *Value) appendTXTs(out []dns.RR, suffix, apexSuffix string) ([]dns.RR, e Name: suffix, Rrtype: dns.TypeTXT, Class: dns.ClassINET, - Ttl: 600, + Ttl: defaultTTL, }, Txt: txt, }) @@ -199,7 +198,7 @@ func (v *Value) appendAlias(out []dns.RR, suffix, apexSuffix string) ([]dns.RR, Name: suffix, Rrtype: dns.TypeCNAME, Class: dns.ClassINET, - Ttl: 600, + Ttl: defaultTTL, }, Target: qn, }) @@ -219,7 +218,7 @@ func (v *Value) appendTranslate(out []dns.RR, suffix, apexSuffix string) ([]dns. Name: suffix, Rrtype: dns.TypeDNAME, Class: dns.ClassINET, - Ttl: 600, + Ttl: defaultTTL, }, Target: qn, }) @@ -235,7 +234,7 @@ func (v *Value) RRsRecursive(out []dns.RR, suffix, apexSuffix string) ([]dns.RR, } for mk, mv := range v.Map { - if !validateLabel(mk) && mk != "" && mk != "*" { + if !util.ValidateLabel(mk) && mk != "" && mk != "*" { continue } @@ -390,7 +389,7 @@ func (v *Value) qualifyIntl(name, suffix, apexSuffix string) string { func (v *Value) qualify(name, suffix, apexSuffix string) (string, bool) { s := v.qualifyIntl(name, suffix, apexSuffix) - if !validateHostName(s) { + if !util.ValidateHostName(s) { return "", false } @@ -630,7 +629,7 @@ func (rv *rawValue) parseHostmaster(v *Value) error { } if s, ok := rv.Hostmaster.(string); ok { - if !validateEmail(s) { + if !util.ValidateEmail(s) { return fmt.Errorf("malformed e. mail address in email field") } @@ -682,7 +681,7 @@ func (rv *rawValue) parseDS(v *Value) error { a4h := hex.EncodeToString(a4b) v.DS = append(v.DS, &dns.DS{ - Hdr: dns.RR_Header{Rrtype: dns.TypeDS, Class: dns.ClassINET, Ttl: 600}, + Hdr: dns.RR_Header{Rrtype: dns.TypeDS, Class: dns.ClassINET, Ttl: defaultTTL}, KeyTag: uint16(a1), Algorithm: uint8(a2), DigestType: uint8(a3), @@ -753,7 +752,8 @@ func (rv *rawValue) parseTLSA(v *Value) error { name := "_" + ports + "._" + transport v.TLSA = append(v.TLSA, &dns.TLSA{ - Hdr: dns.RR_Header{Name: name, Rrtype: dns.TypeTLSA, Class: dns.ClassINET, Ttl: 600}, + Hdr: dns.RR_Header{Name: name, Rrtype: dns.TypeTLSA, Class: dns.ClassINET, + Ttl: defaultTTL}, Usage: uint8(a1), Selector: uint8(a2), MatchingType: uint8(a3), @@ -865,7 +865,7 @@ func (rv *rawValue) parseSingleMX(s interface{}, v *Value, relname string) error } v.MX = append(v.MX, &dns.MX{ - Hdr: dns.RR_Header{Rrtype: dns.TypeMX, Class: dns.ClassINET, Ttl: 600}, + Hdr: dns.RR_Header{Rrtype: dns.TypeMX, Class: dns.ClassINET, Ttl: defaultTTL}, Preference: uint16(prio), Mx: hostname, }) @@ -910,12 +910,12 @@ func (rv *rawValue) parseSingleService(svc interface{}, v *Value, relname string } appProtoName, ok := svca[0].(string) - if !ok || !validateServiceName(appProtoName) { + if !ok || !util.ValidateServiceName(appProtoName) { return fmt.Errorf("malformed service value") } transportProtoName, ok := svca[1].(string) - if !ok || !validateServiceName(transportProtoName) { + if !ok || !util.ValidateServiceName(transportProtoName) { return fmt.Errorf("malformed service value") } @@ -947,7 +947,7 @@ func (rv *rawValue) parseSingleService(svc interface{}, v *Value, relname string Name: sname, Rrtype: dns.TypeSRV, Class: dns.ClassINET, - Ttl: 600, + Ttl: defaultTTL, }, Priority: uint16(priority), Weight: uint16(weight), @@ -1038,33 +1038,3 @@ func (v *Value) moveEmptyMapItems() error { } return nil } - -// Validation functions - -// This is used to validate NS records, targets in SRV records, etc. In these cases -// an IP address is not allowed. Therefore this regex must exclude all-numeric domain names. -// This is done by requiring the final part to start with an alphabetic character. -var re_hostName = regexp.MustCompilePOSIX(`^(([a-z0-9_][a-z0-9_-]{0,62}\.)*[a-z_][a-z0-9_-]{0,62}\.?|\.)$`) -var re_serviceName = regexp.MustCompilePOSIX(`^[a-z_][a-z0-9_-]*$`) -var re_label = regexp.MustCompilePOSIX(`^[a-z0-9_][a-z0-9_-]*$`) - -func validateHostName(name string) bool { - name = dns.Fqdn(name) - return len(name) <= 255 && re_hostName.MatchString(name) -} - -func validateServiceName(name string) bool { - return len(name) < 63 && re_serviceName.MatchString(name) -} - -func validateLabel(name string) bool { - return len(name) <= 63 && re_label.MatchString(name) -} - -func validateEmail(email string) bool { - addr, err := mail.ParseAddress(email) - if addr == nil || err != nil { - return false - } - return addr.Name == "" -} diff --git a/server/server.go b/server/server.go index 6f4ed5d..0f2e043 100644 --- a/server/server.go +++ b/server/server.go @@ -6,8 +6,6 @@ import "github.com/hlandau/ncdns/backend" import "github.com/miekg/dns" import "os" import "fmt" -import "os/signal" -import "syscall" import "path/filepath" const version = "1.0" @@ -68,23 +66,17 @@ func NewServer(cfg *ServerConfig) (s *Server, err error) { // key setup if cfg.PublicKey != "" { - ksk, kskPrivate, err := s.loadKey(cfg.PublicKey, cfg.PrivateKey) + ecfg.KSK, ecfg.KSKPrivate, err = s.loadKey(cfg.PublicKey, cfg.PrivateKey) if err != nil { return nil, err } - - ecfg.KSK = ksk - ecfg.KSKPrivate = kskPrivate } if cfg.ZonePublicKey != "" { - zsk, zskPrivate, err := s.loadKey(cfg.ZonePublicKey, cfg.ZonePrivateKey) + ecfg.ZSK, ecfg.ZSKPrivate, err = s.loadKey(cfg.ZonePublicKey, cfg.ZonePrivateKey) if err != nil { return nil, err } - - ecfg.ZSK = zsk - ecfg.ZSKPrivate = zskPrivate } if ecfg.KSK != nil && ecfg.ZSK == nil { @@ -141,19 +133,6 @@ func (s *Server) Start() error { return nil } -func (s *Server) Run() { - s.Start() - - // wait - sig := make(chan os.Signal) - signal.Notify(sig, syscall.SIGINT, syscall.SIGTERM) - for { - s := <-sig - fmt.Printf("Signal %v received, stopping.", s) - break - } -} - func (s *Server) doRunListener(ds *dns.Server) { err := ds.ListenAndServe() log.Fatale(err) diff --git a/util/util.go b/util/util.go index 20b01c6..62b2ccd 100644 --- a/util/util.go +++ b/util/util.go @@ -1,6 +1,11 @@ package util import "strings" +import "github.com/miekg/dns" +import "github.com/hlandau/madns/merr" +import "fmt" +import "regexp" +import "net/mail" // Split a domain name a.b.c.d.e into parts e (the head) and a.b.c.d (the rest). func SplitDomainHead(name string) (head, rest string) { @@ -30,4 +35,104 @@ func SplitDomainTail(name string) (tail, rest string) { return s[0], s[1] } +// For domains of the form "Y1.Y2...Yn.ANCHOR.X1.X2...Xn.", returns the following values: +// +// basename is the name appearing directly beneath the anchor (Yn). +// +// subname contains any additional labels appearing beneath the anchor (Y1 through Y(n-1)), +// separated by dots. +// +// rootname contains ANCHOR through Xn inclusive, separated by dots. +// +// If no label corresponds to ANCHOR, an error is returned. +// If ANCHOR is the first label, basename is an empty string. +// +// Examples, where anchor="bit": +// "a.b.c.d." -> merr.ErrNotInZone +// "a.b.c.d.bit." -> subname="a.b.c", basename="d", rootname="bit" +// "d.bit." -> subname="", basename="d", rootname="bit" +// "bit." -> subname="", basename="", rootname="bit" +// "bit.x.y.z." -> subname="", basename="", rootname="bit.x.y.z" +// "d.bit.x.y.z." -> subname="", basename="d", rootname="bit.x.y.z" +// "c.d.bit.x.y.z." -> subname="c", basename="d", rootname="bit.x.y.z" +// "a.b.c.d.bit.x.y.z." -> subname="a.b.c", basename="d", rootname="bit.x.y.z" +func SplitDomainByFloatingAnchor(qname, anchor string) (subname, basename, rootname string, err error) { + qname = strings.TrimRight(qname, ".") + parts := strings.Split(qname, ".") + if len(parts) < 2 { + if parts[0] != anchor { + err = merr.ErrNotInZone + return + } + + rootname = parts[0] + return + } + + for i := len(parts) - 1; i >= 0; i-- { + v := parts[i] + + // scanning for rootname + if v == anchor { + if i == 0 { + // i is alreay zero, so we have something like bit.x.y.z. + rootname = qname + return + } + + rootname = strings.Join(parts[i:len(parts)], ".") + basename = parts[i-1] + subname = strings.Join(parts[0:i-1], ".") + return + } + } + + err = merr.ErrNotInZone + return +} + +// Convert a domain name basename (e.g. "example") to a Namecoin domain name +// key name ("d/example"). +func BasenameToNamecoinKey(basename string) (string, error) { + return "d/" + basename, nil +} + +// Convert a Namecoin domain name key name (e.g. "d/example") to a domain name +// basename ("example"). +func NamecoinKeyToBasename(key string) (string, error) { + if strings.HasPrefix(key, "d/") { + return key[2:], nil + } + + return "", fmt.Errorf("not a domain name key") +} + +// This is used to validate NS records, targets in SRV records, etc. In these cases +// an IP address is not allowed. Therefore this regex must exclude all-numeric domain names. +// This is done by requiring the final part to start with an alphabetic character. +var re_hostName = regexp.MustCompilePOSIX(`^(([a-z0-9_][a-z0-9_-]{0,62}\.)*[a-z_][a-z0-9_-]{0,62}\.?|\.)$`) +var re_label = regexp.MustCompilePOSIX(`^[a-z_][a-z0-9_-]*$`) +var re_serviceName = regexp.MustCompilePOSIX(`^[a-z_][a-z0-9_-]*$`) + +func ValidateHostName(name string) bool { + name = dns.Fqdn(name) + return len(name) <= 255 && re_hostName.MatchString(name) +} + +func ValidateLabel(name string) bool { + return len(name) <= 63 && re_label.MatchString(name) +} + +func ValidateServiceName(name string) bool { + return len(name) < 63 && re_serviceName.MatchString(name) +} + +func ValidateEmail(email string) bool { + addr, err := mail.ParseAddress(email) + if addr == nil || err != nil { + return false + } + return addr.Name == "" +} + // © 2014 Hugo Landau GPLv3 or later diff --git a/util/util_test.go b/util/util_test.go index 8a5404e..ad0119d 100644 --- a/util/util_test.go +++ b/util/util_test.go @@ -2,6 +2,7 @@ package util_test import "testing" import "github.com/hlandau/ncdns/util" +import "github.com/hlandau/madns/merr" type item struct { input string @@ -39,3 +40,44 @@ func TestSplitDomainHead(t *testing.T) { } } } + +type aitem struct { + input string + anchor string + expectedSubname string + expectedBasename string + expectedRootname string + expectedError error +} + +var aitems = []aitem{ + aitem{"", "bit", "", "", "", merr.ErrNotInZone}, + aitem{".", "bit", "", "", "", merr.ErrNotInZone}, + aitem{"d.", "bit", "", "", "", merr.ErrNotInZone}, + aitem{"a.b.c.d.", "bit", "", "", "", merr.ErrNotInZone}, + aitem{"a.b.c.d.bit.", "bit", "a.b.c", "d", "bit", nil}, + aitem{"d.bit.", "bit", "", "d", "bit", nil}, + aitem{"bit.", "bit", "", "", "bit", nil}, + aitem{"bit.x.y.z.", "bit", "", "", "bit.x.y.z", nil}, + aitem{"d.bit.x.y.z.", "bit", "", "d", "bit.x.y.z", nil}, + aitem{"c.d.bit.x.y.z.", "bit", "c", "d", "bit.x.y.z", nil}, + aitem{"a.b.c.d.bit.x.y.z.", "bit", "a.b.c", "d", "bit.x.y.z", nil}, +} + +func TestSplitDomainByFloatingAnchor(t *testing.T) { + for i, it := range aitems { + subname, basename, rootname, err := util.SplitDomainByFloatingAnchor(it.input, it.anchor) + if subname != it.expectedSubname { + t.Errorf("Item %d: subname \"%s\" does not equal expected value \"%s\"", i, subname, it.expectedSubname) + } + if basename != it.expectedBasename { + t.Errorf("Item %d: basename \"%s\" does not equal expected value \"%s\"", i, basename, it.expectedBasename) + } + if rootname != it.expectedRootname { + t.Errorf("Item %d: rootname \"%s\" does not equal expected value \"%s\"", i, basename, it.expectedRootname) + } + if err != it.expectedError { + t.Errorf("Item %d: error \"%s\" does not equal expected error \"%s\"", i, err, it.expectedError) + } + } +}