Parse types like dates and addresses as such

This commit is contained in:
Dolf Schimmel (Freeaqingme)
2016-08-14 19:37:43 +02:00
parent b6b075cbdb
commit 0b25415ca7
3 changed files with 206 additions and 21 deletions

View File

@@ -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
End time.Time
}
// 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"` Begin string `xml:"begin"`
End string `xml:"end"` 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,10 +67,27 @@ type PolicyEvaluated struct {
} }
type Row struct { type Row struct {
// TODO: Figure out how to cast this to an IP SourceIp net.IP
Count int `xml:"count"`
PolicyEvaluated PolicyEvaluated `xml:"policy_evaluated"`
}
func (r *Row) UnmarshalXML(d *xml.Decoder, start xml.StartElement) error {
rowAlias := struct {
SourceIp string `xml:"source_ip"` SourceIp string `xml:"source_ip"`
Count int `xml:"count"` Count int `xml:"count"`
PolicyEvaluated PolicyEvaluated `xml:"policy_evaluated"` 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 {
@@ -46,10 +95,28 @@ type Identifiers struct {
} }
type AuthResult struct { type AuthResult struct {
// FIXME: this could be either DKIM or SPF Type string // Either SPF or DKIM
Domain string `xml:"domain"`
Result string `xml:"result"`
}
func (ar *AuthResult) UnmarshalXML(d *xml.Decoder, start xml.StartElement) error {
res := struct {
XMLName xml.Name XMLName xml.Name
Domain string `xml:"domain"` Domain string `xml:"domain"`
Result string `xml:"result"` 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 {
@@ -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
View 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))
}
}

View File

@@ -1,9 +1,9 @@
package dmarcaggparser package dmarcaggparser
import ( import (
"dmarc"
"flag" "flag"
"fmt" "fmt"
"dmarc"
"os" "os"
) )