diff --git a/backend/backend.go b/backend/backend.go index eb21a05..98f091b 100644 --- a/backend/backend.go +++ b/backend/backend.go @@ -374,83 +374,7 @@ func (tx *btx) _findNCValue(ncv *ncValue, isubname, subname string, depth int, } func (tx *btx) addAnswersUnderNCValueActual(ncv *ncValue, sn string) (rrs []dns.RR, err error) { - // A - ips, err := ncv.GetIPs() - if err != nil { - return - } - - for _, ip := range ips { - pip := net.ParseIP(ip) - if pip == nil || pip.To4() == nil { - continue - } - rrs = append(rrs, &dns.A{ - Hdr: dns.RR_Header{Name: dns.Fqdn(tx.qname), Rrtype: dns.TypeA, Class: dns.ClassINET, Ttl: 600}, - A: pip}) - } - - // AAAA - ips, err = ncv.GetIP6s() - if err != nil { - return - } - - for _, ip := range ips { - pip := net.ParseIP(ip) - if pip == nil || pip.To4() != nil { - continue - } - rrs = append(rrs, &dns.AAAA{ - Hdr: dns.RR_Header{Name: dns.Fqdn(tx.qname), Rrtype: dns.TypeAAAA, Class: dns.ClassINET, Ttl: 600}, - AAAA: pip}) - } - - // NS - nss, err := ncv.GetNSs() - if err != nil { - return - } - - for _, ns := range nss { - ns = dns.Fqdn(ns) - rrs = append(rrs, &dns.NS{ - Hdr: dns.RR_Header{Name: dns.Fqdn(tx.qname), Rrtype: dns.TypeNS, Class: dns.ClassINET, Ttl: 600}, - Ns: ns}) - } - - // TXT - txts, err := ncv.GetTXTs() - if err != nil { - return - } - - for _, txt := range txts { - rrs = append(rrs, &dns.TXT{ - Hdr: dns.RR_Header{Name: dns.Fqdn(tx.qname), Rrtype: dns.TypeTXT, Class: dns.ClassINET, Ttl: 600}, - Txt: txt}) - } - - // TODO: MX - // TODO: SRV - - // DS - dss, err := ncv.GetDSs() - if err != nil { - return - } - - for i := range dss { - dss[i].Hdr.Name = dns.Fqdn(tx.qname) - rrs = append(rrs, &dss[i]) - } - - if len(rrs) == 0 { - if m, ok := ncv.Map[""]; ok { - return tx.addAnswersUnderNCValueActual(m, sn) - } - } - + rrs = convertAt(nil, dns.Fqdn(tx.qname), ncv) return } diff --git a/backend/convert.go b/backend/convert.go index 880d65c..0c897b3 100644 --- a/backend/convert.go +++ b/backend/convert.go @@ -1,7 +1,9 @@ package backend +/* import "github.com/miekg/dns" import "net" +import "regexp" // Experimental attempt to factor out the JSON->DNS conversion function. // Currently used only by namesync, not ncdns. @@ -15,40 +17,64 @@ func Convert(suffix string, jsonValue string) ([]dns.RR, error) { } rootNCV := d.ncv - rrs := convertRecursive(suffix, rootNCV, 0) + rrs := convertRecursive(nil, suffix, rootNCV, 0) return rrs, nil } // Try and tolerate errors. -func convertRecursive(suffix string, ncv *ncValue, depth int) (rrs []dns.RR) { +func convertRecursive(out []dns.RR, suffix string, ncv *ncValue, depth int) []dns.RR { if depth > 64 { - return + return out } - rrs = append(rrs, convertIPs(suffix, ncv)...) - rrs = append(rrs, convertIP6s(suffix, ncv)...) - //rrs = append(rrs, ...convertServices(suffix, ncv)) - //rrs = append(rrs, ...convertAlias(suffix, ncv)) - rrs = append(rrs, convertNSs(suffix, ncv)...) + out = convertAt(out, suffix, ncv) for k, v := range ncv.Map { subsuffix := k + "." + suffix if k == "" { subsuffix = suffix } - rrs = append(rrs, convertRecursive(subsuffix, v, depth+1)...) + out = convertRecursive(out, subsuffix, v, depth+1) } - rrs = append(rrs, convertDSs(suffix, ncv)...) - //rrs = append(rrs, ...convertTXT(suffix, ncv)) - return + return out } -func convertIPs(suffix string, ncv *ncValue) (rrs []dns.RR) { +// Conversion at a specific NCV non-recursivey for all types + +func convertAt(out []dns.RR, suffix string, ncv *ncValue) []dns.RR { + return convertAt_(out, suffix, ncv, 0) +} + +func convertAt_(out []dns.RR, suffix string, ncv *ncValue, depth int) []dns.RR { + if depth > 1 { + return out + } + + out = convertIPs(out, suffix, ncv) + out = convertIP6s(out, suffix, ncv) + out = convertNSs(out, suffix, ncv) + out = convertDSs(out, suffix, ncv) + out = convertTXTs(out, suffix, ncv) + + // XXX: should this apply only if no records were added above? + if m, ok := ncv.Map[""]; ok { + out = convertAt_(out, suffix, m, depth+1) + } + + // TODO: CNAME + // TODO: MX + // TODO: SRV + return out +} + +// Conversion at a specific NCV non-recursively for specific types + +func convertIPs(out []dns.RR, suffix string, ncv *ncValue) []dns.RR { ips, err := ncv.GetIPs() if err != nil { - return + return out } for _, ip := range ips { @@ -57,19 +83,19 @@ func convertIPs(suffix string, ncv *ncValue) (rrs []dns.RR) { continue } - rrs = append(rrs, &dns.A{ + out = append(out, &dns.A{ Hdr: dns.RR_Header{Name: dns.Fqdn(suffix), Rrtype: dns.TypeA, Class: dns.ClassINET, Ttl: 600}, A: pip, }) } - return + return out } -func convertIP6s(suffix string, ncv *ncValue) (rrs []dns.RR) { +func convertIP6s(out []dns.RR, suffix string, ncv *ncValue) []dns.RR { ips, err := ncv.GetIP6s() if err != nil { - return + return out } for _, ip := range ips { @@ -78,42 +104,71 @@ func convertIP6s(suffix string, ncv *ncValue) (rrs []dns.RR) { continue } - rrs = append(rrs, &dns.AAAA{ + out = append(out, &dns.AAAA{ Hdr: dns.RR_Header{Name: dns.Fqdn(suffix), Rrtype: dns.TypeAAAA, Class: dns.ClassINET, Ttl: 600}, AAAA: pip, }) } - return + return out } -func convertNSs(suffix string, ncv *ncValue) (rrs []dns.RR) { +func convertNSs(out []dns.RR, suffix string, ncv *ncValue) []dns.RR { nss, err := ncv.GetNSs() if err != nil { - return + return out } for _, ns := range nss { + if !validateHostName(ns) { + continue + } + ns = dns.Fqdn(ns) - rrs = append(rrs, &dns.NS{ + out = append(out, &dns.NS{ Hdr: dns.RR_Header{Name: dns.Fqdn(suffix), Rrtype: dns.TypeNS, Class: dns.ClassINET, Ttl: 600}, Ns: ns, }) } - return + return out +} + +func convertTXTs(out []dns.RR, suffix string, ncv *ncValue) []dns.RR { + txts, err := ncv.GetTXTs() + if err != nil { + return out + } + + for _, txt := range txts { + out = append(out, &dns.TXT{ + Hdr: dns.RR_Header{Name: dns.Fqdn(suffix), Rrtype: dns.TypeTXT, Class: dns.ClassINET, Ttl: 600}, + Txt: txt, + }) + } + + return out } -func convertDSs(suffix string, ncv *ncValue) (rrs []dns.RR) { +func convertDSs(out []dns.RR, suffix string, ncv *ncValue) []dns.RR { dss, err := ncv.GetDSs() if err != nil { - return + return out } for i := range dss { dss[i].Hdr.Name = dns.Fqdn(suffix) - rrs = append(rrs, &dss[i]) + out = append(out, &dss[i]) } - return + return out } + +// Validation functions + +var re_hostName = 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) +}*/ diff --git a/ncdomain/convert.go b/ncdomain/convert.go new file mode 100644 index 0000000..560a1be --- /dev/null +++ b/ncdomain/convert.go @@ -0,0 +1,746 @@ +package ncdomain + +import "encoding/json" +import "net" +import "fmt" +import "github.com/miekg/dns" +import "encoding/base64" +import "encoding/hex" +import "regexp" +import "net/mail" + +const depthLimit = 16 +const mergeDepthLimit = 4 + +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 "" +} + +func (v *Value) RRs(out []dns.RR, suffix string) ([]dns.RR, error) { + il := len(out) + suffix = dns.Fqdn(suffix) + + out, _ = v.appendIPs(out, suffix) + out, _ = v.appendIP6s(out, suffix) + out, _ = v.appendNSs(out, suffix) + out, _ = v.appendTXTs(out, suffix) + out, _ = v.appendDSs(out, suffix) + out, _ = v.appendServices(out, suffix) + out, _ = v.appendMXs(out, suffix) + out, _ = v.appendAlias(out, suffix) + out, _ = v.appendTranslate(out, suffix) + + xout := out[il:] + for i := range xout { + xout[i].Header().Name = suffix + } + + return out, nil +} + +func (v *Value) appendIPs(out []dns.RR, suffix string) ([]dns.RR, error) { + for _, ip := range v.IP { + out = append(out, &dns.A{ + Hdr: dns.RR_Header{ + Name: suffix, + Rrtype: dns.TypeA, + Class: dns.ClassINET, + Ttl: 600, + }, + A: ip, + }) + } + + return out, nil +} + +func (v *Value) appendIP6s(out []dns.RR, suffix string) ([]dns.RR, error) { + for _, ip := range v.IP6 { + out = append(out, &dns.AAAA{ + Hdr: dns.RR_Header{ + Name: suffix, + Rrtype: dns.TypeAAAA, + Class: dns.ClassINET, + Ttl: 600, + }, + AAAA: ip, + }) + } + + return out, nil +} + +func (v *Value) appendNSs(out []dns.RR, suffix string) ([]dns.RR, error) { + for _, ns := range v.NS { + out = append(out, &dns.NS{ + Hdr: dns.RR_Header{ + Name: suffix, + Rrtype: dns.TypeNS, + Class: dns.ClassINET, + Ttl: 600, + }, + Ns: ns, + }) + } + + return out, nil +} + +func (v *Value) appendTXTs(out []dns.RR, suffix string) ([]dns.RR, error) { + for _, txt := range v.TXT { + out = append(out, &dns.TXT{ + Hdr: dns.RR_Header{ + Name: suffix, + Rrtype: dns.TypeTXT, + Class: dns.ClassINET, + Ttl: 600, + }, + Txt: txt, + }) + } + + return out, nil +} + +func (v *Value) appendDSs(out []dns.RR, suffix string) ([]dns.RR, error) { + for _, ds := range v.DS { + out = append(out, ds) + } + + return out, nil +} + +func (v *Value) appendMXs(out []dns.RR, suffix string) ([]dns.RR, error) { + for _, mx := range v.MX { + out = append(out, mx) + } + + return out, nil +} + +func (v *Value) appendServices(out []dns.RR, suffix string) ([]dns.RR, error) { + for _, svc := range v.Service { + out = append(out, svc) + } + + return out, nil +} + +func (v *Value) appendAlias(out []dns.RR, suffix string) ([]dns.RR, error) { + if v.Alias != "" { + out = append(out, &dns.CNAME{ + Hdr: dns.RR_Header{ + Name: suffix, + Rrtype: dns.TypeCNAME, + Class: dns.ClassINET, + Ttl: 600, + }, + Target: v.Alias, + }) + } + + return out, nil +} + +func (v *Value) appendTranslate(out []dns.RR, suffix string) ([]dns.RR, error) { + if v.Translate != "" { + out = append(out, &dns.DNAME{ + Hdr: dns.RR_Header{ + Name: suffix, + Rrtype: dns.TypeDNAME, + Class: dns.ClassINET, + Ttl: 600, + }, + Target: v.Translate, + }) + } + + return out, nil +} + +func (v *Value) RRsRecursive(out []dns.RR, suffix string) ([]dns.RR, error) { + out, err := v.RRs(out, suffix) + if err != nil { + return nil, err + } + + for mk, mv := range v.Map { + out, err = mv.RRsRecursive(out, mk+"."+suffix) + if err != nil { + return nil, err + } + } + + return out, nil +} + +type rawValue struct { + IP interface{} `json:"ip"` + IP6 interface{} `json:"ip6"` + NS interface{} `json:"ns"` + DNS interface{} `json:"dns"` // actually an alias for NS + Alias interface{} `json:"alias"` + Translate interface{} `json:"translate"` + DS interface{} `json:"ds"` + TXT interface{} `json:"txt"` + Hostmaster interface{} `json:"email"` // Hostmaster + MX interface{} `json:"mx"` + + Map json.RawMessage `json:"map"` + + Service interface{} `json:"service"` + Import interface{} `json:"import"` + Delegate interface{} `json:"delegate"` +} + +type ResolveFunc func(name string) (string, error) + +// Call to convert a given JSON value to a parsed Namecoin domain value. +// +// If ResolveFunc is given, it will be called to obtain the values for domains +// referenced by "import" and "delegate" statements. The name passed is in +// 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) { + rv := &rawValue{} + v := &Value{} + + err = json.Unmarshal([]byte(jsonValue), rv) + if err != nil { + return + } + + if resolve == nil { + resolve = func(name string) (string, error) { + return "", fmt.Errorf("not supported") + } + } + + rv.parse(v, resolve, 0, 0) + + value = v + return +} + +func (rv *rawValue) parse(v *Value, resolve ResolveFunc, depth, mergeDepth int) error { + if depth > depthLimit { + return fmt.Errorf("depth limit exceeded") + } + + ok, _ := rv.parseDelegate(v, resolve, depth, mergeDepth) + if ok { + return nil + } + + rv.parseImport(v, resolve, depth, mergeDepth) + rv.parseIP(v, rv.IP, false) + rv.parseIP(v, rv.IP6, true) + rv.parseNS(v) + rv.parseAlias(v) + rv.parseTranslate(v) + rv.parseHostmaster(v) + rv.parseDS(v) + rv.parseTXT(v) + rv.parseService(v) + rv.parseMX(v) + rv.parseMap(v, resolve, depth, mergeDepth) + v.moveEmptyMapItems() + + return nil +} + +func (rv *rawValue) parseMerge(mergeValue string, v *Value, resolve ResolveFunc, depth, mergeDepth int) error { + rv2 := &rawValue{} + + if mergeDepth > mergeDepthLimit { + return fmt.Errorf("merge depth limit exceeded") + } + + err := json.Unmarshal([]byte(mergeValue), rv2) + if err != nil { + return err + } + + return rv2.parse(v, resolve, depth, mergeDepth) +} + +func (rv *rawValue) parseIP(v *Value, ipi interface{}, ipv6 bool) { + if ipi != nil { + if ipv6 { + v.IP6 = nil + } else { + v.IP = nil + } + } + + if ipa, ok := ipi.([]interface{}); ok { + for _, ip := range ipa { + if ips, ok := ip.(string); ok { + rv.addIP(v, ips, ipv6) + } + } + + return + } + + if ip, ok := ipi.(string); ok { + rv.addIP(v, ip, ipv6) + } +} + +func (rv *rawValue) addIP(v *Value, ips string, ipv6 bool) error { + pip := net.ParseIP(ips) + if pip == nil || (pip.To4() == nil) != ipv6 { + return fmt.Errorf("malformed IP") + } + + if ipv6 { + v.IP6 = append(v.IP6, pip) + } else { + v.IP = append(v.IP, pip) + } + + return nil +} + +func (rv *rawValue) parseNS(v *Value) error { + // "dns" takes precedence + if rv.DNS != nil { + rv.NS = rv.DNS + } + + if rv.NS == nil { + return nil + } + + v.NS = nil + + switch rv.NS.(type) { + case []interface{}: + for _, si := range rv.NS.([]interface{}) { + s, ok := si.(string) + if !ok || !validateHostName(s) { + continue + } + + v.NS = append(v.NS, s) + } + return nil + case string: + s := rv.NS.(string) + if !validateHostName(s) { + return fmt.Errorf("malformed NS hostname") + } + + v.NS = append(v.NS, s) + return nil + default: + return fmt.Errorf("unknown NS field format") + } +} + +func (rv *rawValue) parseAlias(v *Value) 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 + return nil + } + + return fmt.Errorf("unknown alias field format") +} + +func (rv *rawValue) parseTranslate(v *Value) 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 + 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) + } + return err + } + + return fmt.Errorf("unknown import field format") +} + +func (rv *rawValue) parseDelegate(v *Value, resolve ResolveFunc, depth, mergeDepth int) (bool, error) { + if rv.Delegate == 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 + } + + return false, fmt.Errorf("unknown delegate field format") +} + +func (rv *rawValue) parseHostmaster(v *Value) error { + if rv.Translate == nil { + return nil + } + + if s, ok := rv.Hostmaster.(string); ok { + if !validateEmail(s) { + return fmt.Errorf("malformed e. mail address in email field") + } + + v.Hostmaster = s + return nil + } + + return fmt.Errorf("unknown email field format") +} + +func (rv *rawValue) parseDS(v *Value) error { + if rv.DS == nil { + return nil + } + + v.DS = nil + + if dsa, ok := rv.DS.([]interface{}); ok { + for _, ds1 := range dsa { + if ds, ok := ds1.([]interface{}); ok { + if len(ds) != 4 { + continue + } + + a1, ok := ds[0].(float64) + if !ok { + continue + } + + a2, ok := ds[1].(float64) + if !ok { + continue + } + + a3, ok := ds[2].(float64) + if !ok { + continue + } + + a4, ok := ds[3].(string) + if !ok { + continue + } + + a4b, err := base64.StdEncoding.DecodeString(a4) + if err != nil { + continue + } + + a4h := hex.EncodeToString(a4b) + v.DS = append(v.DS, &dns.DS{ + Hdr: dns.RR_Header{Rrtype: dns.TypeDS, Class: dns.ClassINET, Ttl: 600}, + KeyTag: uint16(a1), + Algorithm: uint8(a2), + DigestType: uint8(a3), + Digest: a4h, + }) + } + } + } + return fmt.Errorf("malformed DS field format") +} + +func (rv *rawValue) parseTXT(v *Value) error { + if rv.TXT == nil { + return nil + } + + if txta, ok := rv.TXT.([]interface{}); ok { + // ["...", "..."] or [["...", "..."], ["...", "..."]] + for _, vv := range txta { + if sa, ok := vv.([]interface{}); ok { + // [["...", "..."], ["...", "..."]] + a := []string{} + for _, x := range sa { + if xs, ok := x.(string); ok { + a = append(a, xs) + } + } + v.TXT = append(v.TXT, a) + } else if s, ok := vv.(string); ok { + v.TXT = append(v.TXT, segmentizeTXT(s)) + } else { + return fmt.Errorf("malformed TXT value") + } + } + } else { + // "..." + if s, ok := rv.TXT.(string); ok { + v.TXT = append(v.TXT, segmentizeTXT(s)) + } else { + return fmt.Errorf("malformed TXT value") + } + } + + return nil +} + +func segmentizeTXT(txt string) (a []string) { + for len(txt) > 255 { + a = append(a, txt[0:255]) + txt = txt[255:] + } + a = append(a, txt) + return +} + +func (rv *rawValue) parseMX(v *Value) error { + if rv.MX == nil { + return nil + } + + if sa, ok := rv.MX.([]interface{}); ok { + for _, s := range sa { + rv.parseSingleMX(s, v) + } + } + + return fmt.Errorf("malformed MX value") +} + +func (rv *rawValue) parseSingleMX(s interface{}, v *Value) error { + sa, ok := s.([]interface{}) + if !ok { + return fmt.Errorf("malformed MX value") + } + + if len(sa) < 2 { + return fmt.Errorf("malformed MX value") + } + + prio, ok := sa[0].(float64) + if !ok || prio < 0 { + return fmt.Errorf("malformed MX value") + } + + hostname, ok := sa[1].(string) + if !ok || !validateHostName(hostname) { + return fmt.Errorf("malformed MX value") + } + + v.MX = append(v.MX, &dns.MX{ + Hdr: dns.RR_Header{Rrtype: dns.TypeMX, Class: dns.ClassINET, Ttl: 600}, + Preference: uint16(prio), + Mx: hostname, + }) + + return nil +} + +func (rv *rawValue) parseService(v *Value) error { + if rv.Service == nil { + return nil + } + + if sa, ok := rv.Service.([]interface{}); ok { + for _, s := range sa { + rv.parseSingleService(s, v) + } + } + + return fmt.Errorf("malformed service value") +} + +func (rv *rawValue) parseSingleService(svc interface{}, v *Value) error { + svca, ok := svc.([]interface{}) + if !ok { + return fmt.Errorf("malformed service value") + } + + if len(svca) < 6 { + return fmt.Errorf("malformed service value") + } + + appProtoName, ok := svca[0].(string) + if !ok || !validateServiceName(appProtoName) { + return fmt.Errorf("malformed service value") + } + + transportProtoName, ok := svca[1].(string) + if !ok || !validateServiceName(transportProtoName) { + return fmt.Errorf("malformed service value") + } + + priority, ok := svca[2].(float64) + if !ok { + return fmt.Errorf("malformed service value") + } + + weight, ok := svca[3].(float64) + if !ok { + return fmt.Errorf("malformed service value") + } + + port, ok := svca[4].(float64) + if !ok { + return fmt.Errorf("malformed service value") + } + + hostname, ok := svca[5].(string) + if !ok || !validateHostName(hostname) { + return fmt.Errorf("malformed service value") + } + + v.Service = append(v.Service, &dns.SRV{ + Hdr: dns.RR_Header{ + Name: "_" + appProtoName + "._" + transportProtoName, + Rrtype: dns.TypeSRV, + Class: dns.ClassINET, + Ttl: 600, + }, + Priority: uint16(priority), + Weight: uint16(weight), + Port: uint16(port), + Target: hostname, + }) + + return nil +} + +func (rv *rawValue) parseMap(v *Value, resolve ResolveFunc, depth, mergeDepth int) error { + m := map[string]json.RawMessage{} + + err := json.Unmarshal(rv.Map, &m) + if err != nil { + return err + } + + for mk, mv := range m { + rv2 := &rawValue{} + v2 := &Value{} + + var s string + err := json.Unmarshal(mv, &s) + if err == nil { + // deprecated case: "map": { "": "127.0.0.1" } + rv2.IP = s + rv2.IP6 = s + } else { + // normal case: "map": { "": { ... } } + err = json.Unmarshal(mv, rv2) + if err != nil { + continue + } + } + + rv2.parse(v2, resolve, depth+1, mergeDepth) + + if v.Map == nil { + v.Map = make(map[string]*Value) + } + + v.Map[mk] = v2 + } + + return nil +} + +// Moves items in {"map": {"": ...}} to the object itself, then deletes the "" +// entry in the map object. +func (v *Value) moveEmptyMapItems() error { + if ev, ok := v.Map[""]; ok { + if len(v.IP) == 0 { + v.IP = ev.IP + } + if len(v.IP6) == 0 { + v.IP6 = ev.IP6 + } + if len(v.NS) == 0 { + v.NS = ev.NS + } + if len(v.DS) == 0 { + v.DS = ev.DS + } + if len(v.TXT) == 0 { + v.TXT = ev.TXT + } + if len(v.Service) == 0 { + v.Service = ev.Service + } + if len(v.MX) == 0 { + v.MX = ev.MX + } + if len(v.Alias) == 0 { + v.Alias = ev.Alias + } + if len(v.Translate) == 0 { + v.Translate = ev.Translate + } + if len(v.Hostmaster) == 0 { + v.Hostmaster = ev.Hostmaster + } + delete(v.Map, "") + if len(v.Map) == 0 { + v.Map = ev.Map + } + } + return nil +} + +// Validation functions + +var re_hostName = regexp.MustCompilePOSIX(`^([a-z0-9_-]+\.)*[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 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 == "" +} diff --git a/ncdomain/convert_test.go b/ncdomain/convert_test.go new file mode 100644 index 0000000..294950d --- /dev/null +++ b/ncdomain/convert_test.go @@ -0,0 +1,182 @@ +package ncdomain_test + +import "github.com/hlandau/ncdns/convert" +import "github.com/miekg/dns" +import "testing" +import "net" +import "fmt" + +type item struct { + jsonValue string + value *convert.Value + expectedError error + merges map[string]string +} + +var suite = []item{ + item{`{}`, &convert.Value{}, nil, nil}, + item{`{"ip":"1.2.3.4"}`, &convert.Value{IP: []net.IP{net.ParseIP("1.2.3.4")}}, nil, nil}, + item{`{"ip":["1.2.3.4"]}`, &convert.Value{IP: []net.IP{net.ParseIP("1.2.3.4")}}, nil, nil}, + item{`{"ip":["1.2.3.4","200.200.200.200"]}`, &convert.Value{IP: []net.IP{net.ParseIP("1.2.3.4"), net.ParseIP("200.200.200.200")}}, nil, nil}, + item{`{"ip6":"dead:b33f::deca:fbad"}`, &convert.Value{IP6: []net.IP{net.ParseIP("dead:b33f::deca:fbad")}}, nil, nil}, + item{`{"ip6":["dead:b33f::deca:fbad"]}`, &convert.Value{IP6: []net.IP{net.ParseIP("dead:b33f::deca:fbad")}}, nil, nil}, + item{`{"ip6":["dead:b33f::deca:fbad","1234:abcd:5678:bcde:9876:fedc:5432:ba98"]}`, &convert.Value{IP6: []net.IP{net.ParseIP("dead:b33f::deca:fbad"), net.ParseIP("1234:abcd:5678:bcde:9876:fedc:5432:ba98")}}, nil, nil}, + item{`{"ns":"alpha.beta.gamma.delta"}`, &convert.Value{NS: []string{"alpha.beta.gamma.delta"}}, nil, nil}, + item{`{"ns":["alpha.beta.gamma.delta"]}`, &convert.Value{NS: []string{"alpha.beta.gamma.delta"}}, nil, nil}, + item{`{"ns":["alpha.beta.gamma.delta","delta.gamma.beta.alpha"]}`, &convert.Value{NS: []string{"alpha.beta.gamma.delta", "delta.gamma.beta.alpha"}}, nil, nil}, + item{`{"mx":[[10,"alpha.beta.gamma.delta"]]}`, &convert.Value{MX: []dns.MX{dns.MX{Hdr: dns.RR_Header{Rrtype: dns.TypeMX, Class: dns.ClassINET, Ttl: 600}, Preference: 10, Mx: "alpha.beta.gamma.delta"}}}, nil, nil}, + item{`{"mx":[[10,"alpha.beta.gamma.delta"],[20,"epsilon.example"]]}`, &convert.Value{MX: []dns.MX{dns.MX{Hdr: dns.RR_Header{Rrtype: dns.TypeMX, Class: dns.ClassINET, Ttl: 600}, Preference: 10, Mx: "alpha.beta.gamma.delta"}, dns.MX{Hdr: dns.RR_Header{Rrtype: dns.TypeMX, Class: dns.ClassINET, Ttl: 600}, Preference: 20, Mx: "epsilon.example"}}}, nil, nil}, + item{`{"alias":"alpha.beta.gamma.delta"}`, &convert.Value{Alias: "alpha.beta.gamma.delta"}, nil, nil}, + item{`{"translate":"alpha.beta.gamma.delta"}`, &convert.Value{Translate: "alpha.beta.gamma.delta"}, nil, nil}, + item{`{"txt":"text record"}`, &convert.Value{TXT: [][]string{[]string{"text record"}}}, nil, nil}, + item{`{"txt":"[text ... record][text ... record][text ... record][text ... record][text ... record][text ... record][text ... record][text ... record][text ... record][text ... record][text ... record][text ... record][text ... record][text ... record][text ... record][text ... record]"}`, &convert.Value{TXT: [][]string{[]string{"[text ... record][text ... record][text ... record][text ... record][text ... record][text ... record][text ... record][text ... record][text ... record][text ... record][text ... record][text ... record][text ... record][text ... record][text ... record]", "[text ... record]"}}}, nil, nil}, + item{`{"txt":["text record"]}`, &convert.Value{TXT: [][]string{[]string{"text record"}}}, nil, nil}, + item{`{"txt":["text record","text record 2"]}`, &convert.Value{TXT: [][]string{[]string{"text record"}, []string{"text record 2"}}}, nil, nil}, + item{`{"txt":[["text", "record"]]}`, &convert.Value{TXT: [][]string{[]string{"text", "record"}}}, nil, nil}, + item{`{"txt":[["text", "record"],["text", "record", "2"]]}`, &convert.Value{TXT: [][]string{[]string{"text", "record"}, []string{"text", "record", "2"}}}, nil, nil}, + item{`{"service":[ ["http","tcp",1,2,80,"alpha.beta.gamma.delta"] ]}`, &convert.Value{Service: []dns.SRV{dns.SRV{Hdr: dns.RR_Header{Name: "_http._tcp", Ttl: 600, Rrtype: dns.TypeSRV, Class: dns.ClassINET}, Priority: 1, Weight: 2, Port: 80, Target: "alpha.beta.gamma.delta"}}}, nil, nil}, + item{`{"service":[ ["http","tcp",1,2,80,"alpha.beta.gamma.delta"], ["https","tcp",1,2,443,"alpha.beta.gamma.delta"] ]}`, &convert.Value{Service: []dns.SRV{dns.SRV{Hdr: dns.RR_Header{Name: "_http._tcp", Ttl: 600, Rrtype: dns.TypeSRV, Class: dns.ClassINET}, Priority: 1, Weight: 2, Port: 80, Target: "alpha.beta.gamma.delta"}, dns.SRV{Hdr: dns.RR_Header{Name: "_https._tcp", Ttl: 600, Rrtype: dns.TypeSRV, Class: dns.ClassINET}, Priority: 1, Weight: 2, Port: 443, Target: "alpha.beta.gamma.delta"}}}, nil, nil}, + item{`{"map":{ "": { } }}`, &convert.Value{}, nil, nil}, + item{`{"map":{ "": { "ip": "1.2.3.4" } }}`, &convert.Value{IP: []net.IP{net.ParseIP("1.2.3.4")}}, nil, nil}, + item{`{"map":{ "www": { "ip": "1.2.3.4" } }}`, &convert.Value{Map: map[string]*convert.Value{"www": &convert.Value{IP: []net.IP{net.ParseIP("1.2.3.4")}}}}, nil, nil}, + item{`{"map":{ "": "1.2.3.4" }}`, &convert.Value{IP: []net.IP{net.ParseIP("1.2.3.4")}}, nil, nil}, + item{`{"map":{ "www": "1.2.3.4" }}`, &convert.Value{Map: map[string]*convert.Value{"www": &convert.Value{IP: []net.IP{net.ParseIP("1.2.3.4")}}}}, nil, nil}, + item{`{"ds":[[12345,8,2,"4tPJFvbe6scylOgmj7WIUESoM/xUWViPSpGEz8QaV2Y="]]}`, &convert.Value{DS: []dns.DS{dns.DS{Hdr: dns.RR_Header{Rrtype: dns.TypeDS, Class: dns.ClassINET, Ttl: 600}, KeyTag: 12345, Algorithm: 8, DigestType: 2, Digest: "e2d3c916f6deeac73294e8268fb5885044a833fc5459588f4a9184cfc41a5766"}}}, nil, nil}, + item{`{"ds":[[54321,8,1,"5sFxbPtr3IToTOGrVRDaxpFztbI="],[12345,8,2,"4tPJFvbe6scylOgmj7WIUESoM/xUWViPSpGEz8QaV2Y="]]}`, &convert.Value{DS: []dns.DS{dns.DS{Hdr: dns.RR_Header{Rrtype: dns.TypeDS, Class: dns.ClassINET, Ttl: 600}, KeyTag: 54321, Algorithm: 8, DigestType: 1, Digest: "e6c1716cfb6bdc84e84ce1ab5510dac69173b5b2"}, dns.DS{Hdr: dns.RR_Header{Rrtype: dns.TypeDS, Class: dns.ClassINET, Ttl: 600}, KeyTag: 12345, Algorithm: 8, DigestType: 2, Digest: "e2d3c916f6deeac73294e8268fb5885044a833fc5459588f4a9184cfc41a5766"}}}, nil, nil}, + item{`{"email":"hostmaster@example.com"}`, &convert.Value{Hostmaster: "hostmaster@example.com"}, nil, nil}, + item{`{"ip":["1.2.3.4"],"import":"d/example"}`, &convert.Value{IP: []net.IP{net.ParseIP("1.2.3.4")}, IP6: []net.IP{net.ParseIP("::beef")}}, nil, map[string]string{"d/example": `{"ip6":["::beef"]}`}}, + item{`{"ip":["1.2.3.4"],"import":"d/example"}`, &convert.Value{IP: []net.IP{net.ParseIP("1.2.3.4")}, IP6: []net.IP{net.ParseIP("::beef")}}, nil, map[string]string{"d/example": `{"ip":["2.3.4.5"],"ip6":["::beef"]}`}}, + item{`{"ns":["alpha.beta"],"import":"d/example"}`, &convert.Value{NS: []string{"alpha.beta"}, IP6: []net.IP{net.ParseIP("::beef")}}, nil, map[string]string{"d/example": `{"ns":["gamma.delta"],"ip6":["::beef"]}`}}, + item{`{"ds":[[12345,8,2,"4tPJFvbe6scylOgmj7WIUESoM/xUWViPSpGEz8QaV2Y="]],"import":"d/example"}`, &convert.Value{DS: []dns.DS{dns.DS{Hdr: dns.RR_Header{Rrtype: dns.TypeDS, Class: dns.ClassINET, Ttl: 600}, KeyTag: 12345, Algorithm: 8, DigestType: 2, Digest: "e2d3c916f6deeac73294e8268fb5885044a833fc5459588f4a9184cfc41a5766"}}}, nil, map[string]string{"d/example": `{"ds":[ [54321,8,1,"5sFxbPtr3IToTOGrVRDaxpFztbI="] ]}`}}, + item{`{"import":"d/example"}`, &convert.Value{DS: []dns.DS{dns.DS{Hdr: dns.RR_Header{Rrtype: dns.TypeDS, Class: dns.ClassINET, Ttl: 600}, KeyTag: 54321, Algorithm: 8, DigestType: 1, Digest: "e6c1716cfb6bdc84e84ce1ab5510dac69173b5b2"}}}, nil, map[string]string{"d/example": `{"ds":[ [54321,8,1,"5sFxbPtr3IToTOGrVRDaxpFztbI="] ]}`}}, + item{`{"ip":["1.2.3.4"],"delegate":"d/example"}`, &convert.Value{IP6: []net.IP{net.ParseIP("::beef")}}, nil, map[string]string{"d/example": `{"ip6":["::beef"]}`}}, +} + +func TestConversion(t *testing.T) { + for i, item := range suite { + resolve := func(name string) (string, error) { + if item.merges == nil { + return "", fmt.Errorf("not found") + } + + if s, ok := item.merges[name]; ok { + return s, nil + } else { + return "", fmt.Errorf("not found") + } + } + v, err := convert.ParseValue(item.jsonValue, resolve) + if err != item.expectedError { + t.Errorf("Item %d did not match expected error: got %+v but expected %+v", i, err, item.expectedError) + } + if !equals(v, item.value) { + t.Errorf("Item %d value did not match expected value: got %+v but expected %+v", i, v, item.value) + } + } +} + +// utility functions for testing equality + +func equals(v1 *convert.Value, v2 *convert.Value) bool { + return (v1 != nil) == (v2 != nil) && + eqIPArray(v1.IP, v2.IP) && + eqIPArray(v1.IP6, v2.IP6) && + eqStringArray(v1.NS, v2.NS) && + v1.Alias == v2.Alias && + eqDSArray(v1.DS, v2.DS) && + eqStringArrayArray(v1.TXT, v2.TXT) && + eqServiceArray(v1.Service, v2.Service) && + eqValueMap(v1, v2) +} + +func eqIPArray(a []net.IP, b []net.IP) bool { + if len(a) != len(b) { + return false + } + for i := range a { + if !a[i].Equal(b[i]) { + return false + } + } + return true +} + +func eqStringArray(a []string, b []string) bool { + if len(a) != len(b) { + return false + } + for i := range a { + if a[i] != b[i] { + return false + } + } + return true +} + +func eqDSArray(a []dns.DS, b []dns.DS) bool { + if len(a) != len(b) { + return false + } + for i := range a { + if !eqDS(a[i], b[i]) { + return false + } + } + return true +} + +func eqDS(a dns.DS, b dns.DS) bool { + return a.KeyTag == b.KeyTag && a.Algorithm == b.Algorithm && + a.DigestType == b.DigestType && a.Digest == b.Digest && eqHdr(a.Hdr, b.Hdr) +} + +func eqHdr(a dns.RR_Header, b dns.RR_Header) bool { + return a.Name == b.Name && a.Rrtype == b.Rrtype && a.Class == b.Class && a.Ttl == b.Ttl +} + +func eqStringArrayArray(a [][]string, b [][]string) bool { + if len(a) != len(b) { + return false + } + for i := range a { + if !eqStringArray(a[i], b[i]) { + return false + } + } + return true +} + +func eqServiceArray(a []dns.SRV, b []dns.SRV) bool { + if len(a) != len(b) { + return false + } + for i := range a { + if !eqService(a[i], b[i]) { + return false + } + } + return true +} + +func eqService(a dns.SRV, b dns.SRV) bool { + return a.Priority == b.Priority && a.Weight == b.Weight && + a.Port == b.Port && a.Target == b.Target && eqHdr(a.Hdr, b.Hdr) +} + +func eqValueMap(a *convert.Value, b *convert.Value) bool { + if len(a.Map) != len(b.Map) { + return false + } + + for k, v1 := range a.Map { + v2, ok := b.Map[k] + if !ok { + return false + } + if !equals(v1, v2) { + return false + } + } + + return true +}