Skip to content

Commit e8da449

Browse files
authored
feat(TLS): Adding TLS support (#392)
* feat(TLS): Adding TLS support
1 parent de43c5a commit e8da449

File tree

5 files changed

+79
-12
lines changed

5 files changed

+79
-12
lines changed

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,4 +3,5 @@ ftpserver
33
.idea
44
.vscode
55
*.json
6+
*.pem
67
__debug__bin

.golangci.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -200,7 +200,7 @@ linters:
200200
- nolintlint
201201
- revive
202202
- rowserrcheck
203-
# - scopelint #deprecated
203+
- exportloopref # replaces scopelint
204204
- staticcheck
205205
- structcheck
206206
- stylecheck

README.md

Lines changed: 19 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -61,13 +61,23 @@ go get -u github.com/fclairamb/ftpserver
6161
```
6262

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

6666
Here is a sample config file:
6767

6868
```json
6969
{
7070
"version": 1,
71+
"passive_transfer_port_range": {
72+
"start": 2122,
73+
"end": 2130
74+
},
75+
"tls": {
76+
"server_cert": {
77+
"cert": "cert.pem",
78+
"key": "key.pem"
79+
}
80+
},
7181
"accesses": [
7282
{
7383
"user": "test",
@@ -103,7 +113,7 @@ Here is a sample config file:
103113
}
104114
},
105115
{
106-
116+
107117
"user": "s3",
108118
"pass": "s3",
109119
"fs": "s3",
@@ -127,14 +137,15 @@ Here is a sample config file:
127137
"hostname": "192.168.168.11:22"
128138
}
129139
}
130-
],
131-
"passive_transfer_port_range": {
132-
"start": 2122,
133-
"end": 2130
134-
}
140+
]
135141
}
136142
```
137143

144+
You can generate the TLS key pair files with the following command:
145+
```bash
146+
openssl req -new -newkey rsa:4096 -x509 -sha256 -days 365 -nodes -out cert.pem -keyout key.pem
147+
```
148+
138149
### With local binary
139150
You can build the binary and use it directly:
140151

@@ -158,7 +169,7 @@ diff kitty.jpg kitty2.jpg
158169
```
159170

160171
### With docker
161-
There's also a containerized version of the server (15MB, based on alpine).
172+
There's also a containerized version of the server (31MB, based on alpine).
162173

163174
```sh
164175
# Starting the sample FTP server

config/confpar/confpar.go

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,17 @@ type Logging struct {
3232
FileAccesses bool `json:"file_accesses"` // Log all file accesses
3333
}
3434

35+
// TLS define the TLS Config
36+
type TLS struct {
37+
ServerCert *ServerCert `json:"server_cert"` // Server certificates
38+
}
39+
40+
// ServerCert defines the TLS server certificate config
41+
type ServerCert struct {
42+
Cert string `json:"cert"` // Public certificate(s)
43+
Key string `json:"key"` // Private key
44+
}
45+
3546
// Content defines the content of the config file
3647
type Content struct {
3748
Version int `json:"version"` // File format version
@@ -41,4 +52,5 @@ type Content struct {
4152
Accesses []*Access `json:"accesses"` // Accesses offered to users
4253
PassiveTransferPortRange *PortRange `json:"passive_transfer_port_range"` // Listen port range
4354
Logging Logging `json:"logging"` // Logging parameters
55+
TLS *TLS `json:"tls"` // TLS Config
4456
}

server/server.go

Lines changed: 46 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,8 @@ package server
44
import (
55
"crypto/tls"
66
"errors"
7+
"fmt"
8+
"io/ioutil"
79
"sync"
810
"time"
911

@@ -19,12 +21,15 @@ import (
1921
)
2022

2123
// Server structure
22-
type Server struct {
24+
type Server struct { // nolint: maligned
2325
config *config.Config
2426
logger log.Logger
2527
nbClients uint32
2628
nbClientsSync sync.Mutex
2729
zeroClientEvent chan error
30+
tlsOnce sync.Once
31+
tlsConfig *tls.Config
32+
tlsError error
2833
accesses *fsCache
2934
}
3035

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

4550
// ErrNotImplemented is returned when we're using something that has not been implemented yet
46-
var ErrNotImplemented = errors.New("not implemented")
51+
// var ErrNotImplemented = errors.New("not implemented")
52+
53+
// ErrNotEnabled is returned when a feature hasn't been enabled
54+
var ErrNotEnabled = errors.New("not enabled")
4755

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

213+
func (s *Server) loadTLSConfig() (*tls.Config, error) {
214+
tlsConf := s.config.Content.TLS
215+
if tlsConf == nil || tlsConf.ServerCert == nil {
216+
return nil, ErrNotEnabled
217+
}
218+
219+
serverCert := tlsConf.ServerCert
220+
221+
certBytes, errReadFileCert := ioutil.ReadFile(serverCert.Cert)
222+
if errReadFileCert != nil {
223+
return nil, fmt.Errorf("could not load cert file: %s: %w", serverCert.Cert, errReadFileCert)
224+
}
225+
226+
keyBytes, errReadFileKey := ioutil.ReadFile(serverCert.Key)
227+
if errReadFileKey != nil {
228+
return nil, fmt.Errorf("could not load key file: %s: %w", serverCert.Cert, errReadFileCert)
229+
}
230+
231+
keypair, errKeyPair := tls.X509KeyPair(certBytes, keyBytes)
232+
if errKeyPair != nil {
233+
return nil, fmt.Errorf("could not parse key pairs: %w", errKeyPair)
234+
}
235+
236+
return &tls.Config{
237+
MinVersion: tls.VersionTLS12,
238+
Certificates: []tls.Certificate{keypair},
239+
}, nil
240+
}
241+
205242
// GetTLSConfig returns a TLS Certificate to use
206243
// The certificate could frequently change if we use something like "let's encrypt"
207244
func (s *Server) GetTLSConfig() (*tls.Config, error) {
208-
return nil, ErrNotImplemented
245+
// The function is called every single time a control or transfer connection requires a TLS connection. As such
246+
// it's important to cache it.
247+
s.tlsOnce.Do(func() {
248+
s.tlsConfig, s.tlsError = s.loadTLSConfig()
249+
})
250+
251+
return s.tlsConfig, s.tlsError
209252
}

0 commit comments

Comments
 (0)