Skip to content

Commit ca28151

Browse files
authored
Merge pull request #268 from burnettekm/bnk-438-fed-automated-fedach-file-updates
Enable fedach and fedwire download from proxy
2 parents d4d3022 + 32b3082 commit ca28151

File tree

4 files changed

+149
-42
lines changed

4 files changed

+149
-42
lines changed

README.md

+19-10
Original file line numberDiff line numberDiff line change
@@ -70,6 +70,14 @@ FRB_ROUTING_NUMBER=123456780
7070
FRB_DOWNLOAD_CODE=86cfa5a9-1ab9-4af5-bd89-0f84d546de13
7171
```
7272

73+
#### Download files from proxy
74+
75+
Fed can download the files from a proxy or other HTTP resources. The optional URL template is configured as an environment variable. If the URL template is not configured, Fed will download the files directly from FRB eServices by default. This value is considered a template because when preparing the request Fed replaces `%s` in the path with the requested list name(`fedach` or `fedwire`).
76+
77+
```
78+
FRB_DOWNLOAD_URL_TEMPLATE=https://my.example.com/files/%s?format=json
79+
```
80+
7381
### Docker
7482

7583
We publish a [public Docker image `moov/fed`](https://hub.docker.com/r/moov/fed/) from Docker Hub or use this repository. No configuration is required to serve on `:8086` and metrics at `:9096/metrics` in Prometheus format. We also have Docker images for [OpenShift](https://quay.io/repository/moov/fed?tab=tags) published as `quay.io/moov/fed`.
@@ -181,17 +189,18 @@ PONG
181189

182190
### Configuration settings
183191

184-
| Environmental Variable | Description | Default |
185-
|-----|-----|-----|
186-
| `FEDACH_DATA_PATH` | Filepath to FedACH data file | `./data/FedACHdir.txt` |
187-
| `FEDWIRE_DATA_PATH` | Filepath to Fedwire data file | `./data/fpddir.txt` |
188-
| `FRB_ROUTING_NUMBER` | Federal Reserve Board eServices (ABA) routing number used to download FedACH and FedWire files | Empty |
189-
| `FRB_DOWNLOAD_CODE` | Federal Reserve Board eServices (ABA) download code used to download FedACH and FedWire files | Empty |
190-
| `LOG_FORMAT` | Format for logging lines to be written as. | Options: `json`, `plain` - Default: `plain` |
191-
| `HTTP_BIND_ADDRESS` | Address for Fed to bind its HTTP server on. This overrides the command-line flag `-http.addr`. | Default: `:8086` |
192-
| `HTTP_ADMIN_BIND_ADDRESS` | Address for Fed to bind its admin HTTP server on. This overrides the command-line flag `-admin.addr`. | Default: `:9096` |
192+
| Environmental Variable | Description | Default |
193+
|-----|-------------------------------------------------------------------------------------------------------------------------------------|-----|
194+
| `FEDACH_DATA_PATH` | Filepath to FedACH data file | `./data/FedACHdir.txt` |
195+
| `FEDWIRE_DATA_PATH` | Filepath to Fedwire data file | `./data/fpddir.txt` |
196+
| `FRB_ROUTING_NUMBER` | Federal Reserve Board eServices (ABA) routing number used to download FedACH and FedWire files | Empty |
197+
| `FRB_DOWNLOAD_CODE` | Federal Reserve Board eServices (ABA) download code used to download FedACH and FedWire files | Empty |
198+
| `FRB_DOWNLOAD_URL_TEMPLATE` | URL Template for downloading files from alternate source | `https://frbservices.org/EPaymentsDirectory/directories/%s?format=json`|
199+
| `LOG_FORMAT` | Format for logging lines to be written as. | Options: `json`, `plain` - Default: `plain` |
200+
| `HTTP_BIND_ADDRESS` | Address for Fed to bind its HTTP server on. This overrides the command-line flag `-http.addr`. | Default: `:8086` |
201+
| `HTTP_ADMIN_BIND_ADDRESS` | Address for Fed to bind its admin HTTP server on. This overrides the command-line flag `-admin.addr`. | Default: `:9096` |
193202
| `HTTPS_CERT_FILE` | Filepath containing a certificate (or intermediate chain) to be served by the HTTP server. Requires all traffic be over secure HTTP. | Empty |
194-
| `HTTPS_KEY_FILE` | Filepath of a private key matching the leaf certificate from `HTTPS_CERT_FILE`. | Empty |
203+
| `HTTPS_KEY_FILE` | Filepath of a private key matching the leaf certificate from `HTTPS_CERT_FILE`. | Empty |
195204

196205
#### Logos
197206

cmd/server/reader.go

+28-24
Original file line numberDiff line numberDiff line change
@@ -5,43 +5,49 @@
55
package main
66

77
import (
8+
"errors"
89
"fmt"
9-
"io"
10-
"os"
11-
1210
"github.com/moov-io/base/log"
1311
"github.com/moov-io/fed"
1412
"github.com/moov-io/fed/pkg/download"
13+
"io"
14+
"os"
1515
)
1616

1717
func fedACHDataFile(logger log.Logger) (io.Reader, error) {
18-
if file, err := attemptFileDownload(logger, "fedach"); file != nil {
19-
return file, nil
20-
} else if err != nil {
18+
file, err := attemptFileDownload(logger, "fedach")
19+
if err != nil && !errors.Is(err, download.ErrMissingConfigValue) {
2120
return nil, fmt.Errorf("problem downloading fedach: %v", err)
2221
}
2322

23+
if file != nil {
24+
return file, nil
25+
}
26+
2427
path := readDataFilepath("FEDACH_DATA_PATH", "./data/FedACHdir.txt")
2528
logger.Logf("search: loading %s for ACH data", path)
2629

27-
file, err := os.Open(path)
30+
file, err = os.Open(path)
2831
if err != nil {
2932
return nil, fmt.Errorf("problem opening %s: %v", path, err)
3033
}
3134
return file, nil
3235
}
3336

3437
func fedWireDataFile(logger log.Logger) (io.Reader, error) {
35-
if file, err := attemptFileDownload(logger, "fedwire"); file != nil {
38+
file, err := attemptFileDownload(logger, "fedach")
39+
if err != nil && !errors.Is(err, download.ErrMissingConfigValue) {
40+
return nil, fmt.Errorf("problem downloading fedach: %v", err)
41+
}
42+
43+
if file != nil {
3644
return file, nil
37-
} else if err != nil {
38-
return nil, fmt.Errorf("problem downloading fedwire: %v", err)
3945
}
4046

4147
path := readDataFilepath("FEDWIRE_DATA_PATH", "./data/fpddir.txt")
4248
logger.Logf("search: loading %s for Wire data", path)
4349

44-
file, err := os.Open(path)
50+
file, err = os.Open(path)
4551
if err != nil {
4652
return nil, fmt.Errorf("problem opening %s: %v", path, err)
4753
}
@@ -51,20 +57,18 @@ func fedWireDataFile(logger log.Logger) (io.Reader, error) {
5157
func attemptFileDownload(logger log.Logger, listName string) (io.Reader, error) {
5258
routingNumber := os.Getenv("FRB_ROUTING_NUMBER")
5359
downloadCode := os.Getenv("FRB_DOWNLOAD_CODE")
54-
55-
if routingNumber != "" && downloadCode != "" {
56-
logger.Logf("download: attempting %s", listName)
57-
client, err := download.NewClient(&download.ClientOpts{
58-
RoutingNumber: routingNumber,
59-
DownloadCode: downloadCode,
60-
})
61-
if err != nil {
62-
return nil, fmt.Errorf("client setup: %v", err)
63-
}
64-
return client.GetList(listName)
60+
downloadURL := os.Getenv("FRB_DOWNLOAD_URL_TEMPLATE")
61+
62+
logger.Logf("download: attempting %s", listName)
63+
client, err := download.NewClient(&download.ClientOpts{
64+
RoutingNumber: routingNumber,
65+
DownloadCode: downloadCode,
66+
DownloadURL: downloadURL,
67+
})
68+
if err != nil {
69+
return nil, fmt.Errorf("client setup: %w", err)
6570
}
66-
67-
return nil, nil
71+
return client.GetList(listName)
6872
}
6973

7074
func readDataFilepath(env, fallback string) string {

pkg/download/download.go

+27-8
Original file line numberDiff line numberDiff line change
@@ -14,16 +14,26 @@ import (
1414
"time"
1515
)
1616

17+
const DefaultFRBDownloadURLTemplate = "https://frbservices.org/EPaymentsDirectory/directories/%s?format=json"
18+
19+
var (
20+
ErrMissingConfigValue = errors.New("missing config value")
21+
ErrMissingRoutingNumber = errors.New("missing routing number")
22+
ErrMissingDownloadCD = errors.New("missing download code")
23+
)
24+
1725
type Client struct {
1826
httpClient *http.Client
1927

2028
routingNumber string // X_FRB_EPAYMENTS_DIRECTORY_ORG_ID header
2129
downloadCode string // X_FRB_EPAYMENTS_DIRECTORY_DOWNLOAD_CD
30+
downloadURL string // defaults to "https://frbservices.org/EPaymentsDirectory/directories/%s?format=json" where %s is the list name
31+
2232
}
2333

2434
type ClientOpts struct {
25-
HTTPClient *http.Client
26-
RoutingNumber, DownloadCode string
35+
HTTPClient *http.Client
36+
RoutingNumber, DownloadCode, DownloadURL string
2737
}
2838

2939
func NewClient(opts *ClientOpts) (*Client, error) {
@@ -39,23 +49,29 @@ func NewClient(opts *ClientOpts) (*Client, error) {
3949
}
4050

4151
if opts.RoutingNumber == "" {
42-
return nil, errors.New("missing routing number")
52+
return nil, fmt.Errorf("%w: %w", ErrMissingConfigValue, ErrMissingRoutingNumber)
53+
}
54+
55+
if opts.RoutingNumber == "" {
56+
return nil, fmt.Errorf("%w: %w", ErrMissingConfigValue, ErrMissingDownloadCD)
4357
}
44-
if opts.DownloadCode == "" {
45-
return nil, errors.New("missing download code")
58+
59+
if opts.DownloadURL == "" {
60+
opts.DownloadURL = DefaultFRBDownloadURLTemplate
4661
}
4762

4863
return &Client{
4964
httpClient: opts.HTTPClient,
5065
routingNumber: opts.RoutingNumber,
5166
downloadCode: opts.DownloadCode,
67+
downloadURL: opts.DownloadURL,
5268
}, nil
5369
}
5470

5571
// GetList downloads an FRB list and saves it into an io.Reader.
5672
// Example listName values: fedach, fedwire
5773
func (c *Client) GetList(listName string) (io.Reader, error) {
58-
where, err := url.Parse(fmt.Sprintf("https://frbservices.org/EPaymentsDirectory/directories/%s?format=json", listName))
74+
where, err := url.Parse(fmt.Sprintf(c.downloadURL, listName))
5975
if err != nil {
6076
return nil, fmt.Errorf("url: %v", err)
6177
}
@@ -64,8 +80,11 @@ func (c *Client) GetList(listName string) (io.Reader, error) {
6480
if err != nil {
6581
return nil, fmt.Errorf("building %s url: %v", listName, err)
6682
}
67-
req.Header.Set("X_FRB_EPAYMENTS_DIRECTORY_ORG_ID", c.routingNumber)
68-
req.Header.Set("X_FRB_EPAYMENTS_DIRECTORY_DOWNLOAD_CD", c.downloadCode)
83+
84+
if c.downloadCode != "" && c.routingNumber != "" {
85+
req.Header.Set("X_FRB_EPAYMENTS_DIRECTORY_ORG_ID", c.routingNumber)
86+
req.Header.Set("X_FRB_EPAYMENTS_DIRECTORY_DOWNLOAD_CD", c.downloadCode)
87+
}
6988

7089
// perform our request
7190
resp, err := c.httpClient.Do(req)

pkg/download/download_test.go

+75
Original file line numberDiff line numberDiff line change
@@ -6,8 +6,12 @@ package download
66

77
import (
88
"bytes"
9+
"fmt"
910
"io"
11+
"net/http"
12+
"net/http/httptest"
1013
"os"
14+
"path/filepath"
1115
"testing"
1216

1317
"github.com/stretchr/testify/require"
@@ -34,6 +38,41 @@ func TestClient__fedach(t *testing.T) {
3438
}
3539
}
3640

41+
func TestClient__fedach_custom_url(t *testing.T) {
42+
file, err := os.ReadFile(filepath.Join("..", "..", "data", "fedachdir.json"))
43+
if err != nil {
44+
t.Fatal(err)
45+
}
46+
47+
mockHTTPServer := httptest.NewServer(http.HandlerFunc(func(writer http.ResponseWriter, request *http.Request) {
48+
fmt.Fprint(writer, string(file))
49+
}))
50+
defer mockHTTPServer.Close()
51+
52+
t.Setenv("FRB_DOWNLOAD_URL_TEMPLATE", mockHTTPServer.URL+"/%s")
53+
t.Setenv("FRB_ROUTING_NUMBER", "123456789")
54+
t.Setenv("FRB_DOWNLOAD_CODE", "a1b2c3d4-123b-9876-1234-z1x2y3a1b2c3")
55+
56+
client := setupClient(t)
57+
58+
fedach, err := client.GetList("fedach")
59+
if err != nil {
60+
t.Fatal(err)
61+
}
62+
63+
buf, ok := fedach.(*bytes.Buffer)
64+
require.True(t, ok)
65+
66+
if n := buf.Len(); n < 1024 {
67+
t.Errorf("unexpected size of %d bytes", n)
68+
}
69+
70+
bs, _ := io.ReadAll(io.LimitReader(fedach, 10024))
71+
if !bytes.Equal(bs, file) {
72+
t.Errorf("unexpected output:\n%s", string(bs))
73+
}
74+
}
75+
3776
func TestClient__fedwire(t *testing.T) {
3877
client := setupClient(t)
3978

@@ -55,18 +94,54 @@ func TestClient__fedwire(t *testing.T) {
5594
}
5695
}
5796

97+
func TestClient__wire_custom_url(t *testing.T) {
98+
file, err := os.ReadFile(filepath.Join("..", "..", "data", "fedachdir.json"))
99+
if err != nil {
100+
t.Fatal(err)
101+
}
102+
mockHTTPServer := httptest.NewServer(http.HandlerFunc(func(writer http.ResponseWriter, request *http.Request) {
103+
fmt.Fprint(writer, string(file))
104+
}))
105+
defer mockHTTPServer.Close()
106+
107+
t.Setenv("FRB_DOWNLOAD_URL_TEMPLATE", mockHTTPServer.URL+"/%s")
108+
t.Setenv("FRB_ROUTING_NUMBER", "123456789")
109+
t.Setenv("FRB_DOWNLOAD_CODE", "a1b2c3d4-123b-9876-1234-z1x2y3a1b2c3")
110+
111+
client := setupClient(t)
112+
113+
fedach, err := client.GetList("fedwire")
114+
if err != nil {
115+
t.Fatal(err)
116+
}
117+
118+
buf, ok := fedach.(*bytes.Buffer)
119+
require.True(t, ok)
120+
121+
if n := buf.Len(); n < 1024 {
122+
t.Errorf("unexpected size of %d bytes", n)
123+
}
124+
125+
bs, _ := io.ReadAll(io.LimitReader(fedach, 10024))
126+
if !bytes.Equal(bs, file) {
127+
t.Errorf("unexpected output:\n%s", string(bs))
128+
}
129+
}
130+
58131
func setupClient(t *testing.T) *Client {
59132
t.Helper()
60133

61134
routingNumber := os.Getenv("FRB_ROUTING_NUMBER")
62135
downloadCode := os.Getenv("FRB_DOWNLOAD_CODE")
136+
downloadURL := os.Getenv("FRB_DOWNLOAD_URL_TEMPLATE")
63137
if routingNumber == "" || downloadCode == "" {
64138
t.Skip("missing FRB routing number or download code")
65139
}
66140

67141
client, err := NewClient(&ClientOpts{
68142
RoutingNumber: routingNumber,
69143
DownloadCode: downloadCode,
144+
DownloadURL: downloadURL,
70145
})
71146
if err != nil {
72147
t.Fatal(err)

0 commit comments

Comments
 (0)