Parse types like dates and addresses as such
This commit is contained in:
110
dmarc/report.go
110
dmarc/report.go
@@ -3,12 +3,44 @@ package dmarc
|
|||||||
import (
|
import (
|
||||||
"encoding/xml"
|
"encoding/xml"
|
||||||
"io"
|
"io"
|
||||||
|
"fmt"
|
||||||
|
"time"
|
||||||
|
"strconv"
|
||||||
|
"strings"
|
||||||
|
"net"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
const AuthResultType_DKIM = "dkim"
|
||||||
|
const AuthResultType_SPF = "spf"
|
||||||
|
|
||||||
|
|
||||||
type DateRange struct {
|
type DateRange struct {
|
||||||
// TODO: should be int but Y! trailing spaces
|
Begin time.Time
|
||||||
Begin string `xml:"begin"`
|
End time.Time
|
||||||
End string `xml:"end"`
|
}
|
||||||
|
|
||||||
|
// Unmarshal the DateRange element into time.Time objects.
|
||||||
|
// Yahoo tends to put some extra whitespace behind its
|
||||||
|
// timestamps, so we trim that first.
|
||||||
|
func (dr *DateRange) UnmarshalXML(d *xml.Decoder, start xml.StartElement) error {
|
||||||
|
rangeStr := struct {
|
||||||
|
Begin string `xml:"begin"`
|
||||||
|
End string `xml:"end"`
|
||||||
|
}{}
|
||||||
|
d.DecodeElement(&rangeStr, &start)
|
||||||
|
|
||||||
|
begin, err := strconv.Atoi(strings.TrimSpace(rangeStr.Begin))
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
end, err := strconv.Atoi(strings.TrimSpace(rangeStr.End))
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
dr.Begin = time.Unix(int64(begin), 0).UTC()
|
||||||
|
dr.End = time.Unix(int64(end), 0).UTC()
|
||||||
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
type ReportMetadata struct {
|
type ReportMetadata struct {
|
||||||
@@ -35,23 +67,58 @@ type PolicyEvaluated struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type Row struct {
|
type Row struct {
|
||||||
// TODO: Figure out how to cast this to an IP
|
SourceIp net.IP
|
||||||
SourceIp string `xml:"source_ip"`
|
|
||||||
Count int `xml:"count"`
|
Count int `xml:"count"`
|
||||||
PolicyEvaluated PolicyEvaluated `xml:"policy_evaluated"`
|
PolicyEvaluated PolicyEvaluated `xml:"policy_evaluated"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (r *Row) UnmarshalXML(d *xml.Decoder, start xml.StartElement) error {
|
||||||
|
rowAlias := struct {
|
||||||
|
SourceIp string `xml:"source_ip"`
|
||||||
|
Count int `xml:"count"`
|
||||||
|
PolicyEvaluated PolicyEvaluated `xml:"policy_evaluated"`
|
||||||
|
}{}
|
||||||
|
d.DecodeElement(&rowAlias, &start)
|
||||||
|
|
||||||
|
r.Count = rowAlias.Count
|
||||||
|
r.PolicyEvaluated = rowAlias.PolicyEvaluated
|
||||||
|
r.SourceIp = net.ParseIP(rowAlias.SourceIp)
|
||||||
|
if r.SourceIp == nil {
|
||||||
|
return fmt.Errorf("Could not parse source_ip")
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
type Identifiers struct {
|
type Identifiers struct {
|
||||||
HeaderFrom string `xml:"header_from"`
|
HeaderFrom string `xml:"header_from"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type AuthResult struct {
|
type AuthResult struct {
|
||||||
// FIXME: this could be either DKIM or SPF
|
Type string // Either SPF or DKIM
|
||||||
XMLName xml.Name
|
|
||||||
Domain string `xml:"domain"`
|
Domain string `xml:"domain"`
|
||||||
Result string `xml:"result"`
|
Result string `xml:"result"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (ar *AuthResult) UnmarshalXML(d *xml.Decoder, start xml.StartElement) error {
|
||||||
|
res := struct {
|
||||||
|
XMLName xml.Name
|
||||||
|
Domain string `xml:"domain"`
|
||||||
|
Result string `xml:"result"`
|
||||||
|
}{}
|
||||||
|
d.DecodeElement(&res, &start)
|
||||||
|
|
||||||
|
ar.Domain = res.Domain
|
||||||
|
ar.Result = res.Result
|
||||||
|
|
||||||
|
if res.XMLName.Local != AuthResultType_DKIM && res.XMLName.Local != AuthResultType_SPF {
|
||||||
|
return fmt.Errorf("Unrecognized AuthResult type: %s", res.XMLName.Local)
|
||||||
|
}
|
||||||
|
|
||||||
|
ar.Type = res.XMLName.Local
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
type AuthResults struct {
|
type AuthResults struct {
|
||||||
AuthResult []AuthResult `xml:",any"`
|
AuthResult []AuthResult `xml:",any"`
|
||||||
}
|
}
|
||||||
@@ -63,36 +130,39 @@ type Record struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type FeedbackReport struct {
|
type FeedbackReport struct {
|
||||||
XMLName xml.Name `xml:"feedback"`
|
Metadata ReportMetadata `xml:"report_metadata"`
|
||||||
ReportMetadata ReportMetadata `xml:"report_metadata"`
|
|
||||||
PolicyPublished PolicyPublished `xml:"policy_published"`
|
PolicyPublished PolicyPublished `xml:"policy_published"`
|
||||||
Record []Record `xml:"record"`
|
Record []Record `xml:"record"`
|
||||||
}
|
}
|
||||||
|
|
||||||
func ParseReader(xmlFileReader io.Reader) FeedbackReport {
|
func ParseReader(xmlFileReader io.Reader) (*FeedbackReport, error) {
|
||||||
var f FeedbackReport
|
var f *FeedbackReport
|
||||||
|
|
||||||
decoder := xml.NewDecoder(xmlFileReader)
|
decoder := xml.NewDecoder(xmlFileReader)
|
||||||
var inElement string
|
var inElement string
|
||||||
|
|
||||||
for {
|
for {
|
||||||
t, _ := decoder.Token()
|
t, err := decoder.Token()
|
||||||
|
if t == nil && err == io.EOF {
|
||||||
|
break;
|
||||||
|
}
|
||||||
if t == nil {
|
if t == nil {
|
||||||
break
|
return nil, err
|
||||||
}
|
}
|
||||||
switch se := t.(type) {
|
switch se := t.(type) {
|
||||||
case xml.StartElement:
|
case xml.StartElement:
|
||||||
inElement = se.Name.Local
|
inElement = se.Name.Local
|
||||||
if inElement == "feedback" {
|
if inElement == "feedback" {
|
||||||
decoder.DecodeElement(&f, &se)
|
err := decoder.DecodeElement(&f, &se)
|
||||||
// xmlerr := decoder.DecodeElement(&f, &se)
|
if err != nil {
|
||||||
// if xmlerr != nil {
|
return nil, err
|
||||||
// fmt.Printf("decode error: %v\n", xmlerr)
|
}
|
||||||
// }
|
} else {
|
||||||
// fmt.Printf("XMLName: %#v\n", f)
|
return nil, fmt.Errorf("Unknown root element: %s", inElement)
|
||||||
}
|
}
|
||||||
default:
|
default:
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return f
|
|
||||||
|
return f, nil
|
||||||
}
|
}
|
||||||
|
|||||||
115
dmarc/report_test.go
Normal file
115
dmarc/report_test.go
Normal file
@@ -0,0 +1,115 @@
|
|||||||
|
package dmarc
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
"strings"
|
||||||
|
//"fmt"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
var reportXML = `<?xml version="1.0"?>
|
||||||
|
<feedback>
|
||||||
|
<report_metadata>
|
||||||
|
<org_name>Yahoo! Inc.</org_name>
|
||||||
|
<email>postmaster@dmarc.yahoo.com</email>
|
||||||
|
<report_id>1405588706.570207</report_id>
|
||||||
|
<date_range>
|
||||||
|
<begin>1405468800</begin>
|
||||||
|
<end>1405555199 </end>
|
||||||
|
</date_range>
|
||||||
|
</report_metadata>
|
||||||
|
<policy_published>
|
||||||
|
<domain>example.com</domain>
|
||||||
|
<adkim>r</adkim>
|
||||||
|
<aspf>r</aspf>
|
||||||
|
<p>none</p>
|
||||||
|
<pct>100</pct>
|
||||||
|
</policy_published>
|
||||||
|
<record>
|
||||||
|
<row>
|
||||||
|
<source_ip>127.0.0.1</source_ip>
|
||||||
|
<count>2</count>
|
||||||
|
<policy_evaluated>
|
||||||
|
<disposition>none</disposition>
|
||||||
|
<dkim>fail</dkim>
|
||||||
|
<spf>pass</spf>
|
||||||
|
</policy_evaluated>
|
||||||
|
</row>
|
||||||
|
<identifiers>
|
||||||
|
<header_from>example.com</header_from>
|
||||||
|
</identifiers>
|
||||||
|
<auth_results>
|
||||||
|
<dkim>
|
||||||
|
<domain>example.com</domain>
|
||||||
|
<result>neutral</result>
|
||||||
|
</dkim>
|
||||||
|
<spf>
|
||||||
|
<domain>example.com</domain>
|
||||||
|
<result>pass</result>
|
||||||
|
</spf>
|
||||||
|
</auth_results>
|
||||||
|
</record>
|
||||||
|
<record>
|
||||||
|
<row>
|
||||||
|
<source_ip>127.0.0.2</source_ip>
|
||||||
|
<count>988</count>
|
||||||
|
<policy_evaluated>
|
||||||
|
<disposition>none</disposition>
|
||||||
|
<dkim>fail</dkim>
|
||||||
|
<spf>fail</spf>
|
||||||
|
</policy_evaluated>
|
||||||
|
</row>
|
||||||
|
<identifiers>
|
||||||
|
<header_from>idfactor.example.com</header_from>
|
||||||
|
</identifiers>
|
||||||
|
<auth_results>
|
||||||
|
<dkim>
|
||||||
|
<domain>idfactor.example.com</domain>
|
||||||
|
<result>neutral</result>
|
||||||
|
</dkim>
|
||||||
|
<spf>
|
||||||
|
<domain>idfactor.example.com</domain>
|
||||||
|
<result>none</result>
|
||||||
|
</spf>
|
||||||
|
</auth_results>
|
||||||
|
</record>
|
||||||
|
</feedback> `
|
||||||
|
|
||||||
|
func TestInvalidDocument(t *testing.T) {
|
||||||
|
xml := `<?xml version="1.0"?><foobar></foobar>`
|
||||||
|
report, err := ParseReader(strings.NewReader(xml))
|
||||||
|
|
||||||
|
if err == nil {
|
||||||
|
t.Errorf("Expected error, but got none")
|
||||||
|
}
|
||||||
|
|
||||||
|
if report != nil {
|
||||||
|
t.Errorf("Report should have been <nil> but wasn't")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestUnmarshalling(t *testing.T) {
|
||||||
|
report, err := ParseReader(strings.NewReader(reportXML))
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
t.Errorf("Got error: %s:", err.Error())
|
||||||
|
}
|
||||||
|
|
||||||
|
begin := time.Unix(1405468800, 0).UTC()
|
||||||
|
if report.Metadata.DateRange.Begin != begin {
|
||||||
|
t.Errorf("report.Metadata.DateRange.Begin did not match expected date")
|
||||||
|
}
|
||||||
|
|
||||||
|
end := time.Unix(1405555199, 0).UTC()
|
||||||
|
if report.Metadata.DateRange.End != end {
|
||||||
|
t.Errorf("report.Metadata.DateRange.end did not match expected date")
|
||||||
|
}
|
||||||
|
|
||||||
|
if report.PolicyPublished.Domain != "example.com" {
|
||||||
|
t.Errorf("report.PolicyPublished.Domain was '%s', expected 'example.com'", report.PolicyPublished.Domain)
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(report.Record) != 2 {
|
||||||
|
t.Errorf("report.Record was expected to contain 2 items, only got: %d", len(report.Record))
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,9 +1,9 @@
|
|||||||
package dmarcaggparser
|
package dmarcaggparser
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"dmarc"
|
||||||
"flag"
|
"flag"
|
||||||
"fmt"
|
"fmt"
|
||||||
"dmarc"
|
|
||||||
"os"
|
"os"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user