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