package ofxgo import ( "errors" "io" "net/http" "strings" ) // Client serves to aggregate OFX client settings that may be necessary to talk // to a particular server due to quirks in that server's implementation. // Client also provides the Request and RequestNoParse helper methods to aid in // making and parsing requests. type Client interface { // Used to fill out a Request object OfxVersion() ofxVersion ID() String Version() String IndentRequests() bool // Used to initiate requests to servers Request(r *Request) (*Response, error) RequestNoParse(r *Request) (*http.Response, error) } // BasicClient provides a standard Client implementation suitable for most // financial institutions. BasicClient uses default, non-zero settings, even if // its fields are not initialized. type BasicClient struct { // Request fields to overwrite with the client's values. If nonempty, // defaults are used SpecVersion ofxVersion // VERSION in header AppID string // SONRQ>APPID AppVer string // SONRQ>APPVER // Don't insert newlines or indentation when marshalling to SGML/XML NoIndent bool } // OfxVersion returns the OFX specification version this BasicClient will marshal // Requests as. Defaults to "203" if the client's SpecVersion field is empty. func (c *BasicClient) OfxVersion() ofxVersion { if c.SpecVersion.Valid() { return c.SpecVersion } return OfxVersion203 } // ID returns this BasicClient's OFX AppID field, defaulting to "OFXGO" if // unspecified. func (c *BasicClient) ID() String { if len(c.AppID) > 0 { return String(c.AppID) } return String("OFXGO") } // Version returns this BasicClient's version number as a string, defaulting to // "0001" if unspecified. func (c *BasicClient) Version() String { if len(c.AppVer) > 0 { return String(c.AppVer) } return String("0001") } // IndentRequests returns true if the marshaled XML should be indented (and // contain newlines, since the two are linked in the current implementation) func (c *BasicClient) IndentRequests() bool { return !c.NoIndent } // RawRequest is little more than a thin wrapper around http.Post // // In most cases, you should probably be using Request() instead, but // RawRequest can be useful if you need to read the raw unparsed http response // yourself (perhaps for downloading an OFX file for use by an external // program, or debugging server behavior), or have a handcrafted request you'd // like to try. // // Caveats: RawRequest does *not* take client settings into account as // Client.Request() does, so your particular server may or may not like // whatever we read from 'r'. The caller is responsible for closing the http // Response.Body (see the http module's documentation for more information) func RawRequest(URL string, r io.Reader) (*http.Response, error) { if !strings.HasPrefix(URL, "https://") { return nil, errors.New("Refusing to send OFX request with possible plain-text password over non-https protocol") } response, err := http.Post(URL, "application/x-ofx", r) if err != nil { return nil, err } if response.StatusCode != 200 { return nil, errors.New("OFXQuery request status: " + response.Status) } return response, nil } // rawRequestCookies is RawRequest with the added feature of sending cookies func rawRequestCookies(URL string, r io.Reader, cookies []*http.Cookie) (*http.Response, error) { if !strings.HasPrefix(URL, "https://") { return nil, errors.New("Refusing to send OFX request with possible plain-text password over non-https protocol") } request, err := http.NewRequest("POST", URL, r) if err != nil { return nil, err } request.Header.Set("Content-Type", "application/x-ofx") for _, cookie := range cookies { request.AddCookie(cookie) } response, err := http.DefaultClient.Do(request) if err != nil { return nil, err } if response.StatusCode != 200 { return nil, errors.New("OFXQuery request status: " + response.Status) } return response, nil } // RequestNoParse marshals a Request object into XML, makes an HTTP request, // and returns the raw HTTP response. Unlike RawRequest(), it takes client // settings into account. Unlike Request(), it doesn't parse the response into // an ofxgo.Request object. // // Caveat: The caller is responsible for closing the http Response.Body (see // the http module's documentation for more information) func (c *BasicClient) RequestNoParse(r *Request) (*http.Response, error) { r.SetClientFields(c) b, err := r.Marshal() if err != nil { return nil, err } response, err := RawRequest(r.URL, b) // Some financial institutions (cough, Vanguard, cough), require a cookie // to be set on the http request, or they return empty responses. // Fortunately, the initial response contains the cookie we need, so if we // detect an empty response with cookies set that didn't have any errors, // re-try the request while sending their cookies back to them. if err == nil && response.ContentLength <= 0 && len(response.Cookies()) > 0 { b, err = r.Marshal() if err != nil { return nil, err } return rawRequestCookies(r.URL, b, response.Cookies()) } return response, err } // Request marshals a Request object into XML, makes an HTTP request against // it's URL, and then unmarshals the response into a Response object. // // Before being marshaled, some of the the Request object's values are // overwritten, namely those dictated by the BasicClient's configuration (Version, // AppID, AppVer fields), and the client's current time (DtClient). These are // updated in place in the supplied Request object so they may later be // inspected by the caller. func (c *BasicClient) Request(r *Request) (*Response, error) { response, err := c.RequestNoParse(r) if err != nil { return nil, err } defer response.Body.Close() ofxresp, err := ParseResponse(response.Body) if err != nil { return nil, err } return ofxresp, nil }