Last active
June 30, 2016 19:07
-
-
Save nesv/a8a4b9d76b01b66b6342f06c8b7176f3 to your computer and use it in GitHub Desktop.
Parse SPF records
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
package spf | |
import ( | |
"errors" | |
"fmt" | |
) | |
var ( | |
ErrInvalidPrefix = errors.New("invalid or missing spf prefix") | |
) | |
func NewMalformedMechanismError(m string) error { | |
return fmt.Errorf("malformed mechanism %q", m) | |
} | |
func NewUnknownMechanismTypeError(m string) error { | |
return fmt.Errorf("unknown mechanism type: %v", m) | |
} | |
func NewMalformedModifierError(modifier string) error { | |
return fmt.Errorf("malformed modifier %q", modifier) | |
} | |
func NewDuplicateModifierError(m string) error { | |
return fmt.Errorf("duplicate modifier %q", m) | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
package spf | |
import ( | |
"bytes" | |
"fmt" | |
"net" | |
"strings" | |
) | |
func runTests() { | |
tests := []string{ | |
"v=spf1 ip4:192.168.0.1/16 -all", | |
"v=spf1 a mx -all", | |
"v=spf1 exp=refused.example.com -all", | |
"v=spf1 redirect=bounce.example.com", | |
"v=spf1 fart:narf/14 +all", | |
"v=spf1 ip4:10.0.2.4 -all", | |
"?all", | |
} | |
for _, t := range tests { | |
r, err := Parse(t) | |
if err != nil { | |
fmt.Printf("%q -> error: %v\n", t, err) | |
continue | |
} | |
fmt.Printf("%q -> %q\n", t, r) | |
} | |
} | |
type Qualifier rune | |
const ( | |
Pass Qualifier = '+' // Note pass is a default qualifier should no other qualifier be explicitly stated | |
Fail = '-' | |
SoftFail = '~' | |
Neutral = '?' | |
) | |
type MechanismType string | |
const ( | |
All MechanismType = "all" | |
IP4 = "ip4" | |
IP6 = "ip6" | |
A = "a" | |
MX = "mx" | |
PTR = "ptr" | |
Exists = "exists" | |
Include = "include" | |
) | |
var MechanismTypes = map[MechanismType]struct{}{ | |
All: struct{}{}, | |
IP4: struct{}{}, | |
IP6: struct{}{}, | |
A: struct{}{}, | |
MX: struct{}{}, | |
PTR: struct{}{}, | |
Exists: struct{}{}, | |
Include: struct{}{}, | |
} | |
type Mechanism struct { | |
Qualifier Qualifier | |
Type MechanismType | |
Value string | |
} | |
// Expand returns an expanded version of the mechanism's value, and optionally | |
// prefixes it with the qualifier if the qualifier is any value other than | |
// Pass. | |
// | |
// When the mechanism's type is A, MX, PTR, Exists, or Include, this method may | |
// return an error given that expanding these values may require network | |
// activity. | |
func (m Mechanism) Expand() (string, error) { | |
return "", errors.New("not implemented") | |
} | |
func (m Mechanism) String() string { | |
if m.Value == "" { | |
return fmt.Sprintf("%s%s", string(m.Qualifier), m.Type) | |
} | |
return fmt.Sprintf("%s%s:%s", string(m.Qualifier), m.Type, m.Value) | |
} | |
// ParseMechanism parses the given string representation of an SPF mechanism, m. | |
func ParseMechanism(m string) (Mechanism, error) { | |
mech := Mechanism{} | |
// Check to see if the given string has a qualifier before. | |
// If not, then use the default qualifier "+". | |
if i := strings.IndexAny(m, "+-~?"); i == -1 { | |
mech.Qualifier = Pass | |
} else { | |
mech.Qualifier = Qualifier(m[i]) | |
// Alter the input string to not include the qualifier. | |
m = m[i+1:] | |
} | |
// Check to see if we have a separator anywhere. | |
if sep := strings.Index(m, ":"); sep < 0 { | |
switch v := MechanismType(m); v { | |
case All, A, MX, PTR: | |
// These mechanism types can be bare. | |
mech.Type = v | |
default: | |
return Mechanism{}, NewMalformedMechanismError(m) | |
} | |
} else if sep == 0 { | |
return Mechanism{}, NewMalformedMechanismError(m) | |
} else { | |
mt := MechanismType(m[:sep]) | |
if _, ok := MechanismTypes[mt]; !ok { | |
return Mechanism{}, NewUnknownMechanismTypeError(m[:sep]) | |
} | |
mech.Type = mt | |
v, err := checkMechanismValue(mt, m[sep+1:]) | |
if err != nil { | |
return Mechanism{}, err | |
} | |
mech.Value = v | |
} | |
return mech, nil | |
} | |
// checkMechanismValue validates that v is of an acceptable format for t, | |
// and returns a canonicalized form of v. | |
func checkMechanismValue(t MechanismType, v string) (string, error) { | |
switch t { | |
case IP4, IP6: | |
return checkIP(v) | |
} | |
return v, nil | |
} | |
func checkIP(s string) (string, error) { | |
ip := net.ParseIP(s) | |
if ip != nil { | |
if ip.To4() != nil { | |
return ip.String() + "/32", nil | |
} | |
return ip.String() + "/128", nil | |
} | |
_, ipnet, err := net.ParseCIDR(s) | |
return ipnet.String(), err | |
} | |
func NewMechanism(q Qualifier, t MechanismType, v string) Mechanism { | |
return Mechanism{ | |
Qualifier: q, | |
Type: t, | |
Value: v, | |
} | |
} | |
func hasPrefix(txt string) bool { | |
return strings.HasPrefix(txt, "v=spf1 ") | |
} | |
func isModifier(s string) bool { | |
return strings.HasPrefix(s, "exp=") || strings.HasPrefix(s, "redirect=") | |
} | |
func splitModifier(m string) (name, value string, valid bool) { | |
s := strings.SplitN(m, "=", 2) | |
switch len(s) { | |
case 2: | |
return s[0], s[1], true | |
case 1: | |
return s[0], "", false | |
} | |
return "", "", false | |
} | |
func Parse(spfStr string) (SPF, error) { | |
if !hasPrefix(spfStr) { | |
return SPF{}, ErrInvalidPrefix | |
} | |
var ( | |
mechanisms = make([]Mechanism, 0) | |
modifiers = make(map[string]string) | |
) | |
for _, f := range strings.Fields(spfStr)[1:] { | |
if isModifier(f) { | |
k, v, ok := splitModifier(f) | |
if !ok { | |
return SPF{}, NewMalformedModifierError(f) | |
} | |
if _, exists := modifiers[k]; exists { | |
return SPF{}, NewDuplicateModifierError(k) | |
} else { | |
modifiers[k] = v | |
} | |
continue | |
} | |
m, err := ParseMechanism(f) | |
if err != nil { | |
return SPF{}, err | |
} | |
mechanisms = append(mechanisms, m) | |
} | |
return SPF{ | |
Mechanisms: mechanisms, | |
Modifiers: modifiers, | |
}, nil | |
} | |
type SPF struct { | |
Mechanisms []Mechanism | |
Modifiers map[string]string | |
} | |
func (s SPF) String() string { | |
buf := new(bytes.Buffer) | |
fmt.Fprint(buf, "v=spf1") | |
for k, v := range s.Modifiers { | |
fmt.Fprintf(buf, " %s=%s", k, v) | |
} | |
for _, m := range s.Mechanisms { | |
fmt.Fprintf(buf, " %s", m) | |
} | |
return buf.String() | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment