Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

filedrive: add ach FTP driver with PutFile checks #279

Merged
merged 1 commit into from
Jan 17, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
Loading