diff --git a/backend/backend.go b/backend/backend.go index ea5008f..790db88 100644 --- a/backend/backend.go +++ b/backend/backend.go @@ -9,6 +9,8 @@ import "github.com/hlandau/ncdns/ncdomain" import "sync" import "fmt" import "net" +import "net/mail" +import "strings" // Provides an abstract zone file for the Namecoin .bit TLD. type Backend struct { @@ -23,25 +25,33 @@ const defaultMaxEntries = 100 // Backend configuration. type Config struct { + NamecoinConn namecoin.Conn + // Username and password to use for connecting to the Namecoin JSON-RPC interface. - RPCUsername string - RPCPassword string + //RPCUsername string + //RPCPassword string // hostname:port to use for connecting to the Namecoin JSON-RPC interface. - RPCAddress string + //RPCAddress string // Maximum entries to permit in name cache. If zero, a default value is used. CacheMaxEntries int - // The hostname which should be advertised as the primary nameserver for the zone. - // If left empty, a psuedo-hostname resolvable to SelfIP is used. - SelfName string + // Nameservers to advertise at zone apex. The first is considered the primary. + // If empty, a psuedo-hostname resolvable to SelfIP is used. + CanonicalNameservers []string + + // Vanity IPs to place at the zone apex. + VanityIPs []net.IP - // Used only if SelfName is left blank. An IP which the internal psuedo-hostname - // should resolve to. This should be the public IP of the nameserver serving the - // zone expressed by this backend. + // Used only if CanonicalNameservers is left blank. An IP which the internal + // psuedo-hostname should resolve to. This should be the public IP of the + // nameserver serving the zone expressed by this backend. SelfIP string + // Hostmaster in e. mail form (e.g. "hostmaster@example.com"). + Hostmaster string + // Map names (like "d/example") to strings containing JSON values. Used to provide // fake names for testing purposes. You don't need to use this. FakeNames map[string]string @@ -52,20 +62,50 @@ func New(cfg *Config) (backend *Backend, err error) { b := &Backend{} b.cfg = *cfg - b.nc.Username = cfg.RPCUsername - b.nc.Password = cfg.RPCPassword - b.nc.Server = cfg.RPCAddress + b.nc = b.cfg.NamecoinConn + //b.nc.Username = cfg.RPCUsername + //b.nc.Password = cfg.RPCPassword + //b.nc.Server = cfg.RPCAddress b.cache.MaxEntries = cfg.CacheMaxEntries if b.cache.MaxEntries == 0 { b.cache.MaxEntries = defaultMaxEntries } + hostmaster, err := convertEmail(b.cfg.Hostmaster) + if err != nil { + return + } + b.cfg.Hostmaster = hostmaster + backend = b return } +func convertEmail(email string) (string, error) { + if email == "" { + return ".", nil + } + + if util.ValidateHostName(email) { + return email, nil + } + + addr, err := mail.ParseAddress(email) + if err != nil { + return "", err + } + + email = addr.Address + parts := strings.SplitN(email, "@", 2) + if len(parts) < 2 { + return "", fmt.Errorf("invalid e. mail address specified") + } + + return dns.Fqdn(parts[0] + "." + parts[1]), nil +} + // 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) { @@ -111,7 +151,7 @@ func (tx *btx) Do() (rrs []dns.RR, err error) { // 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 == "" { + if tx.basename == "x--nmc" && len(tx.b.cfg.CanonicalNameservers) == 0 { return tx.doMetaDomain() } @@ -125,9 +165,9 @@ func (tx *btx) determineDomain() (subname, basename, rootname string, err error) } func (tx *btx) doRootDomain() (rrs []dns.RR, err error) { - nsname := tx.b.cfg.SelfName - if nsname == "" { - nsname = "this.x--nmc." + tx.rootname + nss := tx.b.cfg.CanonicalNameservers + if len(tx.b.cfg.CanonicalNameservers) == 0 { + nss = []string{dns.Fqdn("this.x--nmc." + tx.rootname)} } soa := &dns.SOA{ @@ -137,8 +177,8 @@ func (tx *btx) doRootDomain() (rrs []dns.RR, err error) { Class: dns.ClassINET, Rrtype: dns.TypeSOA, }, - Ns: dns.Fqdn(nsname), - Mbox: ".", + Ns: nss[0], + Mbox: tx.b.cfg.Hostmaster, Serial: 1, Refresh: 600, Retry: 600, @@ -146,17 +186,48 @@ func (tx *btx) doRootDomain() (rrs []dns.RR, err error) { Minttl: 600, } - ns := &dns.NS{ - Hdr: dns.RR_Header{ - Name: dns.Fqdn(tx.rootname), - Ttl: 86400, - Class: dns.ClassINET, - Rrtype: dns.TypeNS, - }, - Ns: dns.Fqdn(nsname), + rrs = make([]dns.RR, 0, 1+len(nss)+len(tx.b.cfg.VanityIPs)) + rrs = append(rrs, soa) + for _, cn := range nss { + ns := &dns.NS{ + Hdr: dns.RR_Header{ + Name: dns.Fqdn(tx.rootname), + Ttl: 86400, + Class: dns.ClassINET, + Rrtype: dns.TypeNS, + }, + Ns: dns.Fqdn(cn), + } + + rrs = append(rrs, ns) + } + + for _, ip := range tx.b.cfg.VanityIPs { + if ip.To4() != nil { + a := &dns.A{ + Hdr: dns.RR_Header{ + Name: dns.Fqdn(tx.rootname), + Ttl: 86400, + Class: dns.ClassINET, + Rrtype: dns.TypeA, + }, + A: ip, + } + rrs = append(rrs, a) + } else { + a := &dns.AAAA{ + Hdr: dns.RR_Header{ + Name: dns.Fqdn(tx.rootname), + Ttl: 86400, + Class: dns.ClassINET, + Rrtype: dns.TypeAAAA, + }, + AAAA: ip, + } + rrs = append(rrs, a) + } } - rrs = []dns.RR{soa, ns} return } @@ -277,9 +348,9 @@ func (b *Backend) resolveName(name string) (jsonValue string, err error) { 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 + v := ncdomain.ParseValue(name, jsonValue, b.resolveExtraName, nil) + if v == nil { + return nil, fmt.Errorf("couldn't parse value") } d.ncv = v diff --git a/namecoin/namecoin.go b/namecoin/namecoin.go index 1183151..eebd1d0 100644 --- a/namecoin/namecoin.go +++ b/namecoin/namecoin.go @@ -7,6 +7,12 @@ import "github.com/hlandau/ncdns/namecoin/extratypes" import "sync/atomic" import "fmt" +import "expvar" + +var cQueryCalls = expvar.NewInt("ncdns.namecoin.numQueryCalls") +var cSyncCalls = expvar.NewInt("ncdns.namecoin.numSyncCalls") +var cFilterCalls = expvar.NewInt("ncdns.namecoin.numFilterCalls") +var cCurHeightCalls = expvar.NewInt("ncdns.namecoin.numCurHeightCalls") // Used for generating IDs for JSON-RPC requests. var idCounter int32 @@ -27,6 +33,8 @@ type Conn struct { // If the domain exists, returns the value stored in Namecoin, which should be JSON. // Note that this will return domain data even if the domain is expired. func (nc *Conn) Query(name string) (v string, err error) { + cQueryCalls.Add(1) + cmd, err := extratypes.NewNameShowCmd(newID(), name) if err != nil { //log.Info("NC NEWCMD ", err) @@ -65,6 +73,8 @@ var ErrSyncNoSuchBlock = fmt.Errorf("no block exists with given hash") const rpcInvalidAddressOrKey = -5 func (nc *Conn) Sync(hash string, count int, wait bool) ([]extratypes.NameSyncEvent, error) { + cSyncCalls.Add(1) + cmd, err := extratypes.NewNameSyncCmd(newID(), hash, count, wait) if err != nil { return nil, err @@ -94,6 +104,8 @@ func (nc *Conn) Sync(hash string, count int, wait bool) ([]extratypes.NameSyncEv } func (nc *Conn) CurHeight() (int, error) { + cCurHeightCalls.Add(1) + cmd, err := btcjson.NewGetInfoCmd(newID()) if err != nil { return 0, err @@ -120,6 +132,8 @@ func (nc *Conn) CurHeight() (int, error) { } func (nc *Conn) Filter(regexp string, maxage, from, count int) (names []extratypes.NameFilterItem, err error) { + cFilterCalls.Add(1) + cmd, err := extratypes.NewNameFilterCmd(newID(), regexp, maxage, from, count) if err != nil { return nil, err diff --git a/ncdomain/convert.go b/ncdomain/convert.go index 1b03d0b..f48a9f2 100644 --- a/ncdomain/convert.go +++ b/ncdomain/convert.go @@ -46,6 +46,56 @@ type Value struct { IsTopLevel bool } +func (v *Value) mkString(i string) string { + s := i[1:] + "Value:" + i += " " + if v.HasAlias { + s += i + "CNAME: \"" + v.Alias + "\"" + } + if v.HasTranslate { + s += i + "DNAME: \"" + v.Translate + "\"" + } + if v.Hostmaster != "" { + s += i + "Hostmaster: " + v.Hostmaster + } + for _, ip := range v.IP { + s += i + "IPv4 Address: " + ip.String() + } + for _, ip := range v.IP6 { + s += i + "IPv6 Address: " + ip.String() + } + for _, ns := range v.NS { + s += i + "Nameserver: " + ns + } + for _, ds := range v.DS { + s += i + "DS Record: " + ds.String() + } + for _, txt := range v.TXT { + s += i + "TXT Record:" + for _, txtc := range txt { + s += i + " " + txtc + } + } + for _, srv := range v.Service { + s += i + "SRV Record: " + srv.String() + } + for _, tlsa := range v.TLSA { + s += i + "TLSA Record: " + tlsa.String() + } + if len(v.Map) > 0 { + s += i + "Subdomains:" + for k, v := range v.Map { + s += i + " " + k + ":" + s += v.mkString(i + " ") + } + } + return s +} + +func (v *Value) String() string { + return v.mkString("\n") +} + func (v *Value) RRs(out []dns.RR, suffix, apexSuffix string) ([]dns.RR, error) { il := len(out) suffix = dns.Fqdn(suffix) @@ -287,6 +337,19 @@ type rawValue struct { } type ResolveFunc func(name string) (string, error) +type ErrorFunc func(err error, isWarning bool) + +func (ef ErrorFunc) add(err error) { + if ef != nil && err != nil { + ef(err, false) + } +} + +func (ef ErrorFunc) addWarning(err error) { + if ef != nil && err != nil { + ef(err, true) + } +} // Call to convert a given JSON value to a parsed Namecoin domain value. // @@ -295,12 +358,17 @@ 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(name, jsonValue string, resolve ResolveFunc) (value *Value, err error) { +// +// Returns nil if the JSON could not be parsed. For all other errors processing +// continues and recovers as much as possible; errFunc is called for all errors +// and warnings if specified. +func ParseValue(name, jsonValue string, resolve ResolveFunc, errFunc ErrorFunc) (value *Value) { rv := &rawValue{} v := &Value{} - err = json.Unmarshal([]byte(jsonValue), rv) + err := json.Unmarshal([]byte(jsonValue), rv) if err != nil { + errFunc.add(err) return } @@ -313,16 +381,17 @@ func ParseValue(name, jsonValue string, resolve ResolveFunc) (value *Value, err mergedNames := map[string]struct{}{} mergedNames[name] = struct{}{} - rv.parse(v, resolve, 0, 0, "", "", mergedNames) + rv.parse(v, resolve, errFunc, 0, 0, "", "", mergedNames) v.IsTopLevel = true value = v return } -func (rv *rawValue) parse(v *Value, resolve ResolveFunc, depth, mergeDepth int, subdomain, relname string, mergedNames map[string]struct{}) error { +func (rv *rawValue) parse(v *Value, resolve ResolveFunc, errFunc ErrorFunc, depth, mergeDepth int, subdomain, relname string, mergedNames map[string]struct{}) { if depth > depthLimit { - return fmt.Errorf("depth limit exceeded") + errFunc.add(fmt.Errorf("depth limit exceeded")) + return } realv := v @@ -332,35 +401,34 @@ func (rv *rawValue) parse(v *Value, resolve ResolveFunc, depth, mergeDepth int, v = &Value{} } - ok, _ := rv.parseDelegate(v, resolve, depth, mergeDepth, relname, mergedNames) + ok, _ := rv.parseDelegate(v, resolve, errFunc, depth, mergeDepth, relname, mergedNames) if ok { - return nil - } - - rv.parseImport(v, resolve, depth, mergeDepth, relname, mergedNames) - rv.parseIP(v, rv.IP, false) - rv.parseIP(v, rv.IP6, true) - rv.parseNS(v, relname) - rv.parseAlias(v, relname) - rv.parseTranslate(v, relname) - rv.parseHostmaster(v) - rv.parseDS(v) - rv.parseTXT(v) - rv.parseService(v, relname) - rv.parseMX(v, relname) - rv.parseTLSA(v) - rv.parseMap(v, resolve, depth, mergeDepth, relname) + return + } + + rv.parseImport(v, resolve, errFunc, depth, mergeDepth, relname, mergedNames) + rv.parseIP(v, errFunc, rv.IP, false) + rv.parseIP(v, errFunc, rv.IP6, true) + rv.parseNS(v, errFunc, relname) + rv.parseAlias(v, errFunc, relname) + rv.parseTranslate(v, errFunc, relname) + rv.parseHostmaster(v, errFunc) + rv.parseDS(v, errFunc) + rv.parseTXT(v, errFunc) + rv.parseService(v, errFunc, relname) + rv.parseMX(v, errFunc, relname) + rv.parseTLSA(v, errFunc) + rv.parseMap(v, resolve, errFunc, depth, mergeDepth, relname) v.moveEmptyMapItems() if subdomain != "" { subv, err := v.findSubdomainByName(subdomain) if err != nil { - return err + errFunc.add(fmt.Errorf("couldn't find subdomain by name in import or delegate item: %v", err)) + return } *realv = *subv } - - return nil } func (v *Value) qualifyIntl(name, suffix, apexSuffix string) string { @@ -396,22 +464,27 @@ func (v *Value) qualify(name, suffix, apexSuffix string) (string, bool) { return s, true } -func (rv *rawValue) parseMerge(mergeValue string, v *Value, resolve ResolveFunc, depth, mergeDepth int, subdomain, relname string, mergedNames map[string]struct{}) error { +func (rv *rawValue) parseMerge(mergeValue string, v *Value, resolve ResolveFunc, errFunc ErrorFunc, depth, mergeDepth int, subdomain, relname string, mergedNames map[string]struct{}) error { rv2 := &rawValue{} if mergeDepth > mergeDepthLimit { - return fmt.Errorf("merge depth limit exceeded") + err := fmt.Errorf("merge depth limit exceeded") + errFunc.add(err) + return err } err := json.Unmarshal([]byte(mergeValue), rv2) if err != nil { + err = fmt.Errorf("couldn't parse JSON to be merged: %v", err) + errFunc.add(err) return err } - return rv2.parse(v, resolve, depth, mergeDepth, subdomain, relname, mergedNames) + rv2.parse(v, resolve, errFunc, depth, mergeDepth, subdomain, relname, mergedNames) + return nil } -func (rv *rawValue) parseIP(v *Value, ipi interface{}, ipv6 bool) { +func (rv *rawValue) parseIP(v *Value, errFunc ErrorFunc, ipi interface{}, ipv6 bool) { if ipi != nil { if ipv6 { v.IP6 = nil @@ -423,7 +496,7 @@ func (rv *rawValue) parseIP(v *Value, ipi interface{}, ipv6 bool) { if ipa, ok := ipi.([]interface{}); ok { for _, ip := range ipa { if ips, ok := ip.(string); ok { - rv.addIP(v, ips, ipv6) + rv.addIP(v, errFunc, ips, ipv6) } } @@ -431,14 +504,15 @@ func (rv *rawValue) parseIP(v *Value, ipi interface{}, ipv6 bool) { } if ip, ok := ipi.(string); ok { - rv.addIP(v, ip, ipv6) + rv.addIP(v, errFunc, ip, ipv6) } } -func (rv *rawValue) addIP(v *Value, ips string, ipv6 bool) error { +func (rv *rawValue) addIP(v *Value, errFunc ErrorFunc, ips string, ipv6 bool) { pip := net.ParseIP(ips) if pip == nil || (pip.To4() == nil) != ipv6 { - return fmt.Errorf("malformed IP") + errFunc.add(fmt.Errorf("malformed IP: %s", ips)) + return } if ipv6 { @@ -446,18 +520,16 @@ func (rv *rawValue) addIP(v *Value, ips string, ipv6 bool) error { } else { v.IP = append(v.IP, pip) } - - return nil } -func (rv *rawValue) parseNS(v *Value, relname string) error { +func (rv *rawValue) parseNS(v *Value, errFunc ErrorFunc, relname string) { // "dns" takes precedence if rv.DNS != nil { rv.NS = rv.DNS } if rv.NS == nil { - return nil + return } v.NS = nil @@ -475,51 +547,49 @@ func (rv *rawValue) parseNS(v *Value, relname string) error { } rv.addNS(v, s, relname) } - return nil + return case string: s := rv.NS.(string) rv.addNS(v, s, relname) - return nil + return default: - return fmt.Errorf("unknown NS field format") + errFunc.add(fmt.Errorf("unknown NS field format")) } } -func (rv *rawValue) addNS(v *Value, s, relname string) error { +func (rv *rawValue) addNS(v *Value, s, relname string) { if _, ok := rv.nsSet[s]; !ok { v.NS = append(v.NS, s) rv.nsSet[s] = struct{}{} } - - return nil } -func (rv *rawValue) parseAlias(v *Value, relname string) error { +func (rv *rawValue) parseAlias(v *Value, errFunc ErrorFunc, relname string) { if rv.Alias == nil { - return nil + return } if s, ok := rv.Alias.(string); ok { v.Alias = s v.HasAlias = true - return nil + return } - return fmt.Errorf("unknown alias field format") + errFunc.add(fmt.Errorf("unknown alias field format")) } -func (rv *rawValue) parseTranslate(v *Value, relname string) error { +func (rv *rawValue) parseTranslate(v *Value, errFunc ErrorFunc, relname string) { if rv.Translate == nil { - return nil + return } if s, ok := rv.Translate.(string); ok { v.Translate = s v.HasTranslate = true - return nil + return } - return fmt.Errorf("unknown translate field format") + errFunc.add(fmt.Errorf("unknown translate field format")) } func isAllArray(x []interface{}) bool { @@ -540,7 +610,7 @@ func isAllString(x []interface{}) bool { return true } -func (rv *rawValue) parseImportImpl(val *Value, resolve ResolveFunc, depth, mergeDepth int, relname string, mergedNames map[string]struct{}, delegate bool) (bool, error) { +func (rv *rawValue) parseImportImpl(val *Value, resolve ResolveFunc, errFunc ErrorFunc, depth, mergeDepth int, relname string, mergedNames map[string]struct{}, delegate bool) (bool, error) { var err error succeeded := false src := rv.Import @@ -595,14 +665,18 @@ func (rv *rawValue) parseImportImpl(val *Value, resolve ResolveFunc, depth, merg mergedNames[k] = struct{}{} - err = rv.parseMerge(dv, val, resolve, depth, mergeDepth+1, subs, relname, mergedNames) + err = rv.parseMerge(dv, val, resolve, errFunc, depth, mergeDepth+1, subs, relname, mergedNames) if err != nil { + errFunc.add(err) continue } succeeded = true } } + + // ... + return succeeded, nil } // malformed } @@ -611,38 +685,41 @@ func (rv *rawValue) parseImportImpl(val *Value, resolve ResolveFunc, depth, merg err = fmt.Errorf("unknown import/delegate field format") } + errFunc.add(err) + 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) +func (rv *rawValue) parseImport(v *Value, resolve ResolveFunc, errFunc ErrorFunc, depth, mergeDepth int, relname string, mergedNames map[string]struct{}) error { + _, err := rv.parseImportImpl(v, resolve, errFunc, 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) parseDelegate(v *Value, resolve ResolveFunc, errFunc ErrorFunc, depth, mergeDepth int, relname string, mergedNames map[string]struct{}) (bool, error) { + return rv.parseImportImpl(v, resolve, errFunc, depth, mergeDepth, relname, mergedNames, true) } -func (rv *rawValue) parseHostmaster(v *Value) error { +func (rv *rawValue) parseHostmaster(v *Value, errFunc ErrorFunc) { if rv.Hostmaster == nil { - return nil + return } if s, ok := rv.Hostmaster.(string); ok { if !util.ValidateEmail(s) { - return fmt.Errorf("malformed e. mail address in email field") + errFunc.add(fmt.Errorf("malformed e. mail address in email field")) + return } v.Hostmaster = s - return nil + return } - return fmt.Errorf("unknown email field format") + errFunc.add(fmt.Errorf("unknown email field format")) } -func (rv *rawValue) parseDS(v *Value) error { +func (rv *rawValue) parseDS(v *Value, errFunc ErrorFunc) { if rv.DS == nil { - return nil + return } v.DS = nil @@ -650,32 +727,38 @@ func (rv *rawValue) parseDS(v *Value) error { if dsa, ok := rv.DS.([]interface{}); ok { for _, ds1 := range dsa { if ds, ok := ds1.([]interface{}); ok { - if len(ds) != 4 { + if len(ds) < 4 { + errFunc.add(fmt.Errorf("DS item must have four items")) continue } a1, ok := ds[0].(float64) if !ok { + errFunc.add(fmt.Errorf("First item in DS value must be an integer (key tag)")) continue } a2, ok := ds[1].(float64) if !ok { + errFunc.add(fmt.Errorf("Second item in DS value must be an integer (algorithm)")) continue } a3, ok := ds[2].(float64) if !ok { + errFunc.add(fmt.Errorf("Third item in DS value must be an integer (digest type)")) continue } a4, ok := ds[3].(string) if !ok { + errFunc.add(fmt.Errorf("Fourth item in DS value must be a string (digest)")) continue } a4b, err := base64.StdEncoding.DecodeString(a4) if err != nil { + errFunc.add(fmt.Errorf("Fourth item in DS value must be valid base64: %v", err)) continue } @@ -687,16 +770,19 @@ func (rv *rawValue) parseDS(v *Value) error { DigestType: uint8(a3), Digest: a4h, }) + } else { + errFunc.add(fmt.Errorf("DS item must be an array")) } } + return } - return fmt.Errorf("malformed DS field format") + errFunc.add(fmt.Errorf("malformed DS field format")) } -func (rv *rawValue) parseTLSA(v *Value) error { +func (rv *rawValue) parseTLSA(v *Value, errFunc ErrorFunc) { if rv.TLSA == nil { - return nil + return } v.TLSA = nil @@ -706,6 +792,7 @@ func (rv *rawValue) parseTLSA(v *Value) error { if tlsa, ok := tlsa1.([]interface{}); ok { // Format: ["443", "tcp", 1, 2, 3, "base64 certificate data"] if len(tlsa) < 6 { + errFunc.add(fmt.Errorf("TLSA item must have six items")) continue } @@ -713,6 +800,7 @@ func (rv *rawValue) parseTLSA(v *Value) error { if !ok { porti, ok := tlsa[0].(float64) if !ok { + errFunc.add(fmt.Errorf("First item in TLSA value must be an integer or string (port number)")) continue } ports = fmt.Sprintf("%d", int(porti)) @@ -720,31 +808,42 @@ func (rv *rawValue) parseTLSA(v *Value) error { transport, ok := tlsa[1].(string) if !ok { + errFunc.add(fmt.Errorf("Second item in TLSA value must be a string (transport protocol name)")) continue } a1, ok := tlsa[2].(float64) if !ok { + errFunc.add(fmt.Errorf("Third item in TLSA value must be an integer (usage)")) continue } a2, ok := tlsa[3].(float64) if !ok { + errFunc.add(fmt.Errorf("Fourth item in TLSA value must be an integer (selector)")) continue } a3, ok := tlsa[4].(float64) if !ok { + errFunc.add(fmt.Errorf("Fifth item in TLSA value must be an integer (match type)")) continue } a4, ok := tlsa[5].(string) if !ok { + errFunc.add(fmt.Errorf("Sixth item in TLSA value must be a string (certificate)")) continue } a4b, err := base64.StdEncoding.DecodeString(a4) if err != nil { + errFunc.add(fmt.Errorf("Fourth item in DS value must be valid base64: %v", err)) + continue + } + + if len(ports) > 62 || len(transport) > 62 { + errFunc.add(fmt.Errorf("Application and transport names must not exceed 62 characters")) continue } @@ -759,16 +858,19 @@ func (rv *rawValue) parseTLSA(v *Value) error { MatchingType: uint8(a3), Certificate: strings.ToUpper(a4h), }) + } else { + errFunc.add(fmt.Errorf("TLSA item must be an array")) } } + return } - return fmt.Errorf("malformed TLSA field format") + errFunc.add(fmt.Errorf("Malformed TLSA field format")) } -func (rv *rawValue) parseTXT(v *Value) error { +func (rv *rawValue) parseTXT(v *Value, errFunc ErrorFunc) { if rv.TXT == nil { - return nil + return } if txta, ok := rv.TXT.([]interface{}); ok { @@ -788,7 +890,8 @@ func (rv *rawValue) parseTXT(v *Value) error { } else if s, ok := vv.(string); ok { v.TXT = append(v.TXT, segmentizeTXT(s)) } else { - return fmt.Errorf("malformed TXT value") + errFunc.add(fmt.Errorf("malformed TXT value")) + return } } } else { @@ -796,7 +899,8 @@ func (rv *rawValue) parseTXT(v *Value) error { if s, ok := rv.TXT.(string); ok { v.TXT = append(v.TXT, segmentizeTXT(s)) } else { - return fmt.Errorf("malformed TXT value") + errFunc.add(fmt.Errorf("malformed TXT value")) + return } } @@ -818,7 +922,7 @@ func (rv *rawValue) parseTXT(v *Value) error { } } - return nil + return } func segmentizeTXT(txt string) (a []string) { @@ -830,38 +934,43 @@ func segmentizeTXT(txt string) (a []string) { return } -func (rv *rawValue) parseMX(v *Value, relname string) error { +func (rv *rawValue) parseMX(v *Value, errFunc ErrorFunc, relname string) { if rv.MX == nil { - return nil + return } if sa, ok := rv.MX.([]interface{}); ok { for _, s := range sa { - rv.parseSingleMX(s, v, relname) + rv.parseSingleMX(s, v, errFunc, relname) } + return } - return fmt.Errorf("malformed MX value") + errFunc.add(fmt.Errorf("malformed MX value")) } -func (rv *rawValue) parseSingleMX(s interface{}, v *Value, relname string) error { +func (rv *rawValue) parseSingleMX(s interface{}, v *Value, errFunc ErrorFunc, relname string) { sa, ok := s.([]interface{}) if !ok { - return fmt.Errorf("malformed MX value") + errFunc.add(fmt.Errorf("malformed MX value")) + return } if len(sa) < 2 { - return fmt.Errorf("malformed MX value") + errFunc.add(fmt.Errorf("malformed MX value")) + return } prio, ok := sa[0].(float64) if !ok || prio < 0 { - return fmt.Errorf("malformed MX value") + errFunc.add(fmt.Errorf("malformed MX value")) + return } hostname, ok := sa[1].(string) if !ok { - return fmt.Errorf("malformed MX value") + errFunc.add(fmt.Errorf("malformed MX value")) + return } v.MX = append(v.MX, &dns.MX{ @@ -870,12 +979,12 @@ func (rv *rawValue) parseSingleMX(s interface{}, v *Value, relname string) error Mx: hostname, }) - return nil + return } -func (rv *rawValue) parseService(v *Value, relname string) error { +func (rv *rawValue) parseService(v *Value, errFunc ErrorFunc, relname string) { if rv.Service == nil { - return nil + return } // We have to merge the services specified and those imported using an @@ -886,8 +995,10 @@ func (rv *rawValue) parseService(v *Value, relname string) error { if sa, ok := rv.Service.([]interface{}); ok { for _, s := range sa { - rv.parseSingleService(s, v, relname, servicesUsed) + rv.parseSingleService(s, v, errFunc, relname, servicesUsed) } + } else { + errFunc.add(fmt.Errorf("malformed service value")) } for _, svc := range oldServices { @@ -895,48 +1006,54 @@ func (rv *rawValue) parseService(v *Value, relname string) error { v.Service = append(v.Service, svc) } } - - return fmt.Errorf("malformed service value") } -func (rv *rawValue) parseSingleService(svc interface{}, v *Value, relname string, servicesUsed map[string]struct{}) error { +func (rv *rawValue) parseSingleService(svc interface{}, v *Value, errFunc ErrorFunc, relname string, servicesUsed map[string]struct{}) { svca, ok := svc.([]interface{}) if !ok { - return fmt.Errorf("malformed service value") + errFunc.add(fmt.Errorf("malformed service value")) + return } if len(svca) < 6 { - return fmt.Errorf("malformed service value") + errFunc.add(fmt.Errorf("malformed service value: must have six items")) + return } appProtoName, ok := svca[0].(string) if !ok || !util.ValidateServiceName(appProtoName) { - return fmt.Errorf("malformed service value") + errFunc.add(fmt.Errorf("malformed service value: first item must be a string (application protocol)")) + return } transportProtoName, ok := svca[1].(string) if !ok || !util.ValidateServiceName(transportProtoName) { - return fmt.Errorf("malformed service value") + errFunc.add(fmt.Errorf("malformed service value: second item must be a string (transport protocol)")) + return } priority, ok := svca[2].(float64) if !ok { - return fmt.Errorf("malformed service value") + errFunc.add(fmt.Errorf("malformed service value: third item must be an integer (priority)")) + return } weight, ok := svca[3].(float64) if !ok { - return fmt.Errorf("malformed service value") + errFunc.add(fmt.Errorf("malformed service value: fourth item must be an integer (weight)")) + return } port, ok := svca[4].(float64) if !ok { - return fmt.Errorf("malformed service value") + errFunc.add(fmt.Errorf("malformed service value: fifth item must be an integer (port number)")) + return } hostname, ok := svca[5].(string) if !ok { - return fmt.Errorf("malformed service value") + errFunc.add(fmt.Errorf("malformed service value: sixth item must be a string (target)")) + return } sname := "_" + appProtoName + "._" + transportProtoName @@ -955,15 +1072,20 @@ func (rv *rawValue) parseSingleService(svc interface{}, v *Value, relname string Target: hostname, }) - return nil + return } -func (rv *rawValue) parseMap(v *Value, resolve ResolveFunc, depth, mergeDepth int, relname string) error { +func (rv *rawValue) parseMap(v *Value, resolve ResolveFunc, errFunc ErrorFunc, depth, mergeDepth int, relname string) { + if rv.Map == nil { + return + } + m := map[string]json.RawMessage{} err := json.Unmarshal(rv.Map, &m) if err != nil { - return err + errFunc.add(fmt.Errorf("Couldn't unmarshal map: %v", err)) + return } for mk, mv := range m { @@ -980,12 +1102,13 @@ func (rv *rawValue) parseMap(v *Value, resolve ResolveFunc, depth, mergeDepth in // normal case: "map": { "": { ... } } err = json.Unmarshal(mv, rv2) if err != nil { + errFunc.add(fmt.Errorf("Couldn't unmarshal map: %v", err)) continue } } mergedNames := map[string]struct{}{} - rv2.parse(v2, resolve, depth+1, mergeDepth, "", relname, mergedNames) + rv2.parse(v2, resolve, errFunc, depth+1, mergeDepth, "", relname, mergedNames) if v.Map == nil { v.Map = make(map[string]*Value) @@ -993,13 +1116,11 @@ func (rv *rawValue) parseMap(v *Value, resolve ResolveFunc, depth, mergeDepth in 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 { +func (v *Value) moveEmptyMapItems() { if ev, ok := v.Map[""]; ok { if len(v.IP) == 0 { v.IP = ev.IP @@ -1036,5 +1157,4 @@ func (v *Value) moveEmptyMapItems() error { v.Map = ev.Map } } - return nil } diff --git a/ncdomain/convert_test.go b/ncdomain/convert_test.go index 6f46c09..61520a9 100644 --- a/ncdomain/convert_test.go +++ b/ncdomain/convert_test.go @@ -26,8 +26,8 @@ func TestSuite(t *testing.T) { continue } - v, err := ncdomain.ParseValue(k, jsonValue, resolve) - if err != nil { + v := ncdomain.ParseValue(k, jsonValue, resolve, nil) + if v == nil { // TODO continue } diff --git a/server/server.go b/server/server.go index 0f2e043..624270b 100644 --- a/server/server.go +++ b/server/server.go @@ -3,9 +3,12 @@ package server import "github.com/hlandau/madns" import "github.com/hlandau/degoutils/log" import "github.com/hlandau/ncdns/backend" +import "github.com/hlandau/ncdns/namecoin" import "github.com/miekg/dns" import "os" +import "net" import "fmt" +import "strings" import "path/filepath" const version = "1.0" @@ -13,7 +16,8 @@ const version = "1.0" type Server struct { cfg ServerConfig - engine madns.Engine + engine madns.Engine + namecoinConn namecoin.Conn mux *dns.ServeMux udpListener *dns.Server @@ -31,9 +35,18 @@ type ServerConfig struct { NamecoinRPCPassword string `default:"" usage:"Namecoin RPC password"` NamecoinRPCAddress string `default:"localhost:8336" usage:"Namecoin RPC server address"` CacheMaxEntries int `default:"100" usage:"Maximum name cache entries"` - SelfName string `default:"" usage:"Canonical name for this nameserver (default: autogenerated psuedo-hostname resolving to SelfIP; SelfIP is not used if this is set)"` + SelfName string `default:"" usage:"The FQDN of this nameserver. If empty, a psuedo-hostname is generated."` SelfIP string `default:"127.127.127.127" usage:"The canonical IP address for this service"` + HTTPListenAddr string `default:"" usage:"Address for webserver to listen at (default: disabled)"` + + CanonicalSuffix string `default:"bit" usage:"Suffix to advertise via HTTP"` + CanonicalNameservers string `default:"" usage:"Comma-separated list of nameservers to use for NS records. If blank, SelfName (or autogenerated psuedo-hostname) is used."` + canonicalNameservers []string + Hostmaster string `default:"" usage:"Hostmaster e. mail address"` + VanityIPs string `default:"" usage:"Comma separated list of IP addresses to place in A/AAAA records at the zone apex (default: don't add any records)"` + vanityIPs []net.IP + ConfigDir string // path to interpret filenames relative to } @@ -45,13 +58,33 @@ func NewServer(cfg *ServerConfig) (s *Server, err error) { s = &Server{} s.cfg = *cfg + s.cfg.canonicalNameservers = strings.Split(s.cfg.CanonicalNameservers, ",") + for i := range s.cfg.canonicalNameservers { + s.cfg.canonicalNameservers[i] = dns.Fqdn(s.cfg.canonicalNameservers[i]) + } + + vanityIPs := strings.Split(s.cfg.VanityIPs, ",") + for _, ips := range vanityIPs { + ip := net.ParseIP(ips) + if ip == nil { + return nil, fmt.Errorf("Couldn't parse IP: %s", ips) + } + s.cfg.vanityIPs = append(s.cfg.vanityIPs, ip) + } + + s.namecoinConn = namecoin.Conn{ + Username: cfg.NamecoinRPCUsername, + Password: cfg.NamecoinRPCPassword, + Server: cfg.NamecoinRPCAddress, + } + bcfg := &backend.Config{ - RPCUsername: cfg.NamecoinRPCUsername, - RPCPassword: cfg.NamecoinRPCPassword, - RPCAddress: cfg.NamecoinRPCAddress, - CacheMaxEntries: cfg.CacheMaxEntries, - SelfName: cfg.SelfName, - SelfIP: cfg.SelfIP, + NamecoinConn: s.namecoinConn, + CacheMaxEntries: cfg.CacheMaxEntries, + SelfIP: cfg.SelfIP, + Hostmaster: cfg.Hostmaster, + CanonicalNameservers: s.cfg.canonicalNameservers, + VanityIPs: s.cfg.vanityIPs, } b, err := backend.New(bcfg) @@ -89,6 +122,14 @@ func NewServer(cfg *ServerConfig) (s *Server, err error) { } s.engine = e + + if cfg.HTTPListenAddr != "" { + err = webStart(cfg.HTTPListenAddr, s) + if err != nil { + return + } + } + return } diff --git a/server/serverinfo.go b/server/serverinfo.go new file mode 100644 index 0000000..257dbf0 --- /dev/null +++ b/server/serverinfo.go @@ -0,0 +1,28 @@ +package server + +import "os" +import "net" + +var hostname string + +func init() { + names, err := net.LookupAddr("127.0.0.1") + if err != nil || len(names) == 0 { + hn, err := os.Hostname() + if err != nil { + panic(err) + } + hostname = hn + return + } + + hostname = names[0] +} + +func (s *Server) ServerName() string { + n := s.cfg.SelfName + if n == "" { + n = hostname + } + return n +} diff --git a/server/web.go b/server/web.go new file mode 100644 index 0000000..25b99fa --- /dev/null +++ b/server/web.go @@ -0,0 +1,222 @@ +package server + +import "net/http" +import "html/template" +import "github.com/hlandau/degoutils/log" +import "github.com/hlandau/ncdns/util" +import "github.com/hlandau/ncdns/ncdomain" +import "github.com/miekg/dns" +import "github.com/kr/pretty" +import "time" +import "strings" +import "fmt" +import "flag" + +var layoutTpl *template.Template +var mainPageTpl *template.Template +var lookupPageTpl *template.Template + +var tplSetFlag = flag.String("tplset", "std", "Subdirectory of tpl/ to look for templates in (default: std)") + +func initTemplates() error { + if lookupPageTpl != nil { + return nil + } + + if *tplSetFlag == "" { + *tplSetFlag = "std" + } + + var err error + layoutTpl, err = template.ParseFiles(tplFilename("layout")) + if err != nil { + return err + } + + mainPageTpl, err = deriveTemplate(tplFilename("main")) + if err != nil { + return err + } + + lookupPageTpl, err = deriveTemplate(tplFilename("lookup")) + if err != nil { + return err + } + + return nil +} + +func deriveTemplate(filename string) (*template.Template, error) { + cl, err := layoutTpl.Clone() + if err != nil { + return nil, err + } + return cl.ParseFiles(filename) +} + +func tplFilename(filename string) string { + return "tpl/" + *tplSetFlag + "/" + filename + ".tpl" +} + +type webServer struct { + s *Server + sm *http.ServeMux +} + +type layoutInfo struct { + SelfName string + Time string + CanonicalSuffix string + CanonicalNameservers []string + Hostmaster string + CanonicalSuffixHTML template.HTML + TLD string + HasDNSSEC bool +} + +func (ws *webServer) layoutInfo() *layoutInfo { + csparts := strings.SplitN(ws.s.cfg.CanonicalSuffix, ".", 2) + cshtml := `` + csparts[0] + `` + if len(csparts) > 1 { + cshtml = `` + csparts[0] + `.` + csparts[1] + `` + } + + li := &layoutInfo{ + SelfName: ws.s.ServerName(), + Time: time.Now().Format("2006-01-02 15:04:05"), + CanonicalSuffix: ws.s.cfg.CanonicalSuffix, + CanonicalNameservers: ws.s.cfg.canonicalNameservers, + Hostmaster: ws.s.cfg.Hostmaster, + CanonicalSuffixHTML: template.HTML(cshtml), + TLD: "." + csparts[1], + HasDNSSEC: ws.s.cfg.ZonePublicKey != "", + } + + return li +} + +func (ws *webServer) handleRoot(rw http.ResponseWriter, req *http.Request) { + err := mainPageTpl.Execute(rw, ws.layoutInfo()) + log.Infoe(err, "tpl") +} + +func (ws *webServer) handleLookup(rw http.ResponseWriter, req *http.Request) { + info := struct { + layoutInfo + JSONMode bool + JSONValue string + Query string + Advanced bool + NamecoinName string + DomainName string + BareName string + NameParseError error + ExistenceError error + Expired bool + Value string + NCValue *ncdomain.Value + NCValueFmt fmt.Formatter + ParseErrors []error + ParseWarnings []error + RRs []dns.RR + RRError error + Valid bool + }{layoutInfo: *ws.layoutInfo()} + + defer func() { + err := lookupPageTpl.Execute(rw, &info) + log.Infoe(err, "lookup page tpl") + }() + + q := req.FormValue("q") + info.Query = q + info.BareName, info.NamecoinName, info.NameParseError = util.ParseFuzzyDomainNameNC(q) + if info.NameParseError != nil { + return + } + + info.Advanced = (req.FormValue("adv") != "") + info.DomainName = info.BareName + ".bit." + + info.JSONValue = req.FormValue("value") + info.Value = strings.Trim(info.JSONValue, " \t\r\n") + if info.Value == "" { + info.Value, info.ExistenceError = ws.s.namecoinConn.Query(info.NamecoinName) + if info.ExistenceError != nil { + return + } + } else { + info.JSONMode = true + } + + errorFunc := func(e error, isWarning bool) { + if isWarning { + info.ParseWarnings = append(info.ParseWarnings, e) + } else { + info.ParseErrors = append(info.ParseErrors, e) + } + } + + info.NCValue = ncdomain.ParseValue(info.NamecoinName, info.Value, ws.resolveFunc, errorFunc) + if info.NCValue == nil { + return + } + + info.NCValueFmt = pretty.Formatter(info.NCValue) + + info.RRs, info.RRError = info.NCValue.RRsRecursive(nil, info.DomainName, "bit.") + if len(info.ParseErrors) == 0 && info.RRError == nil { + info.Valid = true + } +} + +func (ws *webServer) resolveFunc(name string) (string, error) { + return ws.s.namecoinConn.Query(name) +} + +func (ws *webServer) ServeHTTP(rw http.ResponseWriter, req *http.Request) { + rw.Header().Set("Content-Security-Policy", "default-src 'none'; style-src 'unsafe-inline';") + rw.Header().Set("X-Frame-Options", "DENY") + rw.Header().Set("X-Content-Type-Options", "nosniff") + rw.Header().Set("Server", "ncdns") + //req.Header.Set("Strict-Transport-Security", "max-age=259200") + //req.Header.Set("X-Download-Options", "noopen") + //req.Header.Set("X-XSS-Protection", "0") + //req.Header.Set("X-Permitted-Cross-Domain-Policies", "none") + clearAllCookies(rw, req) + ws.sm.ServeHTTP(rw, req) +} + +func clearAllCookies(rw http.ResponseWriter, req *http.Request) { + for _, ck := range req.Cookies() { + ck2 := http.Cookie{ + Name: ck.Name, + MaxAge: -1, + } + rw.Header().Add("Set-Cookie", ck2.String()) + } +} + +func webStart(listenAddr string, server *Server) error { + err := initTemplates() + if err != nil { + return err + } + + ws := &webServer{ + s: server, + sm: http.NewServeMux(), + } + + ws.sm.HandleFunc("/", ws.handleRoot) + ws.sm.HandleFunc("/lookup", ws.handleLookup) + + s := http.Server{ + Addr: listenAddr, + Handler: ws, + } + + go s.ListenAndServe() + // TODO: error handling + return nil +} diff --git a/util/util.go b/util/util.go index 62b2ccd..eca112b 100644 --- a/util/util.go +++ b/util/util.go @@ -94,17 +94,29 @@ func SplitDomainByFloatingAnchor(qname, anchor string) (subname, basename, rootn // 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 + if !ValidateDomainNameLabel(basename) { + return "", fmt.Errorf("invalid domain name") + } + return basenameToNamecoinKey(basename), nil +} + +func basenameToNamecoinKey(basename string) string { + return "d/" + basename } // 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 + if !strings.HasPrefix(key, "d/") { + return "", fmt.Errorf("not a valid domain name key") + } + + key = key[2:] + if !ValidateDomainNameLabel(key) { + return "", fmt.Errorf("not a valid domain name key") } - return "", fmt.Errorf("not a domain name key") + return key, nil } // This is used to validate NS records, targets in SRV records, etc. In these cases @@ -113,6 +125,7 @@ func NamecoinKeyToBasename(key string) (string, error) { 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_-]*$`) +var re_domainNameLabel = regexp.MustCompilePOSIX(`^(xn--)?[a-z0-9]+(-[a-z0-9]+)*$`) func ValidateHostName(name string) bool { name = dns.Fqdn(name) @@ -127,6 +140,10 @@ func ValidateServiceName(name string) bool { return len(name) < 63 && re_serviceName.MatchString(name) } +func ValidateDomainNameLabel(name string) bool { + return len(name) <= 63 && re_domainNameLabel.MatchString(name) +} + func ValidateEmail(email string) bool { addr, err := mail.ParseAddress(email) if addr == nil || err != nil { @@ -135,4 +152,32 @@ func ValidateEmail(email string) bool { return addr.Name == "" } +// Takes a name in the form "d/example" or "example.bit" and converts it to the +// bareword "example". Returns an error if the input is in neither form. +func ParseFuzzyDomainName(name string) (string, error) { + if strings.HasPrefix(name, "d/") { + return NamecoinKeyToBasename(name) + } + if len(name) > 0 && name[len(name)-1] == '.' { + name = name[0 : len(name)-1] + } + if strings.HasSuffix(name, ".bit") { + name = name[0 : len(name)-4] + if !ValidateDomainNameLabel(name) { + return "", fmt.Errorf("invalid domain name") + } + return name, nil + } + return "", fmt.Errorf("invalid domain name") +} + +func ParseFuzzyDomainNameNC(name string) (bareName string, namecoinKey string, err error) { + name, err = ParseFuzzyDomainName(name) + if err != nil { + return "", "", err + } + + return name, basenameToNamecoinKey(name), nil +} + // © 2014 Hugo Landau GPLv3 or later