package models import ( "fmt" "net" ) // PopulateFromStringFunc populates a RecordConfig by parsing a common RFC1035-like format. // // rtype: the resource record type (rtype) // contents: a string that contains all parameters of the record's rdata (see below) // txtFn: If rtype == "TXT", this function is used to parse contents, or nil if no parsing is needed. // // The "contents" field is the format used in RFC1035 zonefiles. It is the text // after the rtype. For example, in the line: foo IN MX 10 mx.example.com. // contents stores everything after the "MX" (not including the space). // // Typical values for txtFn include: // // nil: no parsing required. // txtutil.ParseQuoted: Parse via Tom's interpretation of RFC1035. // txtutil.ParseCombined: Backwards compatible with Parse via miekg's interpretation of RFC1035. // // Many providers deliver record data in this format or something close to it. // This function is provided to reduce the amount of duplicate code across // providers. If a particular rtype is not handled as a particular provider // expects, simply handle it beforehand as a special case. // // Example 1: Normal use. // // rtype := FILL_IN_RTYPE // rc := &models.RecordConfig{Type: rtype, TTL: FILL_IN_TTL} // rc.SetLabelFromFQDN(FILL_IN_NAME, origin) // rc.Original = FILL_IN_ORIGINAL // The raw data received from provider (if needed later) // if err = rc.PopulateFromStringFunc(rtype, target, origin, nil); err != nil { // return nil, fmt.Errorf("unparsable record type=%q received from PROVDER_NAME: %w", rtype, err) // } // return rc, nil // // Example 2: Use your own MX parser. // // rtype := FILL_IN_RTYPE // rc := &models.RecordConfig{Type: rtype, TTL: FILL_IN_TTL} // rc.SetLabelFromFQDN(FILL_IN_NAME, origin) // rc.Original = FILL_IN_ORIGINAL // The raw data received from provider (if needed later) // switch rtype { // case "MX": // // MX priority in a separate field. // err = rc.SetTargetMX(cr.Priority, target) // default: // err = rc.PopulateFromString(rtype, target, origin) // } // if err != nil { // return nil, fmt.Errorf("unparsable record type=%q received from PROVDER_NAME: %w", rtype, err) // } // return rc, nil func (rc *RecordConfig) PopulateFromStringFunc(rtype, contents, origin string, txtFn func(s string) (string, error)) error { if rc.Type != "" && rc.Type != rtype { return fmt.Errorf("assertion failed: rtype already set (%s) (%s)", rtype, rc.Type) } switch rc.Type = rtype; rtype { // #rtype_variations case "A": ip := net.ParseIP(contents) if ip == nil || ip.To4() == nil { return fmt.Errorf("invalid IP in A record: %s", contents) } return rc.SetTargetIP(ip) // Reformat to canonical form. case "AAAA": ip := net.ParseIP(contents) if ip == nil || ip.To16() == nil { return fmt.Errorf("invalid IP in AAAA record: %s", contents) } return rc.SetTargetIP(ip) // Reformat to canonical form. case "AKAMAICDN", "ALIAS", "ANAME", "CNAME", "NS", "PTR": return rc.SetTarget(contents) case "CAA": return rc.SetTargetCAAString(contents) case "DS": return rc.SetTargetDSString(contents) case "DNSKEY": return rc.SetTargetDNSKEYString(contents) case "DHCID": return rc.SetTarget(contents) case "DNAME": return rc.SetTarget(contents) case "LOC": return rc.SetTargetLOCString(origin, contents) case "MX": return rc.SetTargetMXString(contents) case "NAPTR": return rc.SetTargetNAPTRString(contents) case "SOA": return rc.SetTargetSOAString(contents) case "SPF", "TXT": if txtFn == nil { return rc.SetTargetTXT(contents) } t, err := txtFn(contents) if err != nil { return fmt.Errorf("invalid TXT record: %s", contents) } return rc.SetTargetTXT(t) case "SRV": return rc.SetTargetSRVString(contents) case "SSHFP": return rc.SetTargetSSHFPString(contents) case "SVCB", "HTTPS": return rc.SetTargetSVCBString(origin, contents) case "TLSA": return rc.SetTargetTLSAString(contents) default: //return fmt.Errorf("unknown rtype (%s) when parsing (%s) domain=(%s)", rtype, contents, origin) return MakeUnknown(rc, rtype, contents, origin) } } // PopulateFromString populates a RecordConfig given a type and string. Many // providers give all the parameters of a resource record in one big string. // This helper function lets you not re-invent the wheel. // // NOTE: You almost always want to special-case TXT records. Every provider // seems to quote them differently. // // Recommended calling convention: Process the exceptions first, then use the // PopulateFromString function for everything else. // // rtype := FILL_IN_TYPE // var err error // rc := &models.RecordConfig{Type: rtype} // rc.SetLabelFromFQDN(FILL_IN_NAME, origin) // rc.TTL = uint32(FILL_IN_TTL) // rc.Original = FILL_IN_ORIGINAL // The raw data received from provider (if needed later) // switch rtype { // case "MX": // // MX priority in a separate field. // err = rc.SetTargetMX(cr.Priority, target) // case "TXT": // // TXT records are stored verbatim; no quoting/escaping to parse. // err = rc.SetTargetTXT(target) // default: // err = rc.PopulateFromString(rtype, target, origin) // } // if err != nil { // return nil, fmt.Errorf("unparsable record type=%q received from PROVDER_NAME: %w", rtype, err) // } // return rc, nil func (rc *RecordConfig) PopulateFromString(rtype, contents, origin string) error { if rc.Type != "" && rc.Type != rtype { panic(fmt.Errorf("assertion failed: rtype already set (%s) (%s)", rtype, rc.Type)) } switch rc.Type = rtype; rtype { // #rtype_variations case "A": ip := net.ParseIP(contents) if ip == nil || ip.To4() == nil { return fmt.Errorf("invalid IP in A record: %s", contents) } return rc.SetTargetIP(ip) // Reformat to canonical form. case "AAAA": ip := net.ParseIP(contents) if ip == nil || ip.To16() == nil { return fmt.Errorf("invalid IP in AAAA record: %s", contents) } return rc.SetTargetIP(ip) // Reformat to canonical form. case "AKAMAICDN", "ALIAS", "ANAME", "CNAME", "NS", "PTR": return rc.SetTarget(contents) case "CAA": return rc.SetTargetCAAString(contents) case "DS": return rc.SetTargetDSString(contents) case "DNSKEY": return rc.SetTargetDNSKEYString(contents) case "DHCID": return rc.SetTarget(contents) case "DNAME": return rc.SetTarget(contents) case "LOC": return rc.SetTargetLOCString(origin, contents) case "MX": return rc.SetTargetMXString(contents) case "NAPTR": return rc.SetTargetNAPTRString(contents) case "SOA": return rc.SetTargetSOAString(contents) case "SPF", "TXT": return rc.SetTargetTXTs(ParseQuotedTxt(contents)) case "SRV": return rc.SetTargetSRVString(contents) case "SSHFP": return rc.SetTargetSSHFPString(contents) case "SVCB", "HTTPS": return rc.SetTargetSVCBString(origin, contents) case "TLSA": return rc.SetTargetTLSAString(contents) default: return fmt.Errorf("unknown rtype (%s) when parsing (%s) domain=(%s)", rtype, contents, origin) } }