Skip to content

Commit

Permalink
Merge pull request #279 from ahrtmn/ach-driver-put-file-check
Browse files Browse the repository at this point in the history
filedrive: add ach FTP driver with PutFile checks
  • Loading branch information
adamdecaf authored Jan 17, 2025
2 parents a4bbd2a + 0d1b2b0 commit 39f4a3f
Show file tree
Hide file tree
Showing 4 changed files with 132 additions and 3 deletions.
66 changes: 66 additions & 0 deletions pkg/filedrive/ach_driver.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
package filedrive

import (
"bytes"
"context"
"fmt"
"io"

"github.com/moov-io/ach"
"github.com/moov-io/base/log"
"github.com/moov-io/base/telemetry"
"go.opentelemetry.io/otel/attribute"
"go.opentelemetry.io/otel/trace"
"goftp.io/server/core"
)

// ACHDriver wraps the goftp driver to add additional logic and error checking.
type ACHDriver struct {
core.Driver

logger log.Logger
validateOpts *ach.ValidateOpts
}

func NewACHDriver(logger log.Logger, validateOpts *ach.ValidateOpts, driver core.Driver) *ACHDriver {
return &ACHDriver{
Driver: driver,
logger: logger,
validateOpts: validateOpts,
}
}

// PutFile overrides the existing method to prevent erroneous ACH files from being uploaded.
func (d *ACHDriver) PutFile(path string, r io.Reader, appendData bool) (int64, error) {
_, span := telemetry.StartSpan(context.Background(), "put-file", trace.WithAttributes(
attribute.String("ftp.destination", path),
))
defer span.End()

d.logger.Info().Log(fmt.Sprintf("receiving file for %s", path))

// Read the file that was uploaded
var buf bytes.Buffer
tee := io.TeeReader(r, &buf)

reader := ach.NewReader(tee)
reader.SetValidation(d.validateOpts)

file, err := reader.Read()
if err != nil {
span.RecordError(err)
d.logger.Error().Log(fmt.Sprintf("ftp: error reading ACH file %s: %v", path, err))
return 0, err
}

if err := file.Create(); err != nil {
d.logger.Error().Log(fmt.Sprintf("ftp: error creating file %s: %v", path, err))
return 0, err
}

span.SetAttributes(attribute.Int("ftp.file_size_bytes", buf.Len()))
d.logger.Info().Log(fmt.Sprintf("accepting file at %s", path))

// Call the original PutFile method with a reset reader.
return d.Driver.PutFile(path, &buf, appendData)
}
53 changes: 53 additions & 0 deletions pkg/filedrive/ach_driver_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
package filedrive

import (
"bytes"
"io"
"os"
"path/filepath"
"testing"

"github.com/moov-io/base/log"
"github.com/stretchr/testify/require"
"goftp.io/server/core"
)

// MockDriver is a simple mock implementation of core.Driver for testing purposes.
type MockDriver struct {
core.Driver
}

func (m *MockDriver) PutFile(path string, r io.Reader, appendData bool) (int64, error) {
// Mock implementation, just return success
return 0, nil
}

func TestACHDriver_PutFile_InvalidACH(t *testing.T) {
mockDriver := &MockDriver{}
customDriver := NewACHDriver(log.NewDefaultLogger(), nil, mockDriver)

// Create an invalid ACH file (e.g., missing required fields)
var invalidACH bytes.Buffer
invalidACH.WriteString("invalid ACH content")

// Attempt to upload the invalid ACH file
_, err := customDriver.PutFile("invalid.ach", &invalidACH, false)

// Verify that an error is returned
require.Error(t, err)
}

func TestACHDriver_PutFile(t *testing.T) {
mockDriver := &MockDriver{}
customDriver := NewACHDriver(log.NewDefaultLogger(), nil, mockDriver)

achFile, err := os.Open(filepath.Join("..", "..", "testdata", "20230809-144155-102000021C.ach"))
require.NoError(t, err)
defer achFile.Close()

// Attempt to upload the valid ACH file
_, err = customDriver.PutFile("valid.ach", achFile, false)

// Verify that no error is returned
require.NoError(t, err)
}
9 changes: 8 additions & 1 deletion pkg/filedrive/mtime_filter.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@ package filedrive
import (
"time"

"github.com/moov-io/ach"
"github.com/moov-io/base/log"
"goftp.io/server/core"
)

Expand All @@ -23,14 +25,19 @@ func (mtf MTimeFilter) ListDir(path string, callback func(core.FileInfo) error)

type Factory struct {
DriverFactory core.DriverFactory

Logger log.Logger
ValidateOpts *ach.ValidateOpts
}

func (f *Factory) NewDriver() (core.Driver, error) {
dd, err := f.DriverFactory.NewDriver()
if err != nil {
return nil, err
}

achDriver := NewACHDriver(f.Logger, f.ValidateOpts, dd)
return MTimeFilter{
Driver: dd,
Driver: achDriver,
}, nil
}
7 changes: 5 additions & 2 deletions pkg/service/server.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import (
"os"
"path/filepath"

"github.com/moov-io/ach"
_ "github.com/moov-io/ach-test-harness"
"github.com/moov-io/ach-test-harness/pkg/filedrive"
"github.com/moov-io/base/admin"
Expand All @@ -27,7 +28,7 @@ func (env *Environment) RunServers(terminationListener chan error) func() {

var shutdownFTPServer func()
if env.Config.Servers.FTP != nil {
ftpServer, shutdown := bootFTPServer(terminationListener, env.Logger, env.Config.Servers.FTP, env.Config.responsePaths())
ftpServer, shutdown := bootFTPServer(terminationListener, env.Logger, env.Config.Servers.FTP, env.Config.ValidateOpts, env.Config.responsePaths())
env.FTPServer = ftpServer
shutdownFTPServer = shutdown
}
Expand All @@ -38,7 +39,7 @@ func (env *Environment) RunServers(terminationListener chan error) func() {
}
}

func bootFTPServer(errs chan<- error, logger log.Logger, cfg *FTPConfig, responsePaths []string) (*ftp.Server, func()) {
func bootFTPServer(errs chan<- error, logger log.Logger, cfg *FTPConfig, validateOpts *ach.ValidateOpts, responsePaths []string) (*ftp.Server, func()) {
// Setup data directory
createDataDirectories(errs, logger, cfg)

Expand All @@ -49,6 +50,8 @@ func bootFTPServer(errs chan<- error, logger log.Logger, cfg *FTPConfig, respons
}
filteringDriver := &filedrive.Factory{
DriverFactory: fileDriverFactory,
Logger: logger,
ValidateOpts: validateOpts,
}
opts := &ftp.ServerOpts{
Factory: filteringDriver,
Expand Down

0 comments on commit 39f4a3f

Please sign in to comment.