ofx died 3y ago
This commit is contained in:
505
vendor/github.com/aclindsa/ofxgo/response.go
generated
vendored
Normal file
505
vendor/github.com/aclindsa/ofxgo/response.go
generated
vendored
Normal file
@@ -0,0 +1,505 @@
|
||||
package ofxgo
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"bytes"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"reflect"
|
||||
"regexp"
|
||||
"strings"
|
||||
|
||||
"github.com/aclindsa/xml"
|
||||
)
|
||||
|
||||
// Response is the top-level object returned from a parsed OFX response file.
|
||||
// It can be inspected by using type assertions or switches on the message set
|
||||
// you're interested in.
|
||||
type Response struct {
|
||||
Version ofxVersion // OFX header version
|
||||
Signon SignonResponse //<SIGNONMSGSETV1>
|
||||
Signup []Message //<SIGNUPMSGSETV1>
|
||||
Bank []Message //<BANKMSGSETV1>
|
||||
CreditCard []Message //<CREDITCARDMSGSETV1>
|
||||
Loan []Message //<LOANMSGSETV1>
|
||||
InvStmt []Message //<INVSTMTMSGSETV1>
|
||||
InterXfer []Message //<INTERXFERMSGSETV1>
|
||||
WireXfer []Message //<WIREXFERMSGSETV1>
|
||||
Billpay []Message //<BILLPAYMSGSETV1>
|
||||
Email []Message //<EMAILMSGSETV1>
|
||||
SecList []Message //<SECLISTMSGSETV1>
|
||||
PresDir []Message //<PRESDIRMSGSETV1>
|
||||
PresDlv []Message //<PRESDLVMSGSETV1>
|
||||
Prof []Message //<PROFMSGSETV1>
|
||||
Image []Message //<IMAGEMSGSETV1>
|
||||
}
|
||||
|
||||
func (or *Response) readSGMLHeaders(r *bufio.Reader) error {
|
||||
b, err := r.ReadSlice('<')
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
s := string(b)
|
||||
err = r.UnreadByte()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// According to the latest OFX SGML spec (1.6), headers should be CRLF-separated
|
||||
// and written as KEY:VALUE. However, some banks include a whitespace after the
|
||||
// colon (KEY: VALUE), while others include no line breaks at all. The spec doesn't
|
||||
// require a line break after the OFX headers, but it is allowed, and will be
|
||||
// optionally captured & discarded by the trailing `\s*`. Valid SGML headers must
|
||||
// always be present in exactly this order, so a regular expression is acceptable.
|
||||
headerExp := regexp.MustCompile(
|
||||
`^OFXHEADER:\s*(?P<OFXHEADER>\d+)\s*` +
|
||||
`DATA:\s*(?P<DATA>[A-Z]+)\s*` +
|
||||
`VERSION:\s*(?P<VERSION>\d+)\s*` +
|
||||
`SECURITY:\s*(?P<SECURITY>[\w]+)\s*` +
|
||||
`ENCODING:\s*(?P<ENCODING>[A-Z0-9-]+)\s*` +
|
||||
`CHARSET:\s*(?P<CHARSET>[\w-]+)\s*` +
|
||||
`COMPRESSION:\s*(?P<COMPRESSION>[A-Z]+)\s*` +
|
||||
`OLDFILEUID:\s*(?P<OLDFILEUID>[\w-]+)\s*` +
|
||||
`NEWFILEUID:\s*(?P<NEWFILEUID>[\w-]+)\s*<$`)
|
||||
|
||||
matches := headerExp.FindStringSubmatch(s)
|
||||
if len(matches) == 0 {
|
||||
return errors.New("OFX headers malformed")
|
||||
}
|
||||
|
||||
for i, name := range headerExp.SubexpNames() {
|
||||
if i == 0 {
|
||||
continue
|
||||
}
|
||||
|
||||
headerValue := matches[i]
|
||||
switch name {
|
||||
case "OFXHEADER":
|
||||
if headerValue != "100" {
|
||||
return errors.New("OFXHEADER is not 100")
|
||||
}
|
||||
case "DATA":
|
||||
if headerValue != "OFXSGML" {
|
||||
return errors.New("OFX DATA header does not contain OFXSGML")
|
||||
}
|
||||
case "VERSION":
|
||||
err := or.Version.FromString(headerValue)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if or.Version > OfxVersion160 {
|
||||
return errors.New("OFX VERSION > 160 in SGML header")
|
||||
}
|
||||
case "SECURITY":
|
||||
if !(headerValue == "NONE" || headerValue == "TYPE1") {
|
||||
return errors.New("OFX SECURITY header must be NONE or TYPE1")
|
||||
}
|
||||
case "COMPRESSION":
|
||||
if headerValue != "NONE" {
|
||||
return errors.New("OFX COMPRESSION header not NONE")
|
||||
}
|
||||
case "ENCODING", "CHARSET", "OLDFILEUID", "NEWFILEUID":
|
||||
// TODO: check/handle these headers?
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (or *Response) readXMLHeaders(decoder *xml.Decoder) error {
|
||||
var tok xml.Token
|
||||
tok, err := nextNonWhitespaceToken(decoder)
|
||||
if err != nil {
|
||||
return err
|
||||
} else if xmlElem, ok := tok.(xml.ProcInst); !ok || xmlElem.Target != "xml" {
|
||||
return errors.New("Missing xml processing instruction")
|
||||
}
|
||||
|
||||
// parse the OFX header
|
||||
tok, err = nextNonWhitespaceToken(decoder)
|
||||
if err != nil {
|
||||
return err
|
||||
} else if ofxElem, ok := tok.(xml.ProcInst); ok && ofxElem.Target == "OFX" {
|
||||
var seenHeader, seenVersion bool = false, false
|
||||
|
||||
headers := bytes.TrimSpace(ofxElem.Inst)
|
||||
for len(headers) > 0 {
|
||||
tmp := bytes.SplitN(headers, []byte("=\""), 2)
|
||||
if len(tmp) != 2 {
|
||||
return errors.New("Malformed OFX header")
|
||||
}
|
||||
header := string(tmp[0])
|
||||
headers = tmp[1]
|
||||
tmp = bytes.SplitN(headers, []byte("\""), 2)
|
||||
if len(tmp) != 2 {
|
||||
return errors.New("Malformed OFX header")
|
||||
}
|
||||
value := string(tmp[0])
|
||||
headers = bytes.TrimSpace(tmp[1])
|
||||
|
||||
switch header {
|
||||
case "OFXHEADER":
|
||||
if value != "200" {
|
||||
return errors.New("OFXHEADER is not 200")
|
||||
}
|
||||
seenHeader = true
|
||||
case "VERSION":
|
||||
err := or.Version.FromString(value)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
seenVersion = true
|
||||
|
||||
if or.Version < OfxVersion200 {
|
||||
return errors.New("OFX VERSION < 200 in XML header")
|
||||
}
|
||||
case "SECURITY":
|
||||
if value != "NONE" {
|
||||
return errors.New("OFX SECURITY header not NONE")
|
||||
}
|
||||
case "OLDFILEUID", "NEWFILEUID":
|
||||
// TODO check/handle these headers?
|
||||
default:
|
||||
return errors.New("Invalid OFX header: " + header)
|
||||
}
|
||||
}
|
||||
|
||||
if !seenHeader {
|
||||
return errors.New("OFXHEADER version missing")
|
||||
}
|
||||
if !seenVersion {
|
||||
return errors.New("OFX VERSION header missing")
|
||||
}
|
||||
|
||||
} else {
|
||||
return errors.New("Missing xml 'OFX' processing instruction")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// Number of bytes of response to read when attempting to figure out whether
|
||||
// we're using OFX or SGML
|
||||
const guessVersionCheckBytes = 1024
|
||||
|
||||
// Defaults to XML if it can't determine the version or if there is any
|
||||
// ambiguity
|
||||
// Returns false for SGML, true (for XML) otherwise.
|
||||
func guessVersion(r *bufio.Reader) (bool, error) {
|
||||
b, _ := r.Peek(guessVersionCheckBytes)
|
||||
if b == nil {
|
||||
return false, errors.New("Failed to read OFX header")
|
||||
}
|
||||
sgmlIndex := bytes.Index(b, []byte("OFXHEADER:"))
|
||||
xmlIndex := bytes.Index(b, []byte("OFXHEADER="))
|
||||
if sgmlIndex < 0 {
|
||||
return true, nil
|
||||
} else if xmlIndex < 0 {
|
||||
return false, nil
|
||||
} else {
|
||||
return xmlIndex <= sgmlIndex, nil
|
||||
}
|
||||
}
|
||||
|
||||
// A map of message set tags to a map of transaction wrapper tags to the
|
||||
// reflect.Type of the struct for that transaction type. Used when decoding
|
||||
// Responses. Newly-implemented response transaction types *must* be added to
|
||||
// this map in order to be unmarshalled.
|
||||
var responseTypes = map[string]map[string]reflect.Type{
|
||||
SignupRs.String(): {
|
||||
(&AcctInfoResponse{}).Name(): reflect.TypeOf(AcctInfoResponse{})},
|
||||
BankRs.String(): {
|
||||
(&StatementResponse{}).Name(): reflect.TypeOf(StatementResponse{})},
|
||||
CreditCardRs.String(): {
|
||||
(&CCStatementResponse{}).Name(): reflect.TypeOf(CCStatementResponse{})},
|
||||
LoanRs.String(): {},
|
||||
InvStmtRs.String(): {
|
||||
(&InvStatementResponse{}).Name(): reflect.TypeOf(InvStatementResponse{})},
|
||||
InterXferRs.String(): {},
|
||||
WireXferRs.String(): {},
|
||||
BillpayRs.String(): {},
|
||||
EmailRs.String(): {},
|
||||
SecListRs.String(): {
|
||||
(&SecListResponse{}).Name(): reflect.TypeOf(SecListResponse{}),
|
||||
(&SecurityList{}).Name(): reflect.TypeOf(SecurityList{})},
|
||||
PresDirRs.String(): {},
|
||||
PresDlvRs.String(): {},
|
||||
ProfRs.String(): {
|
||||
(&ProfileResponse{}).Name(): reflect.TypeOf(ProfileResponse{})},
|
||||
ImageRs.String(): {},
|
||||
}
|
||||
|
||||
func decodeMessageSet(d *xml.Decoder, start xml.StartElement, msgs *[]Message, version ofxVersion) error {
|
||||
setTypes, ok := responseTypes[start.Name.Local]
|
||||
if !ok {
|
||||
return errors.New("Invalid message set: " + start.Name.Local)
|
||||
}
|
||||
for {
|
||||
tok, err := nextNonWhitespaceToken(d)
|
||||
if err != nil {
|
||||
return err
|
||||
} else if end, ok := tok.(xml.EndElement); ok && end.Name.Local == start.Name.Local {
|
||||
// If we found the end of our starting element, we're done parsing
|
||||
return nil
|
||||
} else if startElement, ok := tok.(xml.StartElement); ok {
|
||||
responseType, ok := setTypes[startElement.Name.Local]
|
||||
if !ok {
|
||||
// If you are a developer and received this message after you
|
||||
// thought you added a new transaction type, make sure you
|
||||
// added it to the responseTypes map above
|
||||
return errors.New("Unsupported response transaction for " +
|
||||
start.Name.Local + ": " + startElement.Name.Local)
|
||||
}
|
||||
response := reflect.New(responseType).Interface()
|
||||
responseMessage := response.(Message)
|
||||
if err := d.DecodeElement(responseMessage, &startElement); err != nil {
|
||||
return err
|
||||
}
|
||||
*msgs = append(*msgs, responseMessage)
|
||||
} else {
|
||||
return errors.New("Didn't find an opening element")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ParseResponse parses and validates an OFX response in SGML or XML into a
|
||||
// Response object from the given io.Reader
|
||||
//
|
||||
// It is commonly used as part of Client.Request(), but may be used on its own
|
||||
// to parse already-downloaded OFX files (such as those from 'Web Connect'). It
|
||||
// performs version autodetection if it can and attempts to be as forgiving as
|
||||
// possible about the input format.
|
||||
func ParseResponse(reader io.Reader) (*Response, error) {
|
||||
resp, err := DecodeResponse(reader)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
_, err = resp.Valid()
|
||||
return resp, err
|
||||
}
|
||||
|
||||
// DecodeResponse parses an OFX response in SGML or XML into a Response object
|
||||
// from the given io.Reader
|
||||
func DecodeResponse(reader io.Reader) (*Response, error) {
|
||||
var or Response
|
||||
|
||||
r := bufio.NewReaderSize(reader, guessVersionCheckBytes)
|
||||
xmlVersion, err := guessVersion(r)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// parse SGML headers before creating XML decoder
|
||||
if !xmlVersion {
|
||||
if err := or.readSGMLHeaders(r); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
decoder := xml.NewDecoder(r)
|
||||
if !xmlVersion {
|
||||
decoder.Strict = false
|
||||
decoder.AutoCloseAfterCharData = ofxLeafElements
|
||||
}
|
||||
decoder.CharsetReader = func(charset string, input io.Reader) (io.Reader, error) {
|
||||
return input, nil
|
||||
}
|
||||
|
||||
if xmlVersion {
|
||||
// parse the xml header
|
||||
if err := or.readXMLHeaders(decoder); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
tok, err := nextNonWhitespaceToken(decoder)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
} else if ofxStart, ok := tok.(xml.StartElement); !ok || ofxStart.Name.Local != "OFX" {
|
||||
return nil, errors.New("Missing opening OFX xml element")
|
||||
}
|
||||
|
||||
// Unmarshal the signon message
|
||||
tok, err = nextNonWhitespaceToken(decoder)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
} else if signonStart, ok := tok.(xml.StartElement); ok && signonStart.Name.Local == SignonRs.String() {
|
||||
if err := decoder.Decode(&or.Signon); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
} else {
|
||||
return nil, errors.New("Missing opening SIGNONMSGSRSV1 xml element")
|
||||
}
|
||||
|
||||
tok, err = nextNonWhitespaceToken(decoder)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
} else if signonEnd, ok := tok.(xml.EndElement); !ok || signonEnd.Name.Local != SignonRs.String() {
|
||||
return nil, errors.New("Missing closing SIGNONMSGSRSV1 xml element")
|
||||
}
|
||||
|
||||
var messageSlices = map[string]*[]Message{
|
||||
SignupRs.String(): &or.Signup,
|
||||
BankRs.String(): &or.Bank,
|
||||
CreditCardRs.String(): &or.CreditCard,
|
||||
LoanRs.String(): &or.Loan,
|
||||
InvStmtRs.String(): &or.InvStmt,
|
||||
InterXferRs.String(): &or.InterXfer,
|
||||
WireXferRs.String(): &or.WireXfer,
|
||||
BillpayRs.String(): &or.Billpay,
|
||||
EmailRs.String(): &or.Email,
|
||||
SecListRs.String(): &or.SecList,
|
||||
PresDirRs.String(): &or.PresDir,
|
||||
PresDlvRs.String(): &or.PresDlv,
|
||||
ProfRs.String(): &or.Prof,
|
||||
ImageRs.String(): &or.Image,
|
||||
}
|
||||
|
||||
for {
|
||||
tok, err = nextNonWhitespaceToken(decoder)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
} else if ofxEnd, ok := tok.(xml.EndElement); ok && ofxEnd.Name.Local == "OFX" {
|
||||
return &or, nil // found closing XML element, so we're done
|
||||
} else if start, ok := tok.(xml.StartElement); ok {
|
||||
slice, ok := messageSlices[start.Name.Local]
|
||||
if !ok {
|
||||
return nil, errors.New("Invalid message set: " + start.Name.Local)
|
||||
}
|
||||
if err := decodeMessageSet(decoder, start, slice, or.Version); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
} else {
|
||||
return nil, errors.New("Found unexpected token")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Valid returns whether the Response is valid according to the OFX spec
|
||||
func (or *Response) Valid() (bool, error) {
|
||||
var errs errInvalid
|
||||
if ok, err := or.Signon.Valid(or.Version); !ok {
|
||||
errs.AddErr(err)
|
||||
}
|
||||
for _, messageSet := range [][]Message{
|
||||
or.Signup,
|
||||
or.Bank,
|
||||
or.CreditCard,
|
||||
or.Loan,
|
||||
or.InvStmt,
|
||||
or.InterXfer,
|
||||
or.WireXfer,
|
||||
or.Billpay,
|
||||
or.Email,
|
||||
or.SecList,
|
||||
or.PresDir,
|
||||
or.PresDlv,
|
||||
or.Prof,
|
||||
or.Image,
|
||||
} {
|
||||
for _, message := range messageSet {
|
||||
if ok, err := message.Valid(or.Version); !ok {
|
||||
errs.AddErr(err)
|
||||
}
|
||||
}
|
||||
}
|
||||
err := errs.ErrOrNil()
|
||||
return err == nil, err
|
||||
}
|
||||
|
||||
// Marshal this Response into its SGML/XML representation held in a bytes.Buffer
|
||||
//
|
||||
// If error is non-nil, this bytes.Buffer is ready to be sent to an OFX client
|
||||
func (or *Response) Marshal() (*bytes.Buffer, error) {
|
||||
var b bytes.Buffer
|
||||
|
||||
// Write the header appropriate to our version
|
||||
writeHeader(&b, or.Version, false)
|
||||
|
||||
encoder := xml.NewEncoder(&b)
|
||||
encoder.Indent("", " ")
|
||||
|
||||
ofxElement := xml.StartElement{Name: xml.Name{Local: "OFX"}}
|
||||
|
||||
if err := encoder.EncodeToken(ofxElement); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if ok, err := or.Signon.Valid(or.Version); !ok {
|
||||
return nil, err
|
||||
}
|
||||
signonMsgSet := xml.StartElement{Name: xml.Name{Local: SignonRs.String()}}
|
||||
if err := encoder.EncodeToken(signonMsgSet); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if err := encoder.Encode(&or.Signon); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if err := encoder.EncodeToken(signonMsgSet.End()); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
messageSets := []struct {
|
||||
Messages []Message
|
||||
Type messageType
|
||||
}{
|
||||
{or.Signup, SignupRs},
|
||||
{or.Bank, BankRs},
|
||||
{or.CreditCard, CreditCardRs},
|
||||
{or.Loan, LoanRs},
|
||||
{or.InvStmt, InvStmtRs},
|
||||
{or.InterXfer, InterXferRs},
|
||||
{or.WireXfer, WireXferRs},
|
||||
{or.Billpay, BillpayRs},
|
||||
{or.Email, EmailRs},
|
||||
{or.SecList, SecListRs},
|
||||
{or.PresDir, PresDirRs},
|
||||
{or.PresDlv, PresDlvRs},
|
||||
{or.Prof, ProfRs},
|
||||
{or.Image, ImageRs},
|
||||
}
|
||||
for _, set := range messageSets {
|
||||
if err := encodeMessageSet(encoder, set.Messages, set.Type, or.Version); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
if err := encoder.EncodeToken(ofxElement.End()); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if err := encoder.Flush(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &b, nil
|
||||
}
|
||||
|
||||
// errInvalid represents validation failures while parsing an OFX response
|
||||
// If an institution returns slightly malformed data, ParseResponse will return a best-effort parsed response and a validation error.
|
||||
type errInvalid []error
|
||||
|
||||
func (e errInvalid) Error() string {
|
||||
var errStrings []string
|
||||
for _, err := range e {
|
||||
errStrings = append(errStrings, err.Error())
|
||||
}
|
||||
return fmt.Sprintf("Validation failed: %s", strings.Join(errStrings, "; "))
|
||||
}
|
||||
|
||||
func (e *errInvalid) AddErr(err error) {
|
||||
if err != nil {
|
||||
if errs, ok := err.(errInvalid); ok {
|
||||
*e = append(*e, errs...)
|
||||
} else {
|
||||
*e = append(*e, err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (e errInvalid) ErrOrNil() error {
|
||||
if len(e) > 0 {
|
||||
return e
|
||||
}
|
||||
return nil
|
||||
}
|
||||
Reference in New Issue
Block a user