From 986737303994054579b9fb3be64eff34684cb4cb Mon Sep 17 00:00:00 2001 From: Hugo Landau Date: Mon, 1 Dec 2014 00:44:06 +0000 Subject: [PATCH] tests pass --- backend/backend.go | 13 +- namecoin/extratypes/extratypes.go | 72 +++++ namecoin/namecoin.go | 26 ++ ncdomain/convert.go | 478 +++++++++++++++++++++++------- ncdomain/convert_test.go | 202 ++++++++++++- util/util.go | 13 +- 6 files changed, 687 insertions(+), 117 deletions(-) diff --git a/backend/backend.go b/backend/backend.go index 84ad9c1..73c6d95 100644 --- a/backend/backend.go +++ b/backend/backend.go @@ -118,7 +118,7 @@ func (b *Backend) getNamecoinEntryLL(name string) (*domain, error) { log.Info("namecoin query (", name, ") succeeded: ", v) - d, err := b.jsonToDomain(v) + d, err := b.jsonToDomain(name, v) if err != nil { log.Infoe(err, "cannot convert JSON to domain") return nil, err @@ -127,10 +127,10 @@ func (b *Backend) getNamecoinEntryLL(name string) (*domain, error) { return d, nil } -func (b *Backend) jsonToDomain(jsonValue string) (*domain, error) { +func (b *Backend) jsonToDomain(name, jsonValue string) (*domain, error) { d := &domain{} - v, err := ncdomain.ParseValue(jsonValue, b.resolveExtraName) + v, err := ncdomain.ParseValue(name, jsonValue, b.resolveExtraName) if err != nil { return nil, err } @@ -344,10 +344,7 @@ func (tx *btx) _findNCValue(ncv *ncdomain.Value, isubname, subname string, depth } if isubname != "" { - head, rest, err := util.SplitDomainHead(isubname) - if err != nil { - return nil, "", err - } + head, rest := util.SplitDomainHead(isubname) sub, ok := ncv.Map[head] if !ok { @@ -367,7 +364,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)) //convertAt(nil, dns.Fqdn(tx.qname), ncv) + rrs, err = ncv.RRs(nil, dns.Fqdn(tx.qname), dns.Fqdn(tx.basename+"."+tx.rootname)) //convertAt(nil, dns.Fqdn(tx.qname), ncv) return } diff --git a/namecoin/extratypes/extratypes.go b/namecoin/extratypes/extratypes.go index 709c363..b7c081a 100644 --- a/namecoin/extratypes/extratypes.go +++ b/namecoin/extratypes/extratypes.go @@ -196,9 +196,81 @@ func syncReplyParser(m json.RawMessage) (interface{}, error) { return nsr, nil } +// name_filter + +type NameFilterCmd struct { + id interface{} + Regexp string + MaxAge int + From int + Count int +} + +func NewNameFilterCmd(id interface{}, regexp string, maxage, from, count int) (*NameFilterCmd, error) { + return &NameFilterCmd{ + id: id, + Regexp: regexp, + MaxAge: maxage, + From: from, + Count: count, + }, nil +} + +func (c *NameFilterCmd) Id() interface{} { + return c.id +} + +func (c *NameFilterCmd) Method() string { + return "name_filter" +} + +func (c *NameFilterCmd) MarshalJSON() ([]byte, error) { + params := []interface{}{ + c.Regexp, + c.MaxAge, + c.From, + c.Count, + } + + raw, err := btcjson.NewRawCmd(c.id, c.Method(), params) + if err != nil { + return nil, err + } + + return json.Marshal(raw) +} + +func (c *NameFilterCmd) UnmarshalJSON(b []byte) error { + // We don't need to implement this as we are only ever the client. + panic("not implemented") + return nil +} + +type NameFilterReply []NameFilterItem +type NameFilterItem struct { + Name string // "d/example" + Value string // "..." + TxID string + Address string + Height int + ExpiresIn int `json:"expires_in"` + Expired bool +} + +func filterReplyParser(m json.RawMessage) (interface{}, error) { + nsr := NameFilterReply{} + err := json.Unmarshal(m, &nsr) + if err != nil { + return nil, err + } + + return nsr, nil +} + func init() { btcjson.RegisterCustomCmd("name_show", nil, showReplyParser, "name_show ") btcjson.RegisterCustomCmd("name_sync", nil, syncReplyParser, "name_sync ") + btcjson.RegisterCustomCmd("name_filter", nil, filterReplyParser, "name_filter ") } // © 2014 Hugo Landau GPLv3 or later diff --git a/namecoin/namecoin.go b/namecoin/namecoin.go index 7c81f0d..1183151 100644 --- a/namecoin/namecoin.go +++ b/namecoin/namecoin.go @@ -119,4 +119,30 @@ func (nc *Conn) CurHeight() (int, error) { return 0, fmt.Errorf("bad reply") } +func (nc *Conn) Filter(regexp string, maxage, from, count int) (names []extratypes.NameFilterItem, err error) { + cmd, err := extratypes.NewNameFilterCmd(newID(), regexp, maxage, from, count) + if err != nil { + return nil, err + } + + r, err := btcjson.RpcSend(nc.Username, nc.Password, nc.Server, cmd) + if err != nil { + return nil, err + } + + if r.Error != nil { + return nil, r.Error + } + + if r.Result == nil { + return nil, fmt.Errorf("got nil result") + } + + if nsr, ok := r.Result.(extratypes.NameFilterReply); ok { + return []extratypes.NameFilterItem(nsr), nil + } + + return nil, fmt.Errorf("bad reply") +} + // © 2014 Hugo Landau GPLv3 or later diff --git a/ncdomain/convert.go b/ncdomain/convert.go index 6d2600a..745f466 100644 --- a/ncdomain/convert.go +++ b/ncdomain/convert.go @@ -8,48 +8,72 @@ 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 +// 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 +// being used. Non-fully-qualified names are relative to the name apex, and +// should be qualified as such based on whatever the corresponding name for the +// Value is and the zone apex you are using for .bit. These assumptions are not +// built-in for you to give flexibility in where the .bit zone is mounted, +// DNS namespace-wise. If you just call RRs() or RRsRecursive() you don't have +// to worry about any of this. +// +// Because empty values are used to indicate the non-presence of an option +// in some cases, namely for Alias and Translate, the empty string is represented as "=". +// Therefore when qualifying names in a Value yourself you must check if the +// input string is "=" and if so, replace it with "" first. type Value struct { - IP []net.IP - IP6 []net.IP - NS []string - Alias string - Translate string - DS []*dns.DS - TXT [][]string - Service []*dns.SRV // header name contains e.g. "_http._tcp" - Hostmaster string // "hostmaster@example.com" - MX []*dns.MX // header name is left blank - Map map[string]*Value // may contain and "*", will not contain "" + IP []net.IP + IP6 []net.IP + NS []string + Alias string + HasAlias bool // True if Alias was specified. Necessary as "" is a valid relative alias. + Translate string + HasTranslate bool // True if Translate was specified. Necessary as "" is a valid relative value for Translate. + DS []*dns.DS + TXT [][]string + Service []*dns.SRV // header name contains e.g. "_http._tcp" + Hostmaster string // "hostmaster@example.com" + MX []*dns.MX // header name is left blank + TLSA []*dns.TLSA // header name contains e.g. "_443._tcp" + Map map[string]*Value // may contain and "*", will not contain "" + + // set if the value is at the top level (alas necessary for relname interpretation) + IsTopLevel bool } -func (v *Value) RRs(out []dns.RR, suffix string) ([]dns.RR, error) { +func (v *Value) RRs(out []dns.RR, suffix, apexSuffix string) ([]dns.RR, error) { il := len(out) suffix = dns.Fqdn(suffix) + apexSuffix = dns.Fqdn(apexSuffix) - out, _ = v.appendNSs(out, suffix) + out, _ = v.appendNSs(out, suffix, apexSuffix) if len(v.NS) == 0 { - out, _ = v.appendTranslate(out, suffix) - if v.Translate == "" { - out, _ = v.appendAlias(out, suffix) - if v.Alias == "" { - out, _ = v.appendIPs(out, suffix) - out, _ = v.appendIP6s(out, suffix) - out, _ = v.appendTXTs(out, suffix) - out, _ = v.appendServices(out, suffix) - out, _ = v.appendMXs(out, suffix) + out, _ = v.appendTranslate(out, suffix, apexSuffix) + if !v.HasTranslate { + out, _ = v.appendAlias(out, suffix, apexSuffix) + if !v.HasAlias { + out, _ = v.appendIPs(out, suffix, apexSuffix) + out, _ = v.appendIP6s(out, suffix, apexSuffix) + out, _ = v.appendTXTs(out, suffix, apexSuffix) + out, _ = v.appendMXs(out, suffix, apexSuffix) } + // SRV and TLSA records are assigned to a subdomain, but CNAMEs are not recursive so CNAME must not inhibit them + out, _ = v.appendServices(out, suffix, apexSuffix) + out, _ = v.appendTLSA(out, suffix, apexSuffix) } } - out, _ = v.appendDSs(out, suffix) + out, _ = v.appendDSs(out, suffix, apexSuffix) xout := out[il:] for i := range xout { h := xout[i].Header() - if h.Rrtype == dns.TypeSRV { + if rrtypeHasPrefix(h.Rrtype) { h.Name += "." + suffix } else { h.Name = suffix @@ -59,7 +83,11 @@ func (v *Value) RRs(out []dns.RR, suffix string) ([]dns.RR, error) { return out, nil } -func (v *Value) appendIPs(out []dns.RR, suffix string) ([]dns.RR, error) { +func rrtypeHasPrefix(t uint16) bool { + return t == dns.TypeSRV || t == dns.TypeTLSA +} + +func (v *Value) appendIPs(out []dns.RR, suffix, apexSuffix string) ([]dns.RR, error) { for _, ip := range v.IP { out = append(out, &dns.A{ Hdr: dns.RR_Header{ @@ -75,7 +103,7 @@ func (v *Value) appendIPs(out []dns.RR, suffix string) ([]dns.RR, error) { return out, nil } -func (v *Value) appendIP6s(out []dns.RR, suffix string) ([]dns.RR, error) { +func (v *Value) appendIP6s(out []dns.RR, suffix, apexSuffix string) ([]dns.RR, error) { for _, ip := range v.IP6 { out = append(out, &dns.AAAA{ Hdr: dns.RR_Header{ @@ -91,8 +119,13 @@ func (v *Value) appendIP6s(out []dns.RR, suffix string) ([]dns.RR, error) { return out, nil } -func (v *Value) appendNSs(out []dns.RR, suffix string) ([]dns.RR, error) { +func (v *Value) appendNSs(out []dns.RR, suffix, apexSuffix string) ([]dns.RR, error) { for _, ns := range v.NS { + qn, ok := v.qualify(ns, suffix, apexSuffix) + if !ok { + continue + } + out = append(out, &dns.NS{ Hdr: dns.RR_Header{ Name: suffix, @@ -100,14 +133,14 @@ func (v *Value) appendNSs(out []dns.RR, suffix string) ([]dns.RR, error) { Class: dns.ClassINET, Ttl: 600, }, - Ns: dns.Fqdn(ns), + Ns: qn, }) } return out, nil } -func (v *Value) appendTXTs(out []dns.RR, suffix string) ([]dns.RR, error) { +func (v *Value) appendTXTs(out []dns.RR, suffix, apexSuffix string) ([]dns.RR, error) { for _, txt := range v.TXT { out = append(out, &dns.TXT{ Hdr: dns.RR_Header{ @@ -123,7 +156,7 @@ func (v *Value) appendTXTs(out []dns.RR, suffix string) ([]dns.RR, error) { return out, nil } -func (v *Value) appendDSs(out []dns.RR, suffix string) ([]dns.RR, error) { +func (v *Value) appendDSs(out []dns.RR, suffix, apexSuffix string) ([]dns.RR, error) { for _, ds := range v.DS { out = append(out, ds) } @@ -131,7 +164,7 @@ func (v *Value) appendDSs(out []dns.RR, suffix string) ([]dns.RR, error) { return out, nil } -func (v *Value) appendMXs(out []dns.RR, suffix string) ([]dns.RR, error) { +func (v *Value) appendMXs(out []dns.RR, suffix, apexSuffix string) ([]dns.RR, error) { for _, mx := range v.MX { out = append(out, mx) } @@ -139,7 +172,7 @@ func (v *Value) appendMXs(out []dns.RR, suffix string) ([]dns.RR, error) { return out, nil } -func (v *Value) appendServices(out []dns.RR, suffix string) ([]dns.RR, error) { +func (v *Value) appendServices(out []dns.RR, suffix, apexSuffix string) ([]dns.RR, error) { for _, svc := range v.Service { out = append(out, svc) } @@ -147,8 +180,20 @@ func (v *Value) appendServices(out []dns.RR, suffix string) ([]dns.RR, error) { return out, nil } -func (v *Value) appendAlias(out []dns.RR, suffix string) ([]dns.RR, error) { - if v.Alias != "" { +func (v *Value) appendTLSA(out []dns.RR, suffix, apexSuffix string) ([]dns.RR, error) { + for _, tlsa := range v.TLSA { + out = append(out, tlsa) + } + + return out, nil +} + +func (v *Value) appendAlias(out []dns.RR, suffix, apexSuffix string) ([]dns.RR, error) { + if v.HasAlias { + qn, ok := v.qualify(v.Alias, suffix, apexSuffix) + if !ok { + return out, fmt.Errorf("bad alias") + } out = append(out, &dns.CNAME{ Hdr: dns.RR_Header{ Name: suffix, @@ -156,15 +201,19 @@ func (v *Value) appendAlias(out []dns.RR, suffix string) ([]dns.RR, error) { Class: dns.ClassINET, Ttl: 600, }, - Target: v.Alias, + Target: qn, }) } return out, nil } -func (v *Value) appendTranslate(out []dns.RR, suffix string) ([]dns.RR, error) { - if v.Translate != "" { +func (v *Value) appendTranslate(out []dns.RR, suffix, apexSuffix string) ([]dns.RR, error) { + if v.HasTranslate { + qn, ok := v.qualify(v.Translate, suffix, apexSuffix) + if !ok { + return out, fmt.Errorf("bad translate") + } out = append(out, &dns.DNAME{ Hdr: dns.RR_Header{ Name: suffix, @@ -172,15 +221,15 @@ func (v *Value) appendTranslate(out []dns.RR, suffix string) ([]dns.RR, error) { Class: dns.ClassINET, Ttl: 600, }, - Target: v.Translate, + Target: qn, }) } return out, nil } -func (v *Value) RRsRecursive(out []dns.RR, suffix string) ([]dns.RR, error) { - out, err := v.RRs(out, suffix) +func (v *Value) RRsRecursive(out []dns.RR, suffix, apexSuffix string) ([]dns.RR, error) { + out, err := v.RRs(out, suffix, apexSuffix) if err != nil { return nil, err } @@ -190,7 +239,7 @@ func (v *Value) RRsRecursive(out []dns.RR, suffix string) ([]dns.RR, error) { continue } - out, err = mv.RRsRecursive(out, mk+"."+suffix) + out, err = mv.RRsRecursive(out, mk+"."+suffix, apexSuffix) //if err != nil { // return nil, err //} @@ -199,6 +248,24 @@ func (v *Value) RRsRecursive(out []dns.RR, suffix string) ([]dns.RR, error) { return out, nil } +func (v *Value) findSubdomainByName(subdomain string) (*Value, error) { + if subdomain == "" { + return v, nil + } + + if strings.HasSuffix(subdomain, ".") { + return nil, fmt.Errorf("a subdomain name should not be fully qualified") + } + + head, rest := util.SplitDomainHead(subdomain) + + if sub, ok := v.Map[head]; ok { + return sub.findSubdomainByName(rest) + } + + return nil, fmt.Errorf("subdomain part not found: %s", head) +} + type rawValue struct { IP interface{} `json:"ip"` IP6 interface{} `json:"ip6"` @@ -211,6 +278,7 @@ type rawValue struct { TXT interface{} `json:"txt"` Hostmaster interface{} `json:"email"` // Hostmaster MX interface{} `json:"mx"` + TLSA interface{} `json:"tlsa"` Map json.RawMessage `json:"map"` @@ -228,7 +296,7 @@ type ResolveFunc func(name string) (string, error) // Namecoin form (e.g. "d/example"). The JSON value or an error should be // returned. If no ResolveFunc is passed, "import" and "delegate" statements // always fail. -func ParseValue(jsonValue string, resolve ResolveFunc) (value *Value, err error) { +func ParseValue(name, jsonValue string, resolve ResolveFunc) (value *Value, err error) { rv := &rawValue{} v := &Value{} @@ -243,40 +311,93 @@ func ParseValue(jsonValue string, resolve ResolveFunc) (value *Value, err error) } } - rv.parse(v, resolve, 0, 0) + mergedNames := map[string]struct{}{} + mergedNames[name] = struct{}{} + + rv.parse(v, resolve, 0, 0, "", "", mergedNames) + v.IsTopLevel = true value = v return } -func (rv *rawValue) parse(v *Value, resolve ResolveFunc, depth, mergeDepth int) error { +func (rv *rawValue) parse(v *Value, resolve ResolveFunc, depth, mergeDepth int, subdomain, relname string, mergedNames map[string]struct{}) error { if depth > depthLimit { return fmt.Errorf("depth limit exceeded") } - ok, _ := rv.parseDelegate(v, resolve, depth, mergeDepth) + realv := v + if subdomain != "" { + // substitute a dummy value. We will then parse everything into this, find the appropriate level and copy + // the value to the argument value. + v = &Value{} + } + + ok, _ := rv.parseDelegate(v, resolve, depth, mergeDepth, relname, mergedNames) if ok { return nil } - rv.parseImport(v, resolve, depth, mergeDepth) + rv.parseImport(v, resolve, depth, mergeDepth, relname, mergedNames) rv.parseIP(v, rv.IP, false) rv.parseIP(v, rv.IP6, true) - rv.parseNS(v) - rv.parseAlias(v) - rv.parseTranslate(v) + rv.parseNS(v, relname) + rv.parseAlias(v, relname) + rv.parseTranslate(v, relname) rv.parseHostmaster(v) rv.parseDS(v) rv.parseTXT(v) - rv.parseService(v) - rv.parseMX(v) - rv.parseMap(v, resolve, depth, mergeDepth) + rv.parseService(v, relname) + rv.parseMX(v, relname) + rv.parseTLSA(v) + rv.parseMap(v, resolve, depth, mergeDepth, relname) v.moveEmptyMapItems() + if subdomain != "" { + subv, err := v.findSubdomainByName(subdomain) + if err != nil { + return err + } + *realv = *subv + } + return nil } -func (rv *rawValue) parseMerge(mergeValue string, v *Value, resolve ResolveFunc, depth, mergeDepth int) error { +func (v *Value) qualifyIntl(name, suffix, apexSuffix string) string { + if strings.HasSuffix(name, ".") { + return name + } + + if !v.IsTopLevel { + _, suffix = util.SplitDomainTail(suffix) + } + + if name == "" { + return suffix + } + + if name == "@" { + return apexSuffix + } + + if strings.HasSuffix(name, ".@") { + return name[0:len(name)-2] + "." + apexSuffix + } + + return name + "." + suffix +} + +func (v *Value) qualify(name, suffix, apexSuffix string) (string, bool) { + s := v.qualifyIntl(name, suffix, apexSuffix) + if !validateHostName(s) { + return "", false + } + + return s, true +} + +func (rv *rawValue) parseMerge(mergeValue string, v *Value, resolve ResolveFunc, depth, mergeDepth int, subdomain, relname string, mergedNames map[string]struct{}) error { rv2 := &rawValue{} if mergeDepth > mergeDepthLimit { @@ -288,7 +409,7 @@ func (rv *rawValue) parseMerge(mergeValue string, v *Value, resolve ResolveFunc, return err } - return rv2.parse(v, resolve, depth, mergeDepth) + return rv2.parse(v, resolve, depth, mergeDepth, subdomain, relname, mergedNames) } func (rv *rawValue) parseIP(v *Value, ipi interface{}, ipv6 bool) { @@ -330,7 +451,7 @@ func (rv *rawValue) addIP(v *Value, ips string, ipv6 bool) error { return nil } -func (rv *rawValue) parseNS(v *Value) error { +func (rv *rawValue) parseNS(v *Value, relname string) error { // "dns" takes precedence if rv.DNS != nil { rv.NS = rv.DNS @@ -353,23 +474,19 @@ func (rv *rawValue) parseNS(v *Value) error { if !ok { continue } - rv.addNS(v, s) + rv.addNS(v, s, relname) } return nil case string: s := rv.NS.(string) - rv.addNS(v, s) + rv.addNS(v, s, relname) return nil default: return fmt.Errorf("unknown NS field format") } } -func (rv *rawValue) addNS(v *Value, s string) error { - if !validateHostName(s) { - return fmt.Errorf("malformed NS hostname") - } - +func (rv *rawValue) addNS(v *Value, s, relname string) error { if _, ok := rv.nsSet[s]; !ok { v.NS = append(v.NS, s) rv.nsSet[s] = struct{}{} @@ -378,70 +495,133 @@ func (rv *rawValue) addNS(v *Value, s string) error { return nil } -func (rv *rawValue) parseAlias(v *Value) error { +func (rv *rawValue) parseAlias(v *Value, relname string) error { if rv.Alias == nil { return nil } if s, ok := rv.Alias.(string); ok { - if !validateHostName(s) { - return fmt.Errorf("malformed hostname in alias field") - } - v.Alias = s + v.HasAlias = true return nil } return fmt.Errorf("unknown alias field format") } -func (rv *rawValue) parseTranslate(v *Value) error { +func (rv *rawValue) parseTranslate(v *Value, relname string) error { if rv.Translate == nil { return nil } if s, ok := rv.Translate.(string); ok { - if !validateHostName(s) { - return fmt.Errorf("malformed hostname in translate field") - } - v.Translate = s + v.HasTranslate = true return nil } return fmt.Errorf("unknown translate field format") } -func (rv *rawValue) parseImport(v *Value, resolve ResolveFunc, depth, mergeDepth int) error { - if rv.Import == nil { - return nil - } - - if s, ok := rv.Import.(string); ok { - dv, err := resolve(s) - if err == nil { - err = rv.parseMerge(dv, v, resolve, depth, mergeDepth+1) +func isAllArray(x []interface{}) bool { + for _, v := range x { + if _, ok := v.([]interface{}); !ok { + return false } - return err } - - return fmt.Errorf("unknown import field format") + return true } -func (rv *rawValue) parseDelegate(v *Value, resolve ResolveFunc, depth, mergeDepth int) (bool, error) { - if rv.Delegate == nil { +func isAllString(x []interface{}) bool { + for _, v := range x { + if _, ok := v.(string); !ok { + return false + } + } + return true +} + +func (rv *rawValue) parseImportImpl(val *Value, resolve ResolveFunc, depth, mergeDepth int, relname string, mergedNames map[string]struct{}, delegate bool) (bool, error) { + var err error + succeeded := false + src := rv.Import + if delegate { + src = rv.Delegate + } + + if src == nil { return false, nil } - if s, ok := rv.Delegate.(string); ok { - dv, err := resolve(s) - if err == nil { - err = rv.parseMerge(dv, v, resolve, depth, mergeDepth+1) - } - return true, err + if s, ok := src.(string); ok { + src = []interface{}{s} } - return false, fmt.Errorf("unknown delegate field format") + if a, ok := src.([]interface{}); ok { + // [..., ..., ...] + + if isAllString(a) { + // ["s/somedomain"] + // ["s/somedomain", "sub.domain"] + a = []interface{}{a} + } + + if isAllArray(a) { + // [ ["s/somedomain", "sub.domain"], ["s/somedomain", "sub.domain"] ] + for _, vx := range a { + v := vx.([]interface{}) + if len(v) != 1 && len(v) != 2 { + continue + } + + subs := "" + if k, ok := v[0].(string); ok { + if len(v) > 1 { + if sub, ok := v[1].(string); ok { + subs = sub + } + } + + // ok + var dv string + dv, err = resolve(k) + if err != nil { + continue + } + + if _, ok := mergedNames[k]; ok { + // already merged + continue + } + + mergedNames[k] = struct{}{} + + err = rv.parseMerge(dv, val, resolve, depth, mergeDepth+1, subs, relname, mergedNames) + if err != nil { + continue + } + + succeeded = true + } + } + } + // malformed + } + + if err == nil { + err = fmt.Errorf("unknown import/delegate field format") + } + + return succeeded, err +} + +func (rv *rawValue) parseImport(v *Value, resolve ResolveFunc, depth, mergeDepth int, relname string, mergedNames map[string]struct{}) error { + _, err := rv.parseImportImpl(v, resolve, depth, mergeDepth, relname, mergedNames, false) + return err +} + +func (rv *rawValue) parseDelegate(v *Value, resolve ResolveFunc, depth, mergeDepth int, relname string, mergedNames map[string]struct{}) (bool, error) { + return rv.parseImportImpl(v, resolve, depth, mergeDepth, relname, mergedNames, true) } func (rv *rawValue) parseHostmaster(v *Value) error { @@ -511,9 +691,81 @@ func (rv *rawValue) parseDS(v *Value) error { } } } + return fmt.Errorf("malformed DS field format") } +func (rv *rawValue) parseTLSA(v *Value) error { + if rv.TLSA == nil { + return nil + } + + v.TLSA = nil + + if tlsaa, ok := rv.TLSA.([]interface{}); ok { + for _, tlsa1 := range tlsaa { + if tlsa, ok := tlsa1.([]interface{}); ok { + // Format: ["443", "tcp", 1, 2, 3, "base64 certificate data"] + if len(tlsa) < 6 { + continue + } + + ports, ok := tlsa[0].(string) + if !ok { + porti, ok := tlsa[0].(float64) + if !ok { + continue + } + ports = fmt.Sprintf("%d", int(porti)) + } + + transport, ok := tlsa[1].(string) + if !ok { + continue + } + + a1, ok := tlsa[2].(float64) + if !ok { + continue + } + + a2, ok := tlsa[3].(float64) + if !ok { + continue + } + + a3, ok := tlsa[4].(float64) + if !ok { + continue + } + + a4, ok := tlsa[5].(string) + if !ok { + continue + } + + a4b, err := base64.StdEncoding.DecodeString(a4) + if err != nil { + continue + } + + a4h := hex.EncodeToString(a4b) + name := "_" + ports + "._" + transport + + v.TLSA = append(v.TLSA, &dns.TLSA{ + Hdr: dns.RR_Header{Name: name, Rrtype: dns.TypeTLSA, Class: dns.ClassINET, Ttl: 600}, + Usage: uint8(a1), + Selector: uint8(a2), + MatchingType: uint8(a3), + Certificate: strings.ToUpper(a4h), + }) + } + } + } + + return fmt.Errorf("malformed TLSA field format") +} + func (rv *rawValue) parseTXT(v *Value) error { if rv.TXT == nil { return nil @@ -578,21 +830,21 @@ func segmentizeTXT(txt string) (a []string) { return } -func (rv *rawValue) parseMX(v *Value) error { +func (rv *rawValue) parseMX(v *Value, relname string) error { if rv.MX == nil { return nil } if sa, ok := rv.MX.([]interface{}); ok { for _, s := range sa { - rv.parseSingleMX(s, v) + rv.parseSingleMX(s, v, relname) } } return fmt.Errorf("malformed MX value") } -func (rv *rawValue) parseSingleMX(s interface{}, v *Value) error { +func (rv *rawValue) parseSingleMX(s interface{}, v *Value, relname string) error { sa, ok := s.([]interface{}) if !ok { return fmt.Errorf("malformed MX value") @@ -608,7 +860,7 @@ func (rv *rawValue) parseSingleMX(s interface{}, v *Value) error { } hostname, ok := sa[1].(string) - if !ok || !validateHostName(hostname) { + if !ok { return fmt.Errorf("malformed MX value") } @@ -621,21 +873,33 @@ func (rv *rawValue) parseSingleMX(s interface{}, v *Value) error { return nil } -func (rv *rawValue) parseService(v *Value) error { +func (rv *rawValue) parseService(v *Value, relname string) error { if rv.Service == nil { return nil } + // We have to merge the services specified and those imported using an + // import statement. + servicesUsed := map[string]struct{}{} + oldServices := v.Service + v.Service = nil + if sa, ok := rv.Service.([]interface{}); ok { for _, s := range sa { - rv.parseSingleService(s, v) + rv.parseSingleService(s, v, relname, servicesUsed) + } + } + + for _, svc := range oldServices { + if _, ok := servicesUsed[svc.Header().Name]; !ok { + v.Service = append(v.Service, svc) } } return fmt.Errorf("malformed service value") } -func (rv *rawValue) parseSingleService(svc interface{}, v *Value) error { +func (rv *rawValue) parseSingleService(svc interface{}, v *Value, relname string, servicesUsed map[string]struct{}) error { svca, ok := svc.([]interface{}) if !ok { return fmt.Errorf("malformed service value") @@ -671,13 +935,16 @@ func (rv *rawValue) parseSingleService(svc interface{}, v *Value) error { } hostname, ok := svca[5].(string) - if !ok || !validateHostName(hostname) { + if !ok { return fmt.Errorf("malformed service value") } + sname := "_" + appProtoName + "._" + transportProtoName + servicesUsed[sname] = struct{}{} + v.Service = append(v.Service, &dns.SRV{ Hdr: dns.RR_Header{ - Name: "_" + appProtoName + "._" + transportProtoName, + Name: sname, Rrtype: dns.TypeSRV, Class: dns.ClassINET, Ttl: 600, @@ -691,7 +958,7 @@ func (rv *rawValue) parseSingleService(svc interface{}, v *Value) error { return nil } -func (rv *rawValue) parseMap(v *Value, resolve ResolveFunc, depth, mergeDepth int) error { +func (rv *rawValue) parseMap(v *Value, resolve ResolveFunc, depth, mergeDepth int, relname string) error { m := map[string]json.RawMessage{} err := json.Unmarshal(rv.Map, &m) @@ -717,7 +984,8 @@ func (rv *rawValue) parseMap(v *Value, resolve ResolveFunc, depth, mergeDepth in } } - rv2.parse(v2, resolve, depth+1, mergeDepth) + mergedNames := map[string]struct{}{} + rv2.parse(v2, resolve, depth+1, mergeDepth, "", relname, mergedNames) if v.Map == nil { v.Map = make(map[string]*Value) @@ -776,7 +1044,7 @@ func (v *Value) moveEmptyMapItems() error { // 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_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_-]*$`) diff --git a/ncdomain/convert_test.go b/ncdomain/convert_test.go index 973102d..17177be 100644 --- a/ncdomain/convert_test.go +++ b/ncdomain/convert_test.go @@ -1,12 +1,208 @@ package ncdomain_test import "github.com/hlandau/ncdns/ncdomain" -import "github.com/miekg/dns" +import _ "github.com/hlandau/nctestsuite" import "testing" -import "net" import "fmt" import "strings" +import "os" +import "path/filepath" +import "bufio" +import "io" +import "sort" +import "unicode" +import "unicode/utf8" +//import "github.com/miekg/dns" +//import "net" + +type testItem struct { + ID string + Names map[string]string + Records string +} + +func stripTag(L string) string { + if len(L) < 3 { + return L + } + + r, _ := utf8.DecodeRuneInString(L) + + if unicode.IsUpper(r) { + x := strings.IndexRune(L, ' ') + L = L[x+1:] + } + + return L +} + +func suiteReader(t *testing.T) <-chan testItem { + testItemChan := make(chan testItem, 20) + + go func() { + gopath := os.Getenv("GOPATH") + if gopath == "" { + gopath = "." + } + + fpath := filepath.Join(gopath, "src/github.com/hlandau/nctestsuite/testsuite.txt") + f, err := os.Open(fpath) + + if err != nil { + t.Fatalf("Error: Couldn't open %s: %+v", fpath, err) + } + defer f.Close() + + lineChan := make(chan string, 20) + syncChan := make(chan struct{}) + + go func() { + reissue := false + var L string + var ok bool + for { + if reissue { + reissue = false + } else { + L, ok = <-lineChan + } + + if !ok { + break + } + + m := map[string]string{} + + if L != "IN" && !strings.HasPrefix(L, "IN ") { + t.Fatalf("invalid test suite file") + } + + id := "" + if len(L) > 2 { + id = L[3:] + } + + for { + name := <-lineChan + value := <-lineChan + m[name] = value + + L = <-lineChan + if L != "IN" { + break + } + } + + if L != "OUT" { + t.Fatalf("invalid test suite file") + } + + records := []string{} + for { + L, ok = <-lineChan + if !ok || L == "IN" || strings.HasPrefix(L, "IN ") { + reissue = true + break + } + + L = stripTag(L) + records = append(records, L) + } + + sort.Strings(records) + + // process records + ti := testItem{ + ID: id, + Names: m, + Records: strings.Join(records, "\n"), + } + + testItemChan <- ti + } + + close(testItemChan) + close(syncChan) + }() + + r := bufio.NewReader(f) + for { + L, err := r.ReadString('\n') + if err != nil { + if err != io.EOF { + t.Errorf("error while reading line: %+v", err) + } + break + } + + L = strings.Trim(L, " \t\r\n") + if L == "" || (len(L) > 0 && L[0] == '#') { + continue + } + + lineChan <- L + } + close(lineChan) + <-syncChan + }() + + return testItemChan +} + +func TestSuite(t *testing.T) { + items := suiteReader(t) + for ti := range items { + resolve := func(name string) (string, error) { + v, ok := ti.Names[name] + if !ok { + return "", fmt.Errorf("not found") + } + + return v, nil + } + + for k, jsonValue := range ti.Names { + dnsName, err := convertName(k) + if err != nil { + continue + } + + v, err := ncdomain.ParseValue(k, jsonValue, resolve) + if err != nil { + // TODO + continue + } + + rrstrs := []string{} + rrs, _ := v.RRsRecursive(nil, dnsName+".bit.", dnsName+".bit.") + for _, rr := range rrs { + s := rr.String() + s = strings.Replace(s, "\t600\t", "\t", -1) // XXX + rrstrs = append(rrstrs, strings.Replace(s, "\t", " ", -1)) + } + sort.Strings(rrstrs) + rrstr := strings.Join(rrstrs, "\n") + + // CHECK MATCH + if rrstr != ti.Records { + t.Errorf("Didn't match: %s\n%+v\n !=\n%+v\n\n%#v\n\n%#v", ti.ID, rrstr, ti.Records, v, rrs) + } + } + } +} + +func convertName(n string) (string, error) { + if len(n) < 3 || len(n) > 65 { + return "", fmt.Errorf("invalid name") + } + if n[0:2] != "d/" { + return "", fmt.Errorf("not a domain") + } + return n[2:], nil +} + +/* type item struct { jsonValue string value *ncdomain.Value @@ -195,4 +391,4 @@ func eqValueMap(a *ncdomain.Value, b *ncdomain.Value) bool { } return true -} +}*/ diff --git a/util/util.go b/util/util.go index a7e30db..20b01c6 100644 --- a/util/util.go +++ b/util/util.go @@ -3,7 +3,7 @@ package util import "strings" // 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 string, rest string, err error) { +func SplitDomainHead(name string) (head, rest string) { if len(name) > 0 && name[len(name)-1] == '.' { name = name[0 : len(name)-1] } @@ -19,4 +19,15 @@ func SplitDomainHead(name string) (head string, rest string, err error) { return } +// Split a domain name a.b.c.d.e into parts a (the tail) and b.c.d.e (the rest). +func SplitDomainTail(name string) (tail, rest string) { + s := strings.SplitN(name, ".", 2) + + if len(s) == 1 { + return s[0], "" + } + + return s[0], s[1] +} + // © 2014 Hugo Landau GPLv3 or later