Skip to content

Commit

Permalink
feat(TLS): Adding TLS support (#392)
Browse files Browse the repository at this point in the history
* feat(TLS): Adding TLS support
  • Loading branch information
fclairamb authored Jul 27, 2021
1 parent de43c5a commit e8da449
Show file tree
Hide file tree
Showing 5 changed files with 79 additions and 12 deletions.
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -3,4 +3,5 @@ ftpserver
.idea
.vscode
*.json
*.pem
__debug__bin
2 changes: 1 addition & 1 deletion .golangci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -200,7 +200,7 @@ linters:
- nolintlint
- revive
- rowserrcheck
# - scopelint #deprecated
- exportloopref # replaces scopelint
- staticcheck
- structcheck
- stylecheck
Expand Down
27 changes: 19 additions & 8 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -61,13 +61,23 @@ go get -u github.com/fclairamb/ftpserver
```

### Config file
If you don't create a `ftpserver.json` file, it will be created for you.
If you don't create a `ftpserver.json` file, one will be created for you.

Here is a sample config file:

```json
{
"version": 1,
"passive_transfer_port_range": {
"start": 2122,
"end": 2130
},
"tls": {
"server_cert": {
"cert": "cert.pem",
"key": "key.pem"
}
},
"accesses": [
{
"user": "test",
Expand Down Expand Up @@ -103,7 +113,7 @@ Here is a sample config file:
}
},
{

"user": "s3",
"pass": "s3",
"fs": "s3",
Expand All @@ -127,14 +137,15 @@ Here is a sample config file:
"hostname": "192.168.168.11:22"
}
}
],
"passive_transfer_port_range": {
"start": 2122,
"end": 2130
}
]
}
```

You can generate the TLS key pair files with the following command:
```bash
openssl req -new -newkey rsa:4096 -x509 -sha256 -days 365 -nodes -out cert.pem -keyout key.pem
```

### With local binary
You can build the binary and use it directly:

Expand All @@ -158,7 +169,7 @@ diff kitty.jpg kitty2.jpg
```

### With docker
There's also a containerized version of the server (15MB, based on alpine).
There's also a containerized version of the server (31MB, based on alpine).

```sh
# Starting the sample FTP server
Expand Down
12 changes: 12 additions & 0 deletions config/confpar/confpar.go
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,17 @@ type Logging struct {
FileAccesses bool `json:"file_accesses"` // Log all file accesses
}

// TLS define the TLS Config
type TLS struct {
ServerCert *ServerCert `json:"server_cert"` // Server certificates
}

// ServerCert defines the TLS server certificate config
type ServerCert struct {
Cert string `json:"cert"` // Public certificate(s)
Key string `json:"key"` // Private key
}

// Content defines the content of the config file
type Content struct {
Version int `json:"version"` // File format version
Expand All @@ -41,4 +52,5 @@ type Content struct {
Accesses []*Access `json:"accesses"` // Accesses offered to users
PassiveTransferPortRange *PortRange `json:"passive_transfer_port_range"` // Listen port range
Logging Logging `json:"logging"` // Logging parameters
TLS *TLS `json:"tls"` // TLS Config
}
49 changes: 46 additions & 3 deletions server/server.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@ package server
import (
"crypto/tls"
"errors"
"fmt"
"io/ioutil"
"sync"
"time"

Expand All @@ -19,12 +21,15 @@ import (
)

// Server structure
type Server struct {
type Server struct { // nolint: maligned
config *config.Config
logger log.Logger
nbClients uint32
nbClientsSync sync.Mutex
zeroClientEvent chan error
tlsOnce sync.Once
tlsConfig *tls.Config
tlsError error
accesses *fsCache
}

Expand All @@ -43,7 +48,10 @@ func newFsCache() *fsCache {
var ErrTimeout = errors.New("timeout")

// ErrNotImplemented is returned when we're using something that has not been implemented yet
var ErrNotImplemented = errors.New("not implemented")
// var ErrNotImplemented = errors.New("not implemented")

// ErrNotEnabled is returned when a feature hasn't been enabled
var ErrNotEnabled = errors.New("not enabled")

// NewServer creates a server instance
func NewServer(config *config.Config, logger log.Logger) (*Server, error) {
Expand Down Expand Up @@ -202,8 +210,43 @@ type ClientDriver struct {
afero.Fs
}

func (s *Server) loadTLSConfig() (*tls.Config, error) {
tlsConf := s.config.Content.TLS
if tlsConf == nil || tlsConf.ServerCert == nil {
return nil, ErrNotEnabled
}

serverCert := tlsConf.ServerCert

certBytes, errReadFileCert := ioutil.ReadFile(serverCert.Cert)
if errReadFileCert != nil {
return nil, fmt.Errorf("could not load cert file: %s: %w", serverCert.Cert, errReadFileCert)
}

keyBytes, errReadFileKey := ioutil.ReadFile(serverCert.Key)
if errReadFileKey != nil {
return nil, fmt.Errorf("could not load key file: %s: %w", serverCert.Cert, errReadFileCert)
}

keypair, errKeyPair := tls.X509KeyPair(certBytes, keyBytes)
if errKeyPair != nil {
return nil, fmt.Errorf("could not parse key pairs: %w", errKeyPair)
}

return &tls.Config{
MinVersion: tls.VersionTLS12,
Certificates: []tls.Certificate{keypair},
}, nil
}

// GetTLSConfig returns a TLS Certificate to use
// The certificate could frequently change if we use something like "let's encrypt"
func (s *Server) GetTLSConfig() (*tls.Config, error) {
return nil, ErrNotImplemented
// The function is called every single time a control or transfer connection requires a TLS connection. As such
// it's important to cache it.
s.tlsOnce.Do(func() {
s.tlsConfig, s.tlsError = s.loadTLSConfig()
})

return s.tlsConfig, s.tlsError
}

0 comments on commit e8da449

Please sign in to comment.