Skip to content

Commit aed438b

Browse files
fclairambKleissner
andauthored
(from #189) Add support for EPRT (Extended Port) according to RFC 2428. This was missing for full IPv6 support (Active Mode). (#194)
* Add support for EPRT (Extended Port) according to RFC 2428. This was missing for full IPv6 support (Active Mode). * Adding IPv6 tests * README update Co-authored-by: Kleissner <[email protected]>
1 parent f9a2ef5 commit aed438b

File tree

5 files changed

+154
-11
lines changed

5 files changed

+154
-11
lines changed

README.md

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,8 @@
66

77
This library allows to easily build a simple and fully-featured FTP server using [afero](https://github.com/spf13/afero) as the backend filesystem.
88

9-
If you're interested in a fully featured FTP server, you should use [ftpserver](https://github.com/fclairamb/ftpserver).
9+
If you're interested in a fully featured FTP server, you should use [ftpserver](https://github.com/fclairamb/ftpserver)
10+
or [sftpgo](https://github.com/drakkan/).
1011

1112
## Current status of the project
1213

@@ -17,8 +18,9 @@ If you're interested in a fully featured FTP server, you should use [ftpserver](
1718
* File and directory deletion and renaming
1819
* TLS support (AUTH + PROT)
1920
* File download/upload resume support (REST)
20-
* Passive socket connections (EPSV and PASV commands)
21-
* Active socket connections (PORT command)
21+
* Passive socket connections (PASV and EPSV commands)
22+
* Active socket connections (PORT and EPRT commands)
23+
* IPv6 support (EPSV + EPRT)
2224
* Small memory footprint
2325
* Clean code: No sync, no sleep, no panic
2426
* Uses only the standard library except for:
@@ -28,6 +30,7 @@ If you're interested in a fully featured FTP server, you should use [ftpserver](
2830
* [AUTH](https://tools.ietf.org/html/rfc2228#page-6) - Control session protection
2931
* [AUTH TLS](https://tools.ietf.org/html/rfc4217#section-4.1) - TLS session
3032
* [PROT](https://tools.ietf.org/html/rfc2228#page-8) - Transfer protection
33+
* [EPRT/EPSV](https://tools.ietf.org/html/rfc2428) - IPv6 support
3134
* [MDTM](https://tools.ietf.org/html/rfc3659#page-8) - File Modification Time
3235
* [SIZE](https://tools.ietf.org/html/rfc3659#page-11) - Size of a file
3336
* [REST](https://tools.ietf.org/html/rfc3659#page-13) - Restart of interrupted transfer
@@ -124,7 +127,6 @@ type Settings struct {
124127
ListenHost string // Host to receive connections on
125128
ListenPort int // Port to listen on
126129
PublicHost string // Public IP to expose (only an IP address is accepted at this stage)
127-
MaxConnections int // Max number of connections to accept
128130
DataPortRange *PortRange // Port Range for data connections. Random one will be used if not specified
129131
}
130132
```

server.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -76,6 +76,7 @@ var commandsMap = map[string]*CommandDescription{
7676
"PASV": {Fn: (*clientHandler).handlePASV},
7777
"EPSV": {Fn: (*clientHandler).handlePASV},
7878
"PORT": {Fn: (*clientHandler).handlePORT},
79+
"EPRT": {Fn: (*clientHandler).handlePORT},
7980
}
8081

8182
// FtpServer is where everything is stored

server_test.go

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ package ftpserver
33
import "testing"
44

55
func TestPortCommandFormatOK(t *testing.T) {
6-
net, err := parseRemoteAddr("127,0,0,1,239,163")
6+
net, err := parsePORTAddr("127,0,0,1,239,163")
77
if err != nil {
88
t.Fatal("Problem parsing", err)
99
}
@@ -23,7 +23,7 @@ func TestPortCommandFormatInvalid(t *testing.T) {
2323
"127,0,0,1,1,1,1",
2424
}
2525
for _, f := range badFormats {
26-
_, err := parseRemoteAddr(f)
26+
_, err := parsePORTAddr(f)
2727
if err == nil {
2828
t.Fatal("This should have failed", f)
2929
}

transfer_active.go

Lines changed: 49 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -15,12 +15,20 @@ import (
1515
func (c *clientHandler) handlePORT() error {
1616
if c.server.settings.DisableActiveMode {
1717
c.writeMessage(StatusServiceNotAvailable, "PORT command is disabled")
18+
return nil
1819
}
1920

20-
raddr, err := parseRemoteAddr(c.param)
21+
var err error
22+
var raddr *net.TCPAddr
23+
24+
if c.command == "EPRT" {
25+
raddr, err = parseEPRTAddr(c.param)
26+
} else { // PORT
27+
raddr, err = parsePORTAddr(c.param)
28+
}
2129

2230
if err != nil {
23-
c.writeMessage(StatusSyntaxErrorNotRecognised, fmt.Sprintf("Problem parsing PORT: %v", err))
31+
c.writeMessage(StatusSyntaxErrorNotRecognised, fmt.Sprintf("Problem parsing %s: %v", c.command, err))
2432
return nil
2533
}
2634

@@ -34,7 +42,7 @@ func (c *clientHandler) handlePORT() error {
3442
}
3543
}
3644

37-
c.writeMessage(StatusOK, "PORT command successful")
45+
c.writeMessage(StatusOK, c.command+" command successful")
3846
c.transfer = &activeTransferHandler{
3947
raddr: raddr,
4048
settings: c.server.settings,
@@ -93,13 +101,13 @@ var remoteAddrRegex = regexp.MustCompile(`^([0-9]{1,3},){5}[0-9]{1,3}$`)
93101
// ErrRemoteAddrFormat is returned when the remote address has a bad format
94102
var ErrRemoteAddrFormat = errors.New("remote address has a bad format")
95103

96-
// parseRemoteAddr parses remote address of the client from param. This address
104+
// parsePORTAddr parses remote address of the client from param. This address
97105
// is used for establishing a connection with the client.
98106
//
99107
// Param Format: 192,168,150,80,14,178
100108
// Host: 192.168.150.80
101109
// Port: (14 * 256) + 148
102-
func parseRemoteAddr(param string) (*net.TCPAddr, error) {
110+
func parsePORTAddr(param string) (*net.TCPAddr, error) {
103111
if !remoteAddrRegex.Match([]byte(param)) {
104112
return nil, fmt.Errorf("could not parse %s: %w", param, ErrRemoteAddrFormat)
105113
}
@@ -123,3 +131,39 @@ func parseRemoteAddr(param string) (*net.TCPAddr, error) {
123131

124132
return net.ResolveTCPAddr("tcp", fmt.Sprintf("%s:%d", ip, port))
125133
}
134+
135+
// Parse EPRT parameter. Full EPRT command format:
136+
// - IPv4 : "EPRT |1|h1.h2.h3.h4|port|\r\n"
137+
// - IPv6 : "EPRT |2|h1::h2:h3:h4:h5|port|\r\n"
138+
func parseEPRTAddr(param string) (addr *net.TCPAddr, err error) {
139+
params := strings.Split(param, "|")
140+
if len(params) != 5 {
141+
return nil, ErrRemoteAddrFormat
142+
}
143+
144+
netProtocol := params[1]
145+
remoteIP := params[2]
146+
remotePort := params[3]
147+
148+
// check port is valid
149+
var portI int
150+
if portI, err = strconv.Atoi(remotePort); err != nil || portI <= 0 || portI > 65535 {
151+
return nil, ErrRemoteAddrFormat
152+
}
153+
154+
var ip net.IP
155+
156+
switch netProtocol {
157+
case "1", "2":
158+
// use protocol 1 means IPv4. 2 means IPv6
159+
// net.ParseIP for validate IP
160+
if ip = net.ParseIP(remoteIP); ip == nil {
161+
return nil, ErrRemoteAddrFormat
162+
}
163+
default:
164+
// wrong network protocol
165+
return nil, ErrRemoteAddrFormat
166+
}
167+
168+
return net.ResolveTCPAddr("tcp", net.JoinHostPort(ip.String(), strconv.Itoa(portI)))
169+
}

transfer_test.go

Lines changed: 96 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -111,6 +111,22 @@ func ftpDelete(t *testing.T, ftp *goftp.Client, filename string) {
111111
}
112112
}
113113

114+
func TestTransferIPv6(t *testing.T) {
115+
s := NewTestServerWithDriver(
116+
t,
117+
&TestServerDriver{
118+
Debug: true,
119+
Settings: &Settings{
120+
ActiveTransferPortNon20: true,
121+
ListenAddr: "[::1]:0",
122+
},
123+
},
124+
)
125+
126+
t.Run("active", func(t *testing.T) { testTransferOnConnection(t, s, true, false, false) })
127+
t.Run("passive", func(t *testing.T) { testTransferOnConnection(t, s, false, false, false) })
128+
}
129+
114130
// TestTransfer validates the upload of file in both active and passive mode
115131
func TestTransfer(t *testing.T) {
116132
t.Run("without-tls", func(t *testing.T) {
@@ -268,6 +284,86 @@ func TestFailedTransfer(t *testing.T) {
268284
}
269285
}
270286

287+
func TestBogusTransferStart(t *testing.T) {
288+
s := NewTestServer(t, true)
289+
290+
c, err := goftp.DialConfig(goftp.Config{User: "test", Password: "test"}, s.Addr())
291+
if err != nil {
292+
t.Fatal(err)
293+
}
294+
295+
rc, err := c.OpenRawConn()
296+
if err != nil {
297+
t.Fatal(err)
298+
}
299+
300+
{ // Completely bogus port declaration
301+
status, resp, err := rc.SendCommand("PORT something")
302+
if err != nil {
303+
t.Fatal(err)
304+
}
305+
306+
if status != StatusSyntaxErrorNotRecognised {
307+
t.Fatal("Bad status:", status, resp)
308+
}
309+
}
310+
311+
{ // Completely bogus port declaration
312+
status, resp, err := rc.SendCommand("EPRT something")
313+
if err != nil {
314+
t.Fatal(err)
315+
}
316+
317+
if status != StatusSyntaxErrorNotRecognised {
318+
t.Fatal("Bad status:", status, resp)
319+
}
320+
}
321+
322+
{ // Bad port number: 0
323+
status, resp, err := rc.SendCommand("EPRT |2|::1|0|")
324+
if err != nil {
325+
t.Fatal(err)
326+
}
327+
328+
if status != StatusSyntaxErrorNotRecognised {
329+
t.Fatal("Bad status:", status, resp)
330+
}
331+
}
332+
333+
{ // Bad IP
334+
status, resp, err := rc.SendCommand("EPRT |1|253.254.255.256|2000|")
335+
if err != nil {
336+
t.Fatal(err)
337+
}
338+
339+
if status != StatusSyntaxErrorNotRecognised {
340+
t.Fatal("Bad status:", status, resp)
341+
}
342+
}
343+
344+
{ // Bad protocol type: 3
345+
status, resp, err := rc.SendCommand("EPRT |3|::1|2000|")
346+
if err != nil {
347+
t.Fatal(err)
348+
}
349+
350+
if status != StatusSyntaxErrorNotRecognised {
351+
t.Fatal("Bad status:", status, resp)
352+
}
353+
}
354+
355+
{ // We end-up on a positive note
356+
status, resp, err := rc.SendCommand("EPRT |1|::1|2000|")
357+
if err != nil {
358+
t.Fatal(err)
359+
}
360+
361+
if status != StatusOK {
362+
t.Fatal("Bad status:", status, resp)
363+
}
364+
}
365+
}
366+
271367
func TestFailedFileClose(t *testing.T) {
272368
driver := &TestServerDriver{
273369
Debug: true,

0 commit comments

Comments
 (0)