mirror of
				https://github.com/StackExchange/dnscontrol.git
				synced 2025-10-25 05:27:26 +08:00 
			
		
		
		
	
		
			
				
	
	
		
			127 lines
		
	
	
	
		
			3.9 KiB
		
	
	
	
		
			Go
		
	
	
	
	
	
			
		
		
	
	
			127 lines
		
	
	
	
		
			3.9 KiB
		
	
	
	
		
			Go
		
	
	
	
	
	
| package normalize
 | |
| 
 | |
| import (
 | |
| 	"fmt"
 | |
| 	"sort"
 | |
| 	"strconv"
 | |
| 	"strings"
 | |
| 
 | |
| 	"github.com/StackExchange/dnscontrol/v4/models"
 | |
| 	"github.com/StackExchange/dnscontrol/v4/pkg/spflib"
 | |
| 	"golang.org/x/exp/constraints"
 | |
| )
 | |
| 
 | |
| func sortedKeys[K constraints.Ordered, V any](m map[K]V) []K {
 | |
| 	keys := make([]K, len(m))
 | |
| 	i := 0
 | |
| 	for k := range m {
 | |
| 		keys[i] = k
 | |
| 		i++
 | |
| 	}
 | |
| 	sort.Slice(keys, func(i, j int) bool { return keys[i] < keys[j] })
 | |
| 	return keys
 | |
| }
 | |
| 
 | |
| // hasSpfRecords returns true if this record requests SPF unrolling.
 | |
| func flattenSPFs(cfg *models.DNSConfig) []error {
 | |
| 	var cache spflib.CachingResolver
 | |
| 	var errs []error
 | |
| 	var err error
 | |
| 	for _, domain := range cfg.Domains {
 | |
| 		txtRecords := domain.Records.GetByType("TXT")
 | |
| 		// flatten all spf records that have the "flatten" metadata
 | |
| 		for _, txt := range txtRecords {
 | |
| 			var rec *spflib.SPFRecord
 | |
| 			txtTarget := txt.GetTargetTXTJoined()
 | |
| 			if txt.Metadata["flatten"] != "" || txt.Metadata["split"] != "" {
 | |
| 				if cache == nil {
 | |
| 					cache, err = spflib.NewCache("spfcache.json")
 | |
| 					if err != nil {
 | |
| 						return []error{err}
 | |
| 					}
 | |
| 				}
 | |
| 				rec, err = spflib.Parse(txtTarget, cache)
 | |
| 				if err != nil {
 | |
| 					errs = append(errs, err)
 | |
| 					continue
 | |
| 				}
 | |
| 			}
 | |
| 			if flatten, ok := txt.Metadata["flatten"]; ok && strings.HasPrefix(txtTarget, "v=spf1") {
 | |
| 				rec = rec.Flatten(flatten)
 | |
| 				err = txt.SetTargetTXT(rec.TXT())
 | |
| 				if err != nil {
 | |
| 					errs = append(errs, err)
 | |
| 					continue
 | |
| 				}
 | |
| 			}
 | |
| 			// now split if needed
 | |
| 			if split, ok := txt.Metadata["split"]; ok {
 | |
| 				overhead1 := 0
 | |
| 				// overhead1: The first segment of the SPF record
 | |
| 				// needs to be shorter than the others due to the overhead of
 | |
| 				// other (non-SPF) txt records.  If there are (for example) 50
 | |
| 				// bytes of txt records also on this domain record, setting
 | |
| 				// overhead1=50 reduces the maxLen by 50. It only affects the
 | |
| 				// first part of the split.
 | |
| 				if oh, ok := txt.Metadata["overhead1"]; ok {
 | |
| 					i, err := strconv.Atoi(oh)
 | |
| 					if err != nil {
 | |
| 						errs = append(errs, Warning{fmt.Errorf("split overhead1 %q is not an int", oh)})
 | |
| 					}
 | |
| 					overhead1 = i
 | |
| 				}
 | |
| 
 | |
| 				// Default txtMaxSize will not result in multiple TXT strings
 | |
| 				txtMaxSize := 255
 | |
| 				if oh, ok := txt.Metadata["txtMaxSize"]; ok {
 | |
| 					i, err := strconv.Atoi(oh)
 | |
| 					if err != nil {
 | |
| 						errs = append(errs, Warning{fmt.Errorf("split txtMaxSize %q is not an int", oh)})
 | |
| 					}
 | |
| 					txtMaxSize = i
 | |
| 				}
 | |
| 
 | |
| 				if !strings.Contains(split, "%d") {
 | |
| 					errs = append(errs, Warning{fmt.Errorf("split format `%s` in `%s` is not proper format (missing %%d)", split, txt.GetLabelFQDN())})
 | |
| 					continue
 | |
| 				}
 | |
| 				recs := rec.TXTSplit(split+"."+domain.Name, overhead1, txtMaxSize)
 | |
| 
 | |
| 				for _, k := range sortedKeys(recs) {
 | |
| 					v := recs[k]
 | |
| 					if k == "@" {
 | |
| 						if err := txt.SetTargetTXTs(v); err != nil {
 | |
| 							errs = append(errs, err)
 | |
| 						}
 | |
| 					} else {
 | |
| 						cp, _ := txt.Copy()
 | |
| 						if err := cp.SetTargetTXTs(v); err != nil {
 | |
| 							errs = append(errs, err)
 | |
| 						}
 | |
| 						cp.SetLabelFromFQDN(k, domain.Name)
 | |
| 						domain.Records = append(domain.Records, cp)
 | |
| 					}
 | |
| 				}
 | |
| 			}
 | |
| 		}
 | |
| 	}
 | |
| 	if cache == nil {
 | |
| 		return errs
 | |
| 	}
 | |
| 	// check if cache is stale
 | |
| 	for _, e := range cache.ResolveErrors() {
 | |
| 		errs = append(errs, Warning{fmt.Errorf("problem resolving SPF record: %w", e)})
 | |
| 	}
 | |
| 	if len(cache.ResolveErrors()) == 0 {
 | |
| 		changed := cache.ChangedRecords()
 | |
| 		if len(changed) > 0 {
 | |
| 			if err := cache.Save("spfcache.updated.json"); err != nil {
 | |
| 				errs = append(errs, err)
 | |
| 			} else if cache.IsCachePreserved() {
 | |
| 				// Only warn if we loaded an existing cache file. The file is still created, which helps people enable this feature.
 | |
| 				errs = append(errs, Warning{fmt.Errorf("%d spf record lookups are out of date with cache (%s).\nWrote changes to spfcache.updated.json. Please rename and commit:\n    $ mv spfcache.updated.json spfcache.json\n    $ git commit -m 'Update spfcache.json' spfcache.json", len(changed), strings.Join(changed, ","))})
 | |
| 			}
 | |
| 		}
 | |
| 	}
 | |
| 	return errs
 | |
| }
 |