2018-10-03 09:59:04 -04:00
|
|
|
package ofxgo
|
|
|
|
|
|
|
|
import (
|
2024-10-10 21:15:11 -04:00
|
|
|
"crypto/tls"
|
2018-10-03 09:59:04 -04:00
|
|
|
"errors"
|
|
|
|
"io"
|
|
|
|
"net/http"
|
|
|
|
"strings"
|
|
|
|
)
|
|
|
|
|
|
|
|
// VanguardClient provides a Client implementation which handles Vanguard's
|
2024-10-10 21:15:11 -04:00
|
|
|
// cookie-passing requirements and also enables older, disabled-by-default
|
|
|
|
// cipher suites. VanguardClient uses default, non-zero settings, if its fields
|
|
|
|
// are not initialized.
|
2018-10-03 09:59:04 -04:00
|
|
|
type VanguardClient struct {
|
|
|
|
*BasicClient
|
|
|
|
}
|
|
|
|
|
|
|
|
// NewVanguardClient returns a Client interface configured to handle Vanguard's
|
2020-10-06 23:09:45 -04:00
|
|
|
// brand of idiosyncrasy
|
2018-10-03 09:59:04 -04:00
|
|
|
func NewVanguardClient(bc *BasicClient) Client {
|
|
|
|
return &VanguardClient{bc}
|
|
|
|
}
|
|
|
|
|
2024-10-10 21:15:11 -04:00
|
|
|
// vanguardHttpClient returns an http.Client with the default supported
|
|
|
|
// ciphers plus the insecure ciphers Vanguard still uses.
|
|
|
|
func vanguardHttpClient() *http.Client {
|
|
|
|
var clientCiphers []uint16
|
|
|
|
|
|
|
|
vanguardCiphers := []uint16{
|
|
|
|
tls.TLS_RSA_WITH_AES_128_CBC_SHA256,
|
|
|
|
tls.TLS_RSA_WITH_AES_128_GCM_SHA256,
|
|
|
|
tls.TLS_RSA_WITH_AES_256_GCM_SHA384,
|
|
|
|
}
|
|
|
|
defaultCiphers := tls.CipherSuites()
|
|
|
|
for _, cipher := range defaultCiphers {
|
|
|
|
clientCiphers = append(clientCiphers, cipher.ID)
|
|
|
|
}
|
|
|
|
clientCiphers = append(clientCiphers, vanguardCiphers...)
|
|
|
|
|
|
|
|
client := &http.Client{
|
|
|
|
Transport: &http.Transport{
|
|
|
|
TLSClientConfig: &tls.Config{
|
|
|
|
CipherSuites: clientCiphers,
|
|
|
|
},
|
|
|
|
},
|
|
|
|
}
|
|
|
|
return client
|
|
|
|
}
|
|
|
|
|
|
|
|
// RawRequest is a copy of BasicClient RawRequest with a custom http.Client
|
|
|
|
// that enables older cipher suites.
|
|
|
|
func (c *VanguardClient) 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")
|
|
|
|
}
|
|
|
|
|
|
|
|
request, err := http.NewRequest("POST", URL, r)
|
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
request.Header.Set("Content-Type", "application/x-ofx")
|
|
|
|
if c.UserAgent != "" {
|
|
|
|
request.Header.Set("User-Agent", c.UserAgent)
|
|
|
|
}
|
|
|
|
client := vanguardHttpClient()
|
|
|
|
response, err := client.Do(request)
|
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
|
|
|
|
if response.StatusCode != 200 {
|
|
|
|
return response, errors.New("OFXQuery request status: " + response.Status)
|
|
|
|
}
|
|
|
|
|
|
|
|
return response, nil
|
|
|
|
}
|
|
|
|
|
|
|
|
// rawRequestCookiesInsecureCiphers is RawRequest with the added features of
|
|
|
|
// sending cookies and using a custom http.Client that enables older cipher
|
|
|
|
// suites which are disabled by default
|
|
|
|
func rawRequestCookiesInsecureCiphers(URL string, r io.Reader, cookies []*http.Cookie) (*http.Response, error) {
|
2018-10-03 09:59:04 -04:00
|
|
|
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)
|
|
|
|
}
|
|
|
|
|
2024-10-10 21:15:11 -04:00
|
|
|
client := vanguardHttpClient()
|
|
|
|
response, err := client.Do(request)
|
2018-10-03 09:59:04 -04:00
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
|
|
|
|
if response.StatusCode != 200 {
|
|
|
|
return nil, errors.New("OFXQuery request status: " + response.Status)
|
|
|
|
}
|
|
|
|
|
|
|
|
return response, nil
|
|
|
|
}
|
|
|
|
|
2020-10-06 23:08:02 -04:00
|
|
|
// RequestNoParse marshals a Request to XML, makes an HTTP request, and returns
|
|
|
|
// the raw HTTP response
|
2018-10-03 09:59:04 -04:00
|
|
|
func (c *VanguardClient) RequestNoParse(r *Request) (*http.Response, error) {
|
|
|
|
r.SetClientFields(c)
|
|
|
|
|
|
|
|
b, err := r.Marshal()
|
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
|
|
|
|
response, err := c.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.
|
2020-11-17 09:18:40 -05:00
|
|
|
if response != nil && response.ContentLength <= 0 && len(response.Cookies()) > 0 {
|
2018-10-03 09:59:04 -04:00
|
|
|
b, err = r.Marshal()
|
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
|
2024-10-10 21:15:11 -04:00
|
|
|
return rawRequestCookiesInsecureCiphers(r.URL, b, response.Cookies())
|
2018-10-03 09:59:04 -04:00
|
|
|
}
|
|
|
|
|
|
|
|
return response, err
|
|
|
|
}
|
|
|
|
|
2020-10-06 23:08:02 -04:00
|
|
|
// Request marshals a Request to XML, makes an HTTP request, and then
|
|
|
|
// unmarshals the response into a Response object.
|
2018-10-03 09:59:04 -04:00
|
|
|
func (c *VanguardClient) Request(r *Request) (*Response, error) {
|
|
|
|
return clientRequest(c, r)
|
|
|
|
}
|