From ed2e2eb56a6aa977e905c156f83aa47cf35f2e6b Mon Sep 17 00:00:00 2001 From: arzumify Date: Thu, 21 Nov 2024 19:21:27 +0000 Subject: [PATCH] Initial commit --- .gitignore | 1 + LICENSE.md | 157 +++++++++++++++++ README.md | 5 + go.mod | 3 + spf.go | 483 ++++++++++++++++++++++++++++++++++++++++++++++++++++ spf_test.go | 64 +++++++ 6 files changed, 713 insertions(+) create mode 100644 .gitignore create mode 100644 LICENSE.md create mode 100644 README.md create mode 100644 go.mod create mode 100644 spf.go create mode 100644 spf_test.go diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..723ef36 --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +.idea \ No newline at end of file diff --git a/LICENSE.md b/LICENSE.md new file mode 100644 index 0000000..969dabb --- /dev/null +++ b/LICENSE.md @@ -0,0 +1,157 @@ +# GNU LESSER GENERAL PUBLIC LICENSE + +Version 3, 29 June 2007 + +Copyright (C) 2007 Free Software Foundation, Inc. + + +Everyone is permitted to copy and distribute verbatim copies of this +license document, but changing it is not allowed. + +This version of the GNU Lesser General Public License incorporates the +terms and conditions of version 3 of the GNU General Public License, +supplemented by the additional permissions listed below. + +## 0. Additional Definitions. + +As used herein, "this License" refers to version 3 of the GNU Lesser +General Public License, and the "GNU GPL" refers to version 3 of the +GNU General Public License. + +"The Library" refers to a covered work governed by this License, other +than an Application or a Combined Work as defined below. + +An "Application" is any work that makes use of an interface provided +by the Library, but which is not otherwise based on the Library. +Defining a subclass of a class defined by the Library is deemed a mode +of using an interface provided by the Library. + +A "Combined Work" is a work produced by combining or linking an +Application with the Library. The particular version of the Library +with which the Combined Work was made is also called the "Linked +Version". + +The "Minimal Corresponding Source" for a Combined Work means the +Corresponding Source for the Combined Work, excluding any source code +for portions of the Combined Work that, considered in isolation, are +based on the Application, and not on the Linked Version. + +The "Corresponding Application Code" for a Combined Work means the +object code and/or source code for the Application, including any data +and utility programs needed for reproducing the Combined Work from the +Application, but excluding the System Libraries of the Combined Work. + +## 1. Exception to Section 3 of the GNU GPL. + +You may convey a covered work under sections 3 and 4 of this License +without being bound by section 3 of the GNU GPL. + +## 2. Conveying Modified Versions. + +If you modify a copy of the Library, and, in your modifications, a +facility refers to a function or data to be supplied by an Application +that uses the facility (other than as an argument passed when the +facility is invoked), then you may convey a copy of the modified +version: + +- a) under this License, provided that you make a good faith effort + to ensure that, in the event an Application does not supply the + function or data, the facility still operates, and performs + whatever part of its purpose remains meaningful, or +- b) under the GNU GPL, with none of the additional permissions of + this License applicable to that copy. + +## 3. Object Code Incorporating Material from Library Header Files. + +The object code form of an Application may incorporate material from a +header file that is part of the Library. You may convey such object +code under terms of your choice, provided that, if the incorporated +material is not limited to numerical parameters, data structure +layouts and accessors, or small macros, inline functions and templates +(ten or fewer lines in length), you do both of the following: + +- a) Give prominent notice with each copy of the object code that + the Library is used in it and that the Library and its use are + covered by this License. +- b) Accompany the object code with a copy of the GNU GPL and this + license document. + +## 4. Combined Works. + +You may convey a Combined Work under terms of your choice that, taken +together, effectively do not restrict modification of the portions of +the Library contained in the Combined Work and reverse engineering for +debugging such modifications, if you also do each of the following: + +- a) Give prominent notice with each copy of the Combined Work that + the Library is used in it and that the Library and its use are + covered by this License. +- b) Accompany the Combined Work with a copy of the GNU GPL and this + license document. +- c) For a Combined Work that displays copyright notices during + execution, include the copyright notice for the Library among + these notices, as well as a reference directing the user to the + copies of the GNU GPL and this license document. +- d) Do one of the following: + - 0) Convey the Minimal Corresponding Source under the terms of + this License, and the Corresponding Application Code in a form + suitable for, and under terms that permit, the user to + recombine or relink the Application with a modified version of + the Linked Version to produce a modified Combined Work, in the + manner specified by section 6 of the GNU GPL for conveying + Corresponding Source. + - 1) Use a suitable shared library mechanism for linking with + the Library. A suitable mechanism is one that (a) uses at run + time a copy of the Library already present on the user's + computer system, and (b) will operate properly with a modified + version of the Library that is interface-compatible with the + Linked Version. +- e) Provide Installation Information, but only if you would + otherwise be required to provide such information under section 6 + of the GNU GPL, and only to the extent that such information is + necessary to install and execute a modified version of the + Combined Work produced by recombining or relinking the Application + with a modified version of the Linked Version. (If you use option + 4d0, the Installation Information must accompany the Minimal + Corresponding Source and Corresponding Application Code. If you + use option 4d1, you must provide the Installation Information in + the manner specified by section 6 of the GNU GPL for conveying + Corresponding Source.) + +## 5. Combined Libraries. + +You may place library facilities that are a work based on the Library +side by side in a single library together with other library +facilities that are not Applications and are not covered by this +License, and convey such a combined library under terms of your +choice, if you do both of the following: + +- a) Accompany the combined library with a copy of the same work + based on the Library, uncombined with any other library + facilities, conveyed under the terms of this License. +- b) Give prominent notice with the combined library that part of it + is a work based on the Library, and explaining where to find the + accompanying uncombined form of the same work. + +## 6. Revised Versions of the GNU Lesser General Public License. + +The Free Software Foundation may publish revised and/or new versions +of the GNU Lesser General Public License from time to time. Such new +versions will be similar in spirit to the present version, but may +differ in detail to address new problems or concerns. + +Each version is given a distinguishing version number. If the Library +as you received it specifies that a certain numbered version of the +GNU Lesser General Public License "or any later version" applies to +it, you have the option of following the terms and conditions either +of that published version or of any later version published by the +Free Software Foundation. If the Library as you received it does not +specify a version number of the GNU Lesser General Public License, you +may choose any version of the GNU Lesser General Public License ever +published by the Free Software Foundation. + +If the Library as you received it specifies that a proxy can decide +whether future versions of the GNU Lesser General Public License shall +apply, that proxy's public statement of acceptance of any version is +permanent authorization for you to choose that version for the +Library. \ No newline at end of file diff --git a/README.md b/README.md new file mode 100644 index 0000000..7006376 --- /dev/null +++ b/README.md @@ -0,0 +1,5 @@ +~~# Spf + +An ultra-simple library for SPF (Sender Policy Framework) checking in Go. + +[~~![Go Report Card](https://goreportcard.com/badge/git.ailur.dev/ailur/spf)](https://goreportcard.com/report/git.ailur.dev/ailur/spf) [![Go Reference](https://pkg.go.dev/badge/git.ailur.dev/ailur/spf.svg)](https://pkg.go.dev/git.ailur.dev/ailur/spf) diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..065786b --- /dev/null +++ b/go.mod @@ -0,0 +1,3 @@ +module git.ailur.dev/ailur/spf + +go 1.23 diff --git a/spf.go b/spf.go new file mode 100644 index 0000000..68042bf --- /dev/null +++ b/spf.go @@ -0,0 +1,483 @@ +package spf + +import ( + "net" + "strings" +) + +type ErrorType int + +const ( + ErrTypeNeutral ErrorType = iota + ErrTypeNone + ErrTypeFail + ErrTypeSoftFail + ErrTypeInternal +) + +var ( + ErrNeutral = &Error{error: "record returned neutral", errorType: ErrTypeNeutral} + ErrFail = &Error{error: "record returned explicit fail", errorType: ErrTypeFail} + ErrSoftFail = &Error{error: "record returned soft fail", errorType: ErrTypeSoftFail} + ErrNone = &Error{error: "no record returned", errorType: ErrTypeNone} +) + +type Error struct { + error string + errorType ErrorType +} + +func (e *Error) Error() string { + return e.error +} + +func (e *Error) Type() ErrorType { + return e.errorType +} + +func CheckIP(ip string, domain string) *Error { + txt, err := net.LookupTXT(domain) + if err != nil { + return ErrNone + } + + for _, record := range txt { + if strings.HasPrefix(record, "v=spf1") { + parts := strings.Split(record, " ") + for _, part := range parts { + switch { + case strings.HasPrefix(part, "all") || strings.HasPrefix(part, "+all"): + return nil + case strings.HasPrefix(part, "redirect="): + err := CheckIP(ip, part[9:]) + if err != nil { + if err.Type() != ErrTypeInternal { + return err + } + } else { + return nil + } + case strings.HasPrefix(part, "ip4:") || strings.HasPrefix(part, "ip6:"): + if strings.Contains(part, "/") { + _, ipNet, err := net.ParseCIDR(part[4:]) + if err != nil { + return &Error{error: err.Error(), errorType: ErrTypeInternal} + } + if ipNet.Contains(net.ParseIP(ip)) { + return nil + } + } else { + if part[4:] == ip { + return nil + } + } + case strings.HasPrefix(part, "+ip4:") || strings.HasPrefix(part, "+ip6:"): + if strings.Contains(part, "/") { + _, ipNet, err := net.ParseCIDR(part[5:]) + if err != nil { + return &Error{error: err.Error(), errorType: ErrTypeInternal} + } + if ipNet.Contains(net.ParseIP(ip)) { + return nil + } + } else { + if part[5:] == ip { + return nil + } + } + case strings.HasPrefix(part, "-ip4:") || strings.HasPrefix(part, "-ip6:"): + if strings.Contains(part, "/") { + _, ipNet, err := net.ParseCIDR(part[5:]) + if err != nil { + return &Error{error: err.Error(), errorType: ErrTypeInternal} + } + if ipNet.Contains(net.ParseIP(ip)) { + return ErrFail + } + } else { + if part[5:] == ip { + return ErrFail + } + } + case strings.HasPrefix(part, "~ip4:") || strings.HasPrefix(part, "~ip6:"): + if strings.Contains(part, "/") { + _, ipNet, err := net.ParseCIDR(part[5:]) + if err != nil { + return &Error{error: err.Error(), errorType: ErrTypeInternal} + } + if ipNet.Contains(net.ParseIP(ip)) { + return ErrSoftFail + } + } else { + if part[5:] == ip { + return ErrSoftFail + } + } + case strings.HasPrefix(part, "?ip4:") || strings.HasPrefix(part, "?ip6:"): + if strings.Contains(part, "/") { + _, ipNet, err := net.ParseCIDR(part[5:]) + if err != nil { + return &Error{error: err.Error(), errorType: ErrTypeInternal} + } + if ipNet.Contains(net.ParseIP(ip)) { + return ErrNeutral + } + } else { + if part[5:] == ip { + return ErrNeutral + } + } + case strings.HasPrefix(part, "include:"): + err := CheckIP(ip, part[8:]) + if err != nil { + if err.Type() != ErrTypeInternal { + return err + } + } else { + return nil + } + case strings.HasPrefix(part, "+include:"): + err := CheckIP(ip, part[9:]) + if err != nil { + if err.Type() != ErrTypeInternal { + return err + } + } else { + return nil + } + case strings.HasPrefix(part, "-include:"): + err := CheckIP(ip, part[9:]) + if err == nil { + return ErrFail + } + case strings.HasPrefix(part, "~include:"): + err := CheckIP(ip, part[9:]) + if err == nil { + return ErrSoftFail + } + case strings.HasPrefix(part, "?include:"): + err := CheckIP(ip, part[9:]) + if err == nil { + return ErrNeutral + } + case part == "a" || part == "+a": + ips, err := net.LookupIP(domain) + if err != nil { + return &Error{error: err.Error(), errorType: ErrTypeInternal} + } + for _, ipCheck := range ips { + if ipCheck.String() == ip { + return nil + } + } + case part == "-a": + ips, err := net.LookupIP(domain) + if err != nil { + return &Error{error: err.Error(), errorType: ErrTypeInternal} + } + for _, ipCheck := range ips { + if ipCheck.String() == ip { + return ErrFail + } + } + case part == "~a": + ips, err := net.LookupIP(domain) + if err != nil { + return &Error{error: err.Error(), errorType: ErrTypeInternal} + } + for _, ipCheck := range ips { + if ipCheck.String() == ip { + return ErrSoftFail + } + } + case part == "?a": + ips, err := net.LookupIP(domain) + if err != nil { + return &Error{error: err.Error(), errorType: ErrTypeInternal} + } + for _, ipCheck := range ips { + if ipCheck.String() == ip { + return ErrNeutral + } + } + case strings.HasPrefix(part, "a:"): + ips, err := net.LookupIP(part[2:]) + if err != nil { + return &Error{error: err.Error(), errorType: ErrTypeInternal} + } + for _, ipCheck := range ips { + if ipCheck.String() == ip { + return nil + } + } + case strings.HasPrefix(part, "+a:"): + ips, err := net.LookupIP(part[3:]) + if err != nil { + return &Error{error: err.Error(), errorType: ErrTypeInternal} + } + for _, ipCheck := range ips { + if ipCheck.String() == ip { + return nil + } + } + case strings.HasPrefix(part, "-a:"): + ips, err := net.LookupIP(part[3:]) + if err != nil { + return &Error{error: err.Error(), errorType: ErrTypeInternal} + } + for _, ipCheck := range ips { + if ipCheck.String() == ip { + return ErrFail + } + } + case strings.HasPrefix(part, "~a:"): + ips, err := net.LookupIP(part[3:]) + if err != nil { + return &Error{error: err.Error(), errorType: ErrTypeInternal} + } + for _, ipCheck := range ips { + if ipCheck.String() == ip { + return ErrSoftFail + } + } + case strings.HasPrefix(part, "?a:"): + ips, err := net.LookupIP(part[3:]) + if err != nil { + return &Error{error: err.Error(), errorType: ErrTypeInternal} + } + for _, ipCheck := range ips { + if ipCheck.String() == ip { + return ErrNeutral + } + } + case strings.HasPrefix(part, "mx") || strings.HasPrefix(part, "+mx"): + mxs, err := net.LookupMX(domain) + if err != nil { + return &Error{error: err.Error(), errorType: ErrTypeInternal} + } + for _, mx := range mxs { + ips, err := net.LookupIP(mx.Host) + if err != nil { + return &Error{error: err.Error(), errorType: ErrTypeInternal} + } + for _, ipCheck := range ips { + if ipCheck.String() == ip { + return nil + } + } + } + case strings.HasPrefix(part, "-mx"): + mxs, err := net.LookupMX(domain) + if err != nil { + return &Error{error: err.Error(), errorType: ErrTypeInternal} + } + for _, mx := range mxs { + ips, err := net.LookupIP(mx.Host) + if err != nil { + return &Error{error: err.Error(), errorType: ErrTypeInternal} + } + for _, ipCheck := range ips { + if ipCheck.String() == ip { + return ErrFail + } + } + } + case strings.HasPrefix(part, "~mx"): + mxs, err := net.LookupMX(domain) + if err != nil { + return &Error{error: err.Error(), errorType: ErrTypeInternal} + } + for _, mx := range mxs { + ips, err := net.LookupIP(mx.Host) + if err != nil { + return &Error{error: err.Error(), errorType: ErrTypeInternal} + } + for _, ipCheck := range ips { + if ipCheck.String() == ip { + return ErrSoftFail + } + } + } + case strings.HasPrefix(part, "?mx"): + mxs, err := net.LookupMX(domain) + if err != nil { + return &Error{error: err.Error(), errorType: ErrTypeInternal} + } + for _, mx := range mxs { + ips, err := net.LookupIP(mx.Host) + if err != nil { + return &Error{error: err.Error(), errorType: ErrTypeInternal} + } + for _, ipCheck := range ips { + if ipCheck.String() == ip { + return ErrNeutral + } + } + } + case strings.HasPrefix(part, "mx:"): + mxs, err := net.LookupMX(part[3:]) + if err != nil { + return &Error{error: err.Error(), errorType: ErrTypeInternal} + } + for _, mx := range mxs { + ips, err := net.LookupIP(mx.Host) + if err != nil { + return &Error{error: err.Error(), errorType: ErrTypeInternal} + } + for _, ipCheck := range ips { + if ipCheck.String() == ip { + return nil + } + } + } + case strings.HasPrefix(part, "+mx:"): + mxs, err := net.LookupMX(part[4:]) + if err != nil { + return &Error{error: err.Error(), errorType: ErrTypeInternal} + } + for _, mx := range mxs { + ips, err := net.LookupIP(mx.Host) + if err != nil { + return &Error{error: err.Error(), errorType: ErrTypeInternal} + } + for _, ipCheck := range ips { + if ipCheck.String() == ip { + return nil + } + } + } + case strings.HasPrefix(part, "-mx:"): + mxs, err := net.LookupMX(part[4:]) + if err != nil { + return &Error{error: err.Error(), errorType: ErrTypeInternal} + } + for _, mx := range mxs { + ips, err := net.LookupIP(mx.Host) + if err != nil { + return &Error{error: err.Error(), errorType: ErrTypeInternal} + } + for _, ipCheck := range ips { + if ipCheck.String() == ip { + return ErrFail + } + } + } + case strings.HasPrefix(part, "~mx:"): + mxs, err := net.LookupMX(part[4:]) + if err != nil { + return &Error{error: err.Error(), errorType: ErrTypeInternal} + } + for _, mx := range mxs { + ips, err := net.LookupIP(mx.Host) + if err != nil { + return &Error{error: err.Error(), errorType: ErrTypeInternal} + } + for _, ipCheck := range ips { + if ipCheck.String() == ip { + return ErrSoftFail + } + } + } + case strings.HasPrefix(part, "?mx:"): + mxs, err := net.LookupMX(part[4:]) + if err != nil { + return &Error{error: err.Error(), errorType: ErrTypeInternal} + } + for _, mx := range mxs { + ips, err := net.LookupIP(mx.Host) + if err != nil { + return &Error{error: err.Error(), errorType: ErrTypeInternal} + } + for _, ipCheck := range ips { + if ipCheck.String() == ip { + return ErrNeutral + } + } + } + case strings.HasPrefix(part, "ptr:"): + names, err := net.LookupAddr(ip) + if err != nil { + return &Error{error: err.Error(), errorType: ErrTypeInternal} + } + for _, name := range names { + if strings.HasSuffix(name, part[4:]+".") { + return nil + } + } + case strings.HasPrefix(part, "+ptr:"): + names, err := net.LookupAddr(ip) + if err != nil { + return &Error{error: err.Error(), errorType: ErrTypeInternal} + } + for _, name := range names { + if strings.HasSuffix(name, part[5:]+".") { + return nil + } + } + case strings.HasPrefix(part, "-ptr:"): + names, err := net.LookupAddr(ip) + if err != nil { + return &Error{error: err.Error(), errorType: ErrTypeInternal} + } + for _, name := range names { + if strings.HasSuffix(name, part[5:]+".") { + return ErrFail + } + } + case strings.HasPrefix(part, "~ptr:"): + names, err := net.LookupAddr(ip) + if err != nil { + return &Error{error: err.Error(), errorType: ErrTypeInternal} + } + for _, name := range names { + if strings.HasSuffix(name, part[5:]+".") { + return ErrSoftFail + } + } + case strings.HasPrefix(part, "?ptr:"): + names, err := net.LookupAddr(ip) + if err != nil { + return &Error{error: err.Error(), errorType: ErrTypeInternal} + } + for _, name := range names { + if strings.HasSuffix(name, part[5:]+".") { + return ErrNeutral + } + } + case strings.HasPrefix(part, "exists:"): + ips, err := net.LookupIP(part[7:]) + if err != nil { + return &Error{error: err.Error(), errorType: ErrTypeInternal} + } + if len(ips) > 0 { + return nil + } + case strings.HasPrefix(part, "+exists:"): + ips, err := net.LookupIP(part[8:]) + if err != nil { + return &Error{error: err.Error(), errorType: ErrTypeInternal} + } + if len(ips) > 0 { + return nil + } + case strings.HasPrefix(part, "-exists:"): + ips, err := net.LookupIP(part[8:]) + if err != nil { + return &Error{error: err.Error(), errorType: ErrTypeInternal} + } + if len(ips) > 0 { + return ErrFail + } + case strings.HasPrefix(part, "-all"): + return ErrFail + case strings.HasPrefix(part, "~all"): + return ErrSoftFail + case strings.HasPrefix(part, "?all"): + return ErrNeutral + } + } + } + } + + return ErrFail +} diff --git a/spf_test.go b/spf_test.go new file mode 100644 index 0000000..5e1277d --- /dev/null +++ b/spf_test.go @@ -0,0 +1,64 @@ +package spf + +import ( + "fmt" + "net" + "os" + "testing" +) + +func TestError_Error(t *testing.T) { + err := &Error{error: "record returned neutral", errorType: ErrTypeNeutral} + if err.Error() != "record returned neutral" { + t.Error("Error() should return 'record returned neutral'") + } else if err.Type() != ErrTypeNeutral { + t.Error("Type() should return ErrTypeNeutral") + } +} + +func TestCheckIP(t *testing.T) { + includeProviders := true + if os.Getenv("SPF_FOSS_PROVIDERS_ONLY") == "1" { + fmt.Println("Running tests without non-FOSS providers") + includeProviders = false + } + + // Tests for a simple IPv4 address + err := CheckIP("209.51.188.0", "gnu.org") + if err != nil { + t.Error("CheckIP should return nil, got", err) + } + + if includeProviders { + // Tests for Google's complex nested IPv4 addresses with redirects and includes + err = CheckIP("35.190.247.0", "gmail.com") + if err != nil { + t.Error("CheckIP should return nil, got", err) + } + + // Tests for mailgun.org's complex nested IPv4 addresses with redirects and includes + err = CheckIP("146.20.113.0", "mailgun.org") + if err != nil { + t.Error("CheckIP should return nil, got", err) + } + } + + // Tests for PTR records which yahoo still uses for no good reason + err = CheckIP("27.123.204.128", "yahoo.com") + if err != nil { + t.Error("CheckIP should return nil, got", err) + } + + // Tests for cta.social's A and/or MX records + ctaSocialIP, _ := net.LookupIP("cta.social") + err = CheckIP(ctaSocialIP[0].String(), "cta.social") + if err != nil { + t.Error("CheckIP should return nil, got", err) + } + + // Tests for danwin1210.de's simple IPv6 address + err = CheckIP("2a01:4f8:c010:d56::1", "danwin1210.de") + if err != nil { + t.Error("CheckIP should return nil, got", err) + } +}