|
1 | 1 | # CLAUDE.md |
2 | 2 |
|
3 | | -This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository. |
| 3 | +This file provides guidance for AI assistants working with the ftpserverlib codebase. |
4 | 4 |
|
5 | 5 | ## Project Overview |
6 | 6 |
|
7 | | -This is a **Go FTP server library** (`ftpserver` package) that provides a complete RFC 959-compliant FTP server implementation with TLS, IPv6, and extended command support. The library uses a **driver-based architecture** where users implement interfaces to customize file system operations and authentication. |
| 7 | +**ftpserverlib** is a Go library for building FTP servers using [afero](https://github.com/spf13/afero) as the backend filesystem. It implements RFC 959 and numerous extensions, providing a clean, driver-based architecture for customization. |
8 | 8 |
|
9 | | -## Architecture |
| 9 | +**Repository**: `github.com/fclairamb/ftpserverlib` |
10 | 10 |
|
11 | | -### Core Driver Pattern |
12 | | -The library centers around three main interfaces: |
13 | | -- **MainDriver**: Authentication, client lifecycle, TLS configuration |
14 | | -- **ClientDriver**: File system operations (based on `afero.Fs`) |
15 | | -- **ClientContext**: Client connection metadata and context |
| 11 | +## Build and Test Commands |
16 | 12 |
|
17 | | -### Key Components |
18 | | -- **Server Core** (`server.go`): Main server with command mapping system |
19 | | -- **Client Handler** (`client_handler.go`): Per-client connection management and protocol state machine |
20 | | -- **Command Handlers**: Organized by functionality (`handle_*.go` files) |
21 | | -- **Transfer System**: Separate active (`transfer_active.go`) and passive (`transfer_pasv.go`) mode implementations |
22 | | - |
23 | | -## Common Commands |
24 | | - |
25 | | -### Development |
26 | 13 | ```bash |
27 | | -# Build the library |
| 14 | +# Build |
28 | 15 | go build -v ./... |
29 | 16 |
|
30 | | -# Run full test suite with race detection and coverage |
| 17 | +# Run tests (standard) |
| 18 | +go test -v ./... |
| 19 | + |
| 20 | +# Run tests with race detection (as CI does) |
31 | 21 | go test -parallel 20 -v -race -coverprofile=coverage.txt -covermode=atomic ./... |
32 | 22 |
|
33 | | -# Lint code |
| 23 | +# Run linter (requires golangci-lint v2.4.0+) |
34 | 24 | golangci-lint run |
| 25 | + |
| 26 | +# Format code |
| 27 | +gofmt -w . |
| 28 | +goimports -w -local github.com/fclairamb/ftpserverlib . |
| 29 | +``` |
| 30 | + |
| 31 | +## Project Architecture |
| 32 | + |
| 33 | +### Single Package Design |
| 34 | + |
| 35 | +The entire library is a single `ftpserver` package with files organized by responsibility: |
| 36 | + |
| 37 | +| File(s) | Purpose | |
| 38 | +|---------|---------| |
| 39 | +| `server.go` | Main `FtpServer` struct, initialization, listener management | |
| 40 | +| `client_handler.go` | Per-client connection state machine and command parsing | |
| 41 | +| `handle_auth.go` | USER, PASS, AUTH, PROT, PBSZ commands | |
| 42 | +| `handle_dirs.go` | CWD, CDUP, MKD, RMD, PWD commands | |
| 43 | +| `handle_files.go` | STOR, RETR, LIST, NLST, MLST, MLSD, DELE, SIZE, etc. | |
| 44 | +| `handle_misc.go` | SYST, FEAT, NOOP, QUIT, SITE, STAT, HELP commands | |
| 45 | +| `transfer_pasv.go` | Passive mode data connections (PASV, EPSV) | |
| 46 | +| `transfer_active.go` | Active mode data connections (PORT, EPRT) | |
| 47 | +| `driver.go` | All interface definitions (MainDriver, ClientDriver, extensions) | |
| 48 | +| `consts.go` | FTP status codes and constants | |
| 49 | +| `errors.go` | Custom error types (DriverError, NetworkError, FileAccessError) | |
| 50 | +| `asciiconverter.go` | ASCII mode CRLF/LF conversion | |
| 51 | + |
| 52 | +### Driver-Based Architecture |
| 53 | + |
| 54 | +Users implement interfaces to customize server behavior: |
| 55 | + |
| 56 | +```go |
| 57 | +// Required: Main authentication and configuration |
| 58 | +type MainDriver interface { |
| 59 | + GetSettings() (*Settings, error) |
| 60 | + ClientConnected(cc ClientContext) (string, error) |
| 61 | + ClientDisconnected(cc ClientContext) |
| 62 | + AuthUser(cc ClientContext, user, pass string) (ClientDriver, error) |
| 63 | + GetTLSConfig() (*tls.Config, error) |
| 64 | +} |
| 65 | + |
| 66 | +// Required: Filesystem operations (wraps afero.Fs) |
| 67 | +type ClientDriver interface { |
| 68 | + afero.Fs |
| 69 | +} |
| 70 | +``` |
| 71 | + |
| 72 | +### Extension Pattern |
| 73 | + |
| 74 | +Optional features use interface assertion: |
| 75 | + |
| 76 | +```go |
| 77 | +// In handler code: |
| 78 | +if hasher, ok := c.driver.(ClientDriverExtensionHasher); ok { |
| 79 | + // Extension is supported, use it |
| 80 | + hash, err := hasher.ComputeHash(name, algo, start, end) |
| 81 | +} |
| 82 | +``` |
| 83 | + |
| 84 | +Available extensions: |
| 85 | +- `MainDriverExtensionTLSVerifier` - TLS certificate authentication |
| 86 | +- `MainDriverExtensionUserVerifier` - Pre-auth user validation |
| 87 | +- `MainDriverExtensionPostAuthMessage` - Custom post-auth messages |
| 88 | +- `MainDriverExtensionPassiveWrapper` - Wrap passive listeners |
| 89 | +- `MainDriverExtensionQuitMessage` - Custom quit messages |
| 90 | +- `ClientDriverExtensionAllocate` - ALLO command support |
| 91 | +- `ClientDriverExtensionSymlink` - SITE SYMLINK support |
| 92 | +- `ClientDriverExtensionFileList` - Custom directory listing |
| 93 | +- `ClientDriverExtentionFileTransfer` - Custom file transfer handles |
| 94 | +- `ClientDriverExtensionRemoveDir` - Distinguish RMD from DELE |
| 95 | +- `ClientDriverExtensionHasher` - Custom hash implementations |
| 96 | +- `ClientDriverExtensionAvailableSpace` - AVBL command support |
| 97 | +- `ClientDriverExtensionSite` - Custom SITE subcommands |
| 98 | + |
| 99 | +## Coding Conventions |
| 100 | + |
| 101 | +### Naming |
| 102 | +- Exported: `PascalCase` (e.g., `FtpServer`, `MainDriver`, `Settings`) |
| 103 | +- Unexported: `camelCase` (e.g., `clientHandler`, `transferHandler`) |
| 104 | +- Command handlers: `handle{COMMAND}` (e.g., `handleUSER`, `handleRETR`) |
| 105 | +- Test files: `*_test.go` colocated with implementation |
| 106 | + |
| 107 | +### Enums |
| 108 | +Use `type X int8` with `iota`: |
| 109 | +```go |
| 110 | +type HASHAlgo int8 |
| 111 | +const ( |
| 112 | + HASHAlgoCRC32 HASHAlgo = iota |
| 113 | + HASHAlgoMD5 |
| 114 | + HASHAlgoSHA1 |
| 115 | + HASHAlgoSHA256 |
| 116 | + HASHAlgoSHA512 |
| 117 | +) |
35 | 118 | ``` |
36 | 119 |
|
37 | | -### Testing Specific Components |
| 120 | +### Error Handling |
| 121 | +- Use custom error types: `DriverError`, `NetworkError`, `FileAccessError` |
| 122 | +- Wrap errors with context: `fmt.Errorf("operation failed: %w", err)` |
| 123 | +- Check errors with `errors.Is()`: `errors.Is(err, ErrStorageExceeded)` |
| 124 | +- Use `getErrorCode()` to map Go errors to FTP status codes |
| 125 | + |
| 126 | +### Synchronization |
| 127 | +- No global mutexes (only per-client) |
| 128 | +- `paramsMutex` (RWMutex) protects public API fields in `clientHandler` |
| 129 | +- `transferMu` protects transfer connection state |
| 130 | +- `sync.WaitGroup` for command-to-transfer coordination |
| 131 | + |
| 132 | +### Design Principles |
| 133 | +- **No sleep**: Use proper synchronization, not time delays |
| 134 | +- **No panic**: Propagate errors, don't crash |
| 135 | +- **No global sync**: Each client manages its own state |
| 136 | + |
| 137 | +## Linter Configuration |
| 138 | + |
| 139 | +The project uses golangci-lint v2 with strict settings (`.golangci.yml`): |
| 140 | + |
| 141 | +- **Line length**: 120 characters max |
| 142 | +- **Function length**: 80 lines / 40 statements max |
| 143 | +- **Cyclomatic complexity**: 15 max |
| 144 | +- **Cognitive complexity**: 30 max |
| 145 | +- **Import organization**: stdlib, third-party, then local (`github.com/fclairamb/ftpserverlib`) |
| 146 | + |
| 147 | +Key enabled linters: `gosec`, `errcheck`, `errorlint`, `gocyclo`, `gocognit`, `funlen`, `dupl`, `unparam`, `staticcheck` |
| 148 | + |
| 149 | +## Testing |
| 150 | + |
| 151 | +### Test Infrastructure |
| 152 | +- Tests use `github.com/stretchr/testify` (both `assert` and `require`) |
| 153 | +- Reference driver implementation in `driver_test.go` (`TestServerDriver`) |
| 154 | +- Setup helpers: `NewTestServer()`, `NewTestServerWithTestDriver()`, `NewTestServerWithDriver()` |
| 155 | +- FTP client for integration tests: `github.com/secsy/goftp` (replaced with fork) |
| 156 | + |
| 157 | +### Running Tests |
38 | 158 | ```bash |
39 | | -# Test individual handlers |
40 | | -go test -v -run TestHandle |
| 159 | +# Standard test run |
| 160 | +go test -v ./... |
41 | 161 |
|
42 | | -# Test transfers specifically |
43 | | -go test -v -run TestTransfer |
| 162 | +# With race detection (recommended) |
| 163 | +go test -race ./... |
44 | 164 |
|
45 | | -# Run benchmarks |
46 | | -go test -bench=. |
| 165 | +# Specific test |
| 166 | +go test -v -run TestNamePattern ./... |
47 | 167 | ``` |
48 | 168 |
|
49 | | -## File Organization |
| 169 | +### Test Patterns |
| 170 | +- Table-driven tests for multiple scenarios |
| 171 | +- Real filesystem via `afero.NewBasePathFs` with temp directories |
| 172 | +- Integration tests using actual FTP protocol |
| 173 | +- Concurrent client testing (100+ simultaneous connections) |
| 174 | + |
| 175 | +## Dependencies |
| 176 | + |
| 177 | +**Go Version**: 1.24.0 minimum, toolchain 1.25.5 |
| 178 | + |
| 179 | +**Direct Dependencies**: |
| 180 | +- `github.com/spf13/afero` - Filesystem abstraction |
| 181 | +- `log/slog` - Go standard library structured logging (no external dependencies) |
| 182 | +- `golang.org/x/sys` - Platform syscalls |
| 183 | + |
| 184 | +**Test Dependencies**: |
| 185 | +- `github.com/stretchr/testify` - Assertions |
| 186 | +- `github.com/secsy/goftp` (replaced with `github.com/drakkan/goftp`) - FTP client |
| 187 | + |
| 188 | +## Common Tasks |
| 189 | + |
| 190 | +### Adding a New FTP Command |
| 191 | + |
| 192 | +1. Add handler method in appropriate `handle_*.go` file: |
| 193 | + ```go |
| 194 | + func (c *clientHandler) handleNEWCMD(param string) error { |
| 195 | + // Implementation |
| 196 | + return c.writeMessage(StatusOK, "Command successful") |
| 197 | + } |
| 198 | + ``` |
| 199 | + |
| 200 | +2. Register in `commandsMap` in `consts.go`: |
| 201 | + ```go |
| 202 | + "NEWCMD": {Fn: (*clientHandler).handleNEWCMD, Open: false, TransferRelated: false}, |
| 203 | + ``` |
| 204 | + |
| 205 | +3. Add to FEAT response if applicable (in `handleFEAT`) |
| 206 | + |
| 207 | +4. Write tests in corresponding `handle_*_test.go` |
| 208 | + |
| 209 | +### Adding a Driver Extension |
50 | 210 |
|
51 | | -### Command Handler Structure |
52 | | -- `handle_auth.go`: Authentication commands (USER, PASS, AUTH, PBSZ, PROT) |
53 | | -- `handle_files.go`: File operations (STOR, RETR, LIST, NLST, MLST, MLSD) |
54 | | -- `handle_dirs.go`: Directory operations (CWD, CDUP, MKD, RMD, PWD) |
55 | | -- `handle_misc.go`: System commands (SYST, FEAT, NOOP, QUIT, HELP) |
| 211 | +1. Define interface in `driver.go`: |
| 212 | + ```go |
| 213 | + type ClientDriverExtensionNewFeature interface { |
| 214 | + NewFeatureMethod(args) (result, error) |
| 215 | + } |
| 216 | + ``` |
56 | 217 |
|
57 | | -### Core Files |
58 | | -- `driver.go`: Interface definitions and driver extensions |
59 | | -- `client_handler.go`: Main protocol implementation and state management |
60 | | -- `server.go`: Server initialization and command routing |
61 | | -- `errors.go`: FTP-specific error codes and handling |
| 218 | +2. Check for extension in handler: |
| 219 | + ```go |
| 220 | + if ext, ok := c.driver.(ClientDriverExtensionNewFeature); ok { |
| 221 | + result, err := ext.NewFeatureMethod(args) |
| 222 | + } |
| 223 | + ``` |
62 | 224 |
|
63 | | -## Testing Architecture |
| 225 | +### Modifying Server Settings |
64 | 226 |
|
65 | | -The test suite uses a **reference driver implementation** (`driver_test.go`) with: |
66 | | -- Mock file system using `afero.NewBasePathFs` with temp directories |
67 | | -- Integration tests that simulate real FTP client interactions |
68 | | -- Comprehensive coverage of all command handlers and transfer modes |
69 | | -- Race condition testing for concurrent operations |
| 227 | +Settings are defined in `driver.go` (`Settings` struct) and returned via `MainDriver.GetSettings()`. Add new fields there and handle them appropriately in server/client code. |
70 | 228 |
|
71 | | -## Key Dependencies |
| 229 | +## CI/CD |
72 | 230 |
|
73 | | -- `github.com/spf13/afero`: File system abstraction for driver implementations |
74 | | -- `log/slog`: Go standard library structured logging (no external logging dependencies) |
| 231 | +GitHub Actions workflow (`.github/workflows/build.yml`): |
| 232 | +- Runs on: `ubuntu-24.04` |
| 233 | +- Go versions: 1.25 (with linting), 1.24 |
| 234 | +- Steps: Lint -> Build -> Test (with race detection) -> Codecov upload |
75 | 235 |
|
76 | | -## Code Conventions |
| 236 | +## Key Files Reference |
77 | 237 |
|
78 | | -- **No global state**: All server instances are isolated |
79 | | -- **Interface-based design**: Extensive use of optional interfaces for extensibility |
80 | | -- **Error handling**: Custom FTP error types with appropriate status codes |
81 | | -- **Concurrency**: Clean goroutine management without sleep/panic patterns |
82 | | -- **Line length**: 120 characters maximum (enforced by linter) |
83 | | -- **Function length**: 80 lines maximum (enforced by linter) |
| 238 | +- `driver.go` - All public interfaces |
| 239 | +- `server.go` - Server initialization and lifecycle |
| 240 | +- `client_handler.go` - Client state machine (largest file) |
| 241 | +- `consts.go` - FTP status codes and command registration |
| 242 | +- `driver_test.go` - Reference driver implementation for testing |
0 commit comments