mirror of
https://github.com/aclindsa/ofxgo.git
synced 2024-11-22 03:30:04 -05:00
Initial commit
This commit is contained in:
commit
99cd8f7273
23
acctinfo.go
Normal file
23
acctinfo.go
Normal file
@ -0,0 +1,23 @@
|
||||
package ofxgo
|
||||
|
||||
import (
|
||||
"github.com/golang/go/src/encoding/xml"
|
||||
)
|
||||
|
||||
type OfxAcctInfoRequest struct {
|
||||
XMLName xml.Name `xml:"ACCTINFOTRNRQ"`
|
||||
TrnUID OfxUID `xml:"TRNUID"`
|
||||
CltCookie OfxInt `xml:"CLTCOOKIE"`
|
||||
DtAcctup OfxDate `xml:"ACCTINFORQ>DTACCTUP"`
|
||||
}
|
||||
|
||||
func (r *OfxAcctInfoRequest) Name() string {
|
||||
return "ACCTINFOTRNRQ"
|
||||
}
|
||||
|
||||
func (r *OfxAcctInfoRequest) Valid() (bool, error) {
|
||||
if ok, err := r.TrnUID.Valid(); !ok {
|
||||
return false, err
|
||||
}
|
||||
return true, nil
|
||||
}
|
236
leaf_elements.go
Normal file
236
leaf_elements.go
Normal file
@ -0,0 +1,236 @@
|
||||
package ofxgo
|
||||
|
||||
// A list of all the leaf elements in OFX 1.0.3 (the last SGML version of the
|
||||
// spec). These are all the elements that are possibly left unclosed, and which
|
||||
// can have no children of their own. Fortunately these two sets of elements
|
||||
// are the same. We use this list when parsing to remove ambiguities about
|
||||
// element nesting.
|
||||
//
|
||||
// Generated using the following command with the 1.0.3 SPEC .dtd file:
|
||||
// # sed -rn 's/^<!ELEMENT\s+([A-Z]+)\s+-\s+o\s+%.*TYPE>.*$/\t"\1",/p' *.dtd | sort
|
||||
var ofxLeafElements = []string{
|
||||
"ACCESSKEY",
|
||||
"ACCTID",
|
||||
"ACCTKEY",
|
||||
"ACCTREQUIRED",
|
||||
"ACCTTYPE",
|
||||
"ADJAMT",
|
||||
"ADJDATE",
|
||||
"ADJDESC",
|
||||
"ADJNO",
|
||||
"APPID",
|
||||
"APPVER",
|
||||
"AUTHTOKEN",
|
||||
"AUTHTOKENFIRST",
|
||||
"AUTHTOKENINFOURL",
|
||||
"AUTHTOKENLABEL",
|
||||
"AVAILACCTS",
|
||||
"BALAMT",
|
||||
"BALCLOSE",
|
||||
"BALMIN",
|
||||
"BALOPEN",
|
||||
"BALTYPE",
|
||||
"BANKID",
|
||||
"BILLREFINFO",
|
||||
"BRANCHID",
|
||||
"CANADDPAYEE",
|
||||
"CANBILLPAY",
|
||||
"CANCELWND",
|
||||
"CANEMAIL",
|
||||
"CANMODMDLS",
|
||||
"CANMODPMTS",
|
||||
"CANMODXFERS",
|
||||
"CANNOTIFY",
|
||||
"CANPENDING",
|
||||
"CANRECUR",
|
||||
"CANSCHED",
|
||||
"CANUSEDESC",
|
||||
"CANUSERANGE",
|
||||
"CASESEN",
|
||||
"CHARTYPE",
|
||||
"CHECKNUM",
|
||||
"CHGPINFIRST",
|
||||
"CHGUSERINFO",
|
||||
"CHKANDDEB",
|
||||
"CHKERROR",
|
||||
"CHKNUMEND",
|
||||
"CHKNUMSTART",
|
||||
"CHKSTATUS",
|
||||
"CITY",
|
||||
"CLIENTACTREQ",
|
||||
"CLIENTROUTING",
|
||||
"CLIENTUID",
|
||||
"CLIENTUIDREQ",
|
||||
"CLOSINGAVAIL",
|
||||
"CLTCOOKIE",
|
||||
"CODE",
|
||||
"CONFMSG",
|
||||
"CORRECTACTION",
|
||||
"CORRECTFITID",
|
||||
"COUNTRY",
|
||||
"CREDITLIMIT",
|
||||
"CSPHONE",
|
||||
"CURDEF",
|
||||
"CURRATE",
|
||||
"CURSYM",
|
||||
"DATEBIRTH",
|
||||
"DAYPHONE",
|
||||
"DAYSTOPAY",
|
||||
"DAYSWITH",
|
||||
"DEBADJ",
|
||||
"DEPANDCREDIT",
|
||||
"DESC",
|
||||
"DFLTDAYSTOPAY",
|
||||
"DIFFFIRSTPMT",
|
||||
"DIFFLASTPMT",
|
||||
"DOMXFERFEE",
|
||||
"DSCAMT",
|
||||
"DSCDATE",
|
||||
"DSCDESC",
|
||||
"DSCRATE",
|
||||
"DTACCTUP",
|
||||
"DTASOF",
|
||||
"DTAVAIL",
|
||||
"DTCHANGED",
|
||||
"DTCLIENT",
|
||||
"DTCLOSE",
|
||||
"DTCREATED",
|
||||
"DTDUE",
|
||||
"DTEND",
|
||||
"DTEXPIRE",
|
||||
"DTINFOCHG",
|
||||
"DTNEXT",
|
||||
"DTOPEN",
|
||||
"DTPMTDUE",
|
||||
"DTPMTPRC",
|
||||
"DTPOSTED",
|
||||
"DTPOSTEND",
|
||||
"DTPOSTSTART",
|
||||
"DTPROFUP",
|
||||
"DTPURCHASE",
|
||||
"DTSERVER",
|
||||
"DTSTART",
|
||||
"DTUSER",
|
||||
"DTXFERPRC",
|
||||
"DTXFERPRJ",
|
||||
"EMAIL",
|
||||
"EVEPHONE",
|
||||
"EXTDPMTCHK",
|
||||
"EXTDPMTFOR",
|
||||
"FAXPHONE",
|
||||
"FEE",
|
||||
"FEEMSG",
|
||||
"FICERTID",
|
||||
"FID",
|
||||
"FINALAMT",
|
||||
"FINAME",
|
||||
"FINCHG",
|
||||
"FIRSTNAME",
|
||||
"FITID",
|
||||
"FREQ",
|
||||
"FROM",
|
||||
"GENUSERKEY",
|
||||
"GETMIMESUP",
|
||||
"HASEXTDPMT",
|
||||
"IDSCOPE",
|
||||
"INCIMAGES",
|
||||
"INITIALAMT",
|
||||
"INTLXFERFEE",
|
||||
"INVALIDACCTTYPE",
|
||||
"INVDATE",
|
||||
"INVDESC",
|
||||
"INVNO",
|
||||
"INVPAIDAMT",
|
||||
"INVTOTALAMT",
|
||||
"LANGUAGE",
|
||||
"LASTNAME",
|
||||
"LITMAMT",
|
||||
"LITMDESC",
|
||||
"LOSTSYNC",
|
||||
"MAILSUP",
|
||||
"MAX",
|
||||
"MEMO",
|
||||
"MESSAGE",
|
||||
"MFACHALLENGEFIRST",
|
||||
"MFACHALLENGESUPT",
|
||||
"MFAPHRASEA",
|
||||
"MFAPHRASEID",
|
||||
"MFAPHRASELABEL",
|
||||
"MIDDLENAME",
|
||||
"MIN",
|
||||
"MINPMTDUE",
|
||||
"MKTGINFO",
|
||||
"MODELWND",
|
||||
"MODPENDING",
|
||||
"NAME",
|
||||
"NEWUSERPASS",
|
||||
"NINSTS",
|
||||
"NONCE",
|
||||
"OFXSEC",
|
||||
"ORG",
|
||||
"PAYACCT",
|
||||
"PAYANDCREDIT",
|
||||
"PAYEEID",
|
||||
"PAYEELSTID",
|
||||
"PAYINSTRUCT",
|
||||
"PHONE",
|
||||
"PINCH",
|
||||
"PMTBYADDR",
|
||||
"PMTBYPAYEEID",
|
||||
"PMTBYXFER",
|
||||
"PMTPRCCODE",
|
||||
"POSTALCODE",
|
||||
"POSTPROCWND",
|
||||
"PROCDAYSOFF",
|
||||
"PROCENDTM",
|
||||
"PURANDADV",
|
||||
"RECSRVRTID",
|
||||
"REFNUM",
|
||||
"REFRESH",
|
||||
"REFRESHSUPT",
|
||||
"REJECTIFMISSING",
|
||||
"RESPFILEER",
|
||||
"SECURITYNAME",
|
||||
"SESSCOOKIE",
|
||||
"SEVERITY",
|
||||
"SIC",
|
||||
"SIGNONREALM",
|
||||
"SPACES",
|
||||
"SPECIAL",
|
||||
"SPNAME",
|
||||
"SRVRTID",
|
||||
"STATE",
|
||||
"STPCHKFEE",
|
||||
"STSVIAMODS",
|
||||
"SUBJECT",
|
||||
"SUPTXDL",
|
||||
"SVC",
|
||||
"SVCSTATUS",
|
||||
"SYNCMODE",
|
||||
"TAN",
|
||||
"TAXID",
|
||||
"TEMPPASS",
|
||||
"TO",
|
||||
"TOKEN",
|
||||
"TOKENONLY",
|
||||
"TOTALFEES",
|
||||
"TOTALINT",
|
||||
"TRANSPSEC",
|
||||
"TRNAMT",
|
||||
"TRNTYPE",
|
||||
"TRNUID",
|
||||
"TSKEYEXPIRE",
|
||||
"TSPHONE",
|
||||
"URL",
|
||||
"USEHTML",
|
||||
"USERID",
|
||||
"USERKEY",
|
||||
"USERPASS",
|
||||
"VALUE",
|
||||
"VER",
|
||||
"XFERDAYSWITH",
|
||||
"XFERDEST",
|
||||
"XFERDFLTDAYSTOPAY",
|
||||
"XFERPRCCODE",
|
||||
"XFERSRC",
|
||||
}
|
378
ofx.go
Normal file
378
ofx.go
Normal file
@ -0,0 +1,378 @@
|
||||
package ofxgo
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"bytes"
|
||||
"errors"
|
||||
"fmt"
|
||||
"github.com/golang/go/src/encoding/xml"
|
||||
"io"
|
||||
"net/http"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
type OfxMessage interface {
|
||||
Name() string
|
||||
Valid() (bool, error)
|
||||
}
|
||||
|
||||
type OfxRequest struct {
|
||||
URL string
|
||||
Version string // String for OFX header, defaults to 203
|
||||
Signon OfxSignonRequest //<SIGNONMSGSETV1>
|
||||
Signup []OfxMessage //<SIGNUPMSGSETV1>
|
||||
//<BANKMSGSETV1>
|
||||
//<CREDITCARDMSGSETV1>
|
||||
//<LOANMSGSETV1>
|
||||
//<INVSTMTMSGSETV1>
|
||||
//<INTERXFERMSGSETV1>
|
||||
//<WIREXFERMSGSETV1>
|
||||
//<BILLPAYMSGSETV1>
|
||||
//<EMAILMSGSETV1>
|
||||
//<SECLISTMSGSETV1>
|
||||
//<PRESDIRMSGSETV1>
|
||||
//<PRESDLVMSGSETV1>
|
||||
Profile []OfxMessage //<PROFMSGSETV1>
|
||||
//<IMAGEMSGSETV1>
|
||||
}
|
||||
|
||||
func (oq *OfxRequest) marshalMessageSet(e *xml.Encoder, requests []OfxMessage, setname string) error {
|
||||
if len(requests) > 0 {
|
||||
messageSetElement := xml.StartElement{Name: xml.Name{Local: setname}}
|
||||
if err := e.EncodeToken(messageSetElement); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
for _, request := range requests {
|
||||
if ok, err := request.Valid(); !ok {
|
||||
return err
|
||||
}
|
||||
if err := e.Encode(request); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
if err := e.EncodeToken(messageSetElement.End()); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (oq *OfxRequest) Marshal() (*bytes.Buffer, error) {
|
||||
var b bytes.Buffer
|
||||
|
||||
if len(oq.Version) == 0 {
|
||||
oq.Version = "203"
|
||||
}
|
||||
|
||||
switch oq.Version {
|
||||
case "102", "103", "151", "160":
|
||||
b.WriteString(`OFXHEADER:100
|
||||
DATA:OFXSGML
|
||||
VERSION:` + oq.Version + `
|
||||
SECURITY:NONE
|
||||
ENCODING:USASCII
|
||||
CHARSET:1252
|
||||
COMPRESSION:NONE
|
||||
OLDFILEUID:NONE
|
||||
NEWFILEUID:NONE
|
||||
|
||||
`)
|
||||
case "200", "201", "202", "203", "210", "211", "220":
|
||||
b.WriteString(`<?xml version="1.0" encoding="UTF-8" standalone="no"?>` + "\n")
|
||||
b.WriteString(`<?OFX OFXHEADER="200" VERSION="` + oq.Version + `" SECURITY="NONE" OLDFILEUID="NONE" NEWFILEUID="NONE"?>` + "\n")
|
||||
default:
|
||||
return nil, errors.New(oq.Version + " is not a valid OFX version string")
|
||||
}
|
||||
|
||||
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 := oq.Signon.Valid(); !ok {
|
||||
return nil, err
|
||||
}
|
||||
signonMsgSet := xml.StartElement{Name: xml.Name{Local: "SIGNONMSGSRQV1"}}
|
||||
if err := encoder.EncodeToken(signonMsgSet); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if err := encoder.Encode(&oq.Signon); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if err := encoder.EncodeToken(signonMsgSet.End()); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if err := oq.marshalMessageSet(encoder, oq.Signup, "SIGNUPMSGSRQV1"); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if err := oq.marshalMessageSet(encoder, oq.Profile, "PROFMSGSRQV1"); 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
|
||||
}
|
||||
|
||||
func (oq *OfxRequest) Request() (*OfxResponse, error) {
|
||||
oq.Signon.Dtclient = OfxDate(time.Now())
|
||||
|
||||
b, err := oq.Marshal()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
response, err := http.Post(oq.URL, "application/x-ofx", b)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer response.Body.Close()
|
||||
|
||||
if response.StatusCode != 200 {
|
||||
return nil, errors.New("OFXQuery request status: " + response.Status)
|
||||
}
|
||||
|
||||
// Help the parser out by giving it a clue about what header format to
|
||||
// expect
|
||||
var xmlVersion bool = true
|
||||
switch oq.Version {
|
||||
case "102", "103", "151", "160":
|
||||
xmlVersion = false
|
||||
}
|
||||
|
||||
var ofxresp OfxResponse
|
||||
if err := ofxresp.Unmarshal(response.Body, xmlVersion); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &ofxresp, nil
|
||||
}
|
||||
|
||||
type OfxResponse struct {
|
||||
Version string // String for OFX header, defaults to 203
|
||||
Signon OfxSignonResponse //<SIGNONMSGSETV1>
|
||||
Signup []OfxMessage //<SIGNUPMSGSETV1>
|
||||
//<BANKMSGSETV1>
|
||||
//<CREDITCARDMSGSETV1>
|
||||
//<LOANMSGSETV1>
|
||||
//<INVSTMTMSGSETV1>
|
||||
//<INTERXFERMSGSETV1>
|
||||
//<WIREXFERMSGSETV1>
|
||||
//<BILLPAYMSGSETV1>
|
||||
//<EMAILMSGSETV1>
|
||||
//<SECLISTMSGSETV1>
|
||||
//<PRESDIRMSGSETV1>
|
||||
//<PRESDLVMSGSETV1>
|
||||
Profile []OfxMessage //<PROFMSGSETV1>
|
||||
//<IMAGEMSGSETV1>
|
||||
}
|
||||
|
||||
func (or *OfxResponse) readSGMLHeaders(r *bufio.Reader) error {
|
||||
var seenHeader, seenVersion bool = false, false
|
||||
for {
|
||||
line, err := r.ReadString('\n')
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
// r.ReadString leaves the '\n' on the end...
|
||||
line = strings.TrimSpace(line)
|
||||
|
||||
if len(line) == 0 {
|
||||
if seenHeader {
|
||||
break
|
||||
} else {
|
||||
continue
|
||||
}
|
||||
}
|
||||
header := strings.SplitN(line, ":", 2)
|
||||
if header == nil || len(header) != 2 {
|
||||
return errors.New("OFX headers malformed")
|
||||
}
|
||||
|
||||
switch header[0] {
|
||||
case "OFXHEADER":
|
||||
if header[1] != "100" {
|
||||
return errors.New("OFXHEADER is not 100")
|
||||
}
|
||||
seenHeader = true
|
||||
case "DATA":
|
||||
if header[1] != "OFXSGML" {
|
||||
return errors.New("OFX DATA header does not contain OFXSGML")
|
||||
}
|
||||
case "VERSION":
|
||||
switch header[1] {
|
||||
case "102", "103", "151", "160":
|
||||
seenVersion = true
|
||||
or.Version = header[1]
|
||||
default:
|
||||
return errors.New("Invalid OFX VERSION in header")
|
||||
}
|
||||
case "SECURITY":
|
||||
if header[1] != "NONE" {
|
||||
return errors.New("OFX SECURITY header not NONE")
|
||||
}
|
||||
case "COMPRESSION":
|
||||
if header[1] != "NONE" {
|
||||
return errors.New("OFX COMPRESSION header not NONE")
|
||||
}
|
||||
case "ENCODING", "CHARSET", "OLDFILEUID", "NEWFILEUID":
|
||||
// TODO check/handle these headers?
|
||||
default:
|
||||
return errors.New("Invalid OFX header: " + header[0])
|
||||
}
|
||||
}
|
||||
|
||||
if !seenVersion {
|
||||
return errors.New("OFX VERSION header missing")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (or *OfxResponse) readXMLHeaders(decoder *xml.Decoder) error {
|
||||
tok, err := decoder.Token()
|
||||
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 = decoder.Token()
|
||||
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":
|
||||
switch value {
|
||||
case "200", "201", "202", "203", "210", "211", "220":
|
||||
seenVersion = true
|
||||
or.Version = value
|
||||
default:
|
||||
return errors.New("Invalid OFX VERSION in 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
|
||||
}
|
||||
|
||||
func (or *OfxResponse) Unmarshal(reader io.Reader, xmlVersion bool) error {
|
||||
r := bufio.NewReader(reader)
|
||||
|
||||
// parse SGML headers before creating XML decoder
|
||||
if !xmlVersion {
|
||||
if err := or.readSGMLHeaders(r); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
decoder := xml.NewDecoder(r)
|
||||
if !xmlVersion {
|
||||
decoder.Strict = false
|
||||
decoder.AutoCloseAfterCharData = ofxLeafElements
|
||||
}
|
||||
|
||||
if xmlVersion {
|
||||
// parse the xml header
|
||||
if err := or.readXMLHeaders(decoder); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
tok, err := decoder.Token()
|
||||
if err != nil {
|
||||
return err
|
||||
} else if ofxStart, ok := tok.(xml.StartElement); !ok || ofxStart.Name.Local != "OFX" {
|
||||
return errors.New("Missing opening OFX xml element")
|
||||
}
|
||||
|
||||
// Unmarshal the signon message
|
||||
tok, err = decoder.Token()
|
||||
if err != nil {
|
||||
return err
|
||||
} else if signonStart, ok := tok.(xml.StartElement); ok && signonStart.Name.Local == "SIGNONMSGSRSV1" {
|
||||
if err := decoder.Decode(&or.Signon); err != nil {
|
||||
return err
|
||||
}
|
||||
} else {
|
||||
return errors.New("Missing opening SIGNONMSGSRSV1 xml element")
|
||||
}
|
||||
|
||||
tok, err = decoder.Token()
|
||||
if err != nil {
|
||||
return err
|
||||
} else if signonEnd, ok := tok.(xml.EndElement); !ok || signonEnd.Name.Local != "SIGNONMSGSRSV1" {
|
||||
return errors.New("Missing closing SIGNONMSGSRSV1 xml element")
|
||||
}
|
||||
if ok, err := or.Signon.Valid(); !ok {
|
||||
return err
|
||||
}
|
||||
|
||||
for {
|
||||
tok, err = decoder.Token()
|
||||
if err != nil {
|
||||
return err
|
||||
} else if ofxEnd, ok := tok.(xml.EndElement); ok && ofxEnd.Name.Local == "OFX" {
|
||||
return nil // found closing XML element, so we're done
|
||||
} else if start, ok := tok.(xml.StartElement); ok {
|
||||
// TODO decode other types
|
||||
fmt.Println("Found starting element for: " + start.Name.Local)
|
||||
} else {
|
||||
return errors.New("Found unexpected token")
|
||||
}
|
||||
|
||||
decoder.Skip()
|
||||
}
|
||||
}
|
24
profile.go
Normal file
24
profile.go
Normal file
@ -0,0 +1,24 @@
|
||||
package ofxgo
|
||||
|
||||
import (
|
||||
"github.com/golang/go/src/encoding/xml"
|
||||
)
|
||||
|
||||
type OfxProfileRequest struct {
|
||||
XMLName xml.Name `xml:"PROFTRNRQ"`
|
||||
TrnUID OfxUID `xml:"TRNUID"`
|
||||
ClientRouting OfxString `xml:"PROFRQ>CLIENTROUTING"` // Forced to NONE
|
||||
DtProfup OfxDate `xml:"PROFRQ>DTPROFUP"`
|
||||
}
|
||||
|
||||
func (r *OfxProfileRequest) Name() string {
|
||||
return "PROFTRNRQ"
|
||||
}
|
||||
|
||||
func (r *OfxProfileRequest) Valid() (bool, error) {
|
||||
if ok, err := r.TrnUID.Valid(); !ok {
|
||||
return false, err
|
||||
}
|
||||
r.ClientRouting = "NONE"
|
||||
return true, nil
|
||||
}
|
102
signon.go
Normal file
102
signon.go
Normal file
@ -0,0 +1,102 @@
|
||||
package ofxgo
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"github.com/golang/go/src/encoding/xml"
|
||||
)
|
||||
|
||||
type OfxSignonRequest struct {
|
||||
XMLName xml.Name `xml:"SONRQ"`
|
||||
Dtclient OfxDate `xml:"DTCLIENT"` // Overridden in OfxRequest.Request()
|
||||
UserId OfxString `xml:"USERID"`
|
||||
UserPass OfxString `xml:"USERPASS,omitempty"`
|
||||
UserKey OfxString `xml:"USERKEY,omitempty"`
|
||||
Language OfxString `xml:"LANGUAGE"` // Defaults to ENG
|
||||
Org OfxString `xml:"FI>ORG"`
|
||||
Fid OfxString `xml:"FI>FID"`
|
||||
AppId OfxString `xml:"APPID"` // Defaults to OFXGO
|
||||
AppVer OfxString `xml:"APPVER"` // Defaults to 0001
|
||||
ClientUID OfxUID `xml:"CLIENTUID,omitempty"`
|
||||
}
|
||||
|
||||
func (r *OfxSignonRequest) Name() string {
|
||||
return "SONRQ"
|
||||
}
|
||||
|
||||
func (r *OfxSignonRequest) Valid() (bool, error) {
|
||||
if len(r.UserId) < 1 || len(r.UserId) > 32 {
|
||||
return false, errors.New("SONRQ>USERID invalid length")
|
||||
}
|
||||
if (len(r.UserPass) == 0) == (len(r.UserKey) == 0) {
|
||||
return false, errors.New("One and only one of SONRQ>USERPASS and USERKEY must be supplied")
|
||||
}
|
||||
if len(r.UserPass) > 32 {
|
||||
return false, errors.New("SONRQ>USERPASS invalid length")
|
||||
}
|
||||
if len(r.UserKey) > 64 {
|
||||
return false, errors.New("SONRQ>USERKEY invalid length")
|
||||
}
|
||||
if len(r.Language) == 0 {
|
||||
r.Language = "ENG"
|
||||
} else if len(r.Language) != 3 {
|
||||
return false, errors.New("SONRQ>LANGUAGE invalid length")
|
||||
}
|
||||
if len(r.AppId) == 0 {
|
||||
r.AppId = "OFXGO"
|
||||
} else if len(r.AppId) > 5 {
|
||||
return false, errors.New("SONRQ>APPID invalid length")
|
||||
}
|
||||
if len(r.AppVer) == 0 {
|
||||
r.AppVer = "0001"
|
||||
} else if len(r.AppVer) > 4 {
|
||||
return false, errors.New("SONRQ>APPVER invalid length")
|
||||
}
|
||||
if ok, err := r.ClientUID.Valid(); !ok {
|
||||
if len(r.ClientUID) > 0 { // ClientUID isn't required
|
||||
return false, err
|
||||
}
|
||||
}
|
||||
return true, nil
|
||||
}
|
||||
|
||||
type OfxStatus struct {
|
||||
XMLName xml.Name `xml:"STATUS"`
|
||||
Code OfxInt `xml:"CODE"`
|
||||
Severity OfxString `xml:"SEVERITY"`
|
||||
Message OfxString `xml:"MESSAGE,omitempty"`
|
||||
}
|
||||
|
||||
func (s *OfxStatus) Valid() (bool, error) {
|
||||
switch s.Severity {
|
||||
case "INFO", "WARN", "ERROR":
|
||||
return true, nil
|
||||
default:
|
||||
return false, errors.New("Invalid STATUS>SEVERITY")
|
||||
}
|
||||
}
|
||||
|
||||
type OfxSignonResponse struct {
|
||||
XMLName xml.Name `xml:"SONRS"`
|
||||
Status OfxStatus `xml:"STATUS"`
|
||||
Dtserver OfxDate `xml:"DTSERVER"`
|
||||
UserKey OfxString `xml:"USERKEY,omitempty"`
|
||||
TsKeyExpire OfxDate `xml:"TSKEYEXPIRE,omitempty"`
|
||||
Language OfxString `xml:"LANGUAGE"`
|
||||
Dtprofup OfxDate `xml:"DTPROFUP,omitempty"`
|
||||
Dtacctup OfxDate `xml:"DTACCTUP,omitempty"`
|
||||
Org OfxString `xml:"FI>ORG"`
|
||||
Fid OfxString `xml:"FI>FID"`
|
||||
SessCookie OfxString `xml:"SESSCOOKIE,omitempty"`
|
||||
AccessKey OfxString `xml:"ACCESSKEY,omitempty"`
|
||||
}
|
||||
|
||||
func (r *OfxSignonResponse) Name() string {
|
||||
return "SONRS"
|
||||
}
|
||||
|
||||
func (r *OfxSignonResponse) Valid() (bool, error) {
|
||||
if len(r.Language) != 3 {
|
||||
return false, errors.New("SONRS>LANGUAGE invalid length: " + string(r.Language))
|
||||
}
|
||||
return r.Status.Valid()
|
||||
}
|
165
types.go
Normal file
165
types.go
Normal file
@ -0,0 +1,165 @@
|
||||
package ofxgo
|
||||
|
||||
import (
|
||||
"crypto/rand"
|
||||
"errors"
|
||||
"fmt"
|
||||
"github.com/golang/go/src/encoding/xml"
|
||||
"regexp"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
type OfxInt int64
|
||||
|
||||
func (oi *OfxInt) UnmarshalXML(d *xml.Decoder, start xml.StartElement) error {
|
||||
var value string
|
||||
err := d.DecodeElement(&value, &start)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
i, err := strconv.ParseInt(strings.TrimSpace(value), 10, 64)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
*oi = (OfxInt)(i)
|
||||
return nil
|
||||
}
|
||||
|
||||
var ofxDateFormats = []string{
|
||||
"20060102150405.000",
|
||||
"20060102150405",
|
||||
"200601021504",
|
||||
"2006010215",
|
||||
"20060102",
|
||||
}
|
||||
var ofxDateZoneFormat = "20060102150405.000 -0700"
|
||||
var ofxDateZoneRegex = regexp.MustCompile(`^\[([+-]?[0-9]+)(\.([0-9]{2}))?(:([A-Z]+))?\]$`)
|
||||
|
||||
type OfxDate time.Time
|
||||
|
||||
func (od *OfxDate) UnmarshalXML(d *xml.Decoder, start xml.StartElement) error {
|
||||
var value string
|
||||
err := d.DecodeElement(&value, &start)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
value = strings.TrimSpace(value)
|
||||
|
||||
if len(value) > len(ofxDateFormats[0]) {
|
||||
matches := ofxDateZoneRegex.FindStringSubmatch(value[len(ofxDateFormats[0]):])
|
||||
if matches == nil {
|
||||
return errors.New("Invalid OFX Date Format")
|
||||
}
|
||||
var err error
|
||||
var zonehours, zoneminutes int
|
||||
zonehours, err = strconv.Atoi(matches[1])
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if len(matches[3]) > 0 {
|
||||
zoneminutes, err = strconv.Atoi(matches[1])
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
zoneminutes = zoneminutes * 60 / 100
|
||||
}
|
||||
value = value[:len(ofxDateFormats[0])] + " " + fmt.Sprintf("%+d%02d", zonehours, zoneminutes)
|
||||
t, err := time.Parse(ofxDateZoneFormat, value)
|
||||
if err == nil {
|
||||
tmpod := OfxDate(t)
|
||||
*od = tmpod
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
for _, format := range ofxDateFormats {
|
||||
t, err := time.Parse(format, value)
|
||||
if err == nil {
|
||||
tmpod := OfxDate(t)
|
||||
*od = tmpod
|
||||
return nil
|
||||
}
|
||||
}
|
||||
return errors.New("OFX: Couldn't parse date:" + value)
|
||||
}
|
||||
|
||||
func (od *OfxDate) String() string {
|
||||
t := time.Time(*od)
|
||||
format := t.Format(ofxDateFormats[0])
|
||||
zonename, zoneoffset := t.Zone()
|
||||
format += "[" + fmt.Sprintf("%+d", zoneoffset/3600)
|
||||
fractionaloffset := (zoneoffset % 3600) / 360
|
||||
if fractionaloffset > 0 {
|
||||
format += "." + fmt.Sprintf("%02d", fractionaloffset)
|
||||
} else if fractionaloffset < 0 {
|
||||
format += "." + fmt.Sprintf("%02d", -fractionaloffset)
|
||||
}
|
||||
return format + ":" + zonename + "]"
|
||||
}
|
||||
|
||||
func (od *OfxDate) MarshalXML(e *xml.Encoder, start xml.StartElement) error {
|
||||
return e.EncodeElement(od.String(), start)
|
||||
}
|
||||
|
||||
type OfxString string
|
||||
|
||||
func (os *OfxString) UnmarshalXML(d *xml.Decoder, start xml.StartElement) error {
|
||||
var value string
|
||||
err := d.DecodeElement(&value, &start)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
*os = OfxString(strings.TrimSpace(value))
|
||||
return nil
|
||||
}
|
||||
|
||||
type OfxBoolean bool
|
||||
|
||||
func (ob *OfxBoolean) UnmarshalXML(d *xml.Decoder, start xml.StartElement) error {
|
||||
var value string
|
||||
err := d.DecodeElement(&value, &start)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
tmpob := strings.TrimSpace(value)
|
||||
switch tmpob {
|
||||
case "Y":
|
||||
*ob = OfxBoolean(true)
|
||||
case "N":
|
||||
*ob = OfxBoolean(false)
|
||||
default:
|
||||
return errors.New("Invalid OFX Boolean")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (ob *OfxBoolean) MarshalXML(e *xml.Encoder, start xml.StartElement) error {
|
||||
if *ob {
|
||||
return e.EncodeElement("Y", start)
|
||||
}
|
||||
return e.EncodeElement("N", start)
|
||||
}
|
||||
|
||||
type OfxUID string
|
||||
|
||||
func (ou *OfxUID) Valid() (bool, error) {
|
||||
if len(*ou) != 36 {
|
||||
return false, errors.New("UID not 36 characters long")
|
||||
}
|
||||
return true, nil
|
||||
}
|
||||
|
||||
func RandomUID() (*OfxUID, error) {
|
||||
uidbytes := make([]byte, 16)
|
||||
n, err := rand.Read(uidbytes[:])
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if n != 16 {
|
||||
return nil, errors.New("RandomUID failed to read 16 random bytes")
|
||||
}
|
||||
uid := OfxUID(fmt.Sprintf("%08x-%04x-%04x-%04x-%012x", uidbytes[:4], uidbytes[4:6], uidbytes[6:8], uidbytes[8:10], uidbytes[10:]))
|
||||
return &uid, nil
|
||||
}
|
Loading…
Reference in New Issue
Block a user