From aed438b4c9034ff85024d9426a565923d8a8d6c4 Mon Sep 17 00:00:00 2001 From: Florent Clairambault Date: Fri, 11 Dec 2020 00:23:50 +0100 Subject: [PATCH] (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 --- README.md | 10 +++-- server.go | 1 + server_test.go | 4 +- transfer_active.go | 54 +++++++++++++++++++++++--- transfer_test.go | 96 ++++++++++++++++++++++++++++++++++++++++++++++ 5 files changed, 154 insertions(+), 11 deletions(-) diff --git a/README.md b/README.md index 9997f52f..6f38fc44 100644 --- a/README.md +++ b/README.md @@ -6,7 +6,8 @@ This library allows to easily build a simple and fully-featured FTP server using [afero](https://github.com/spf13/afero) as the backend filesystem. -If you're interested in a fully featured FTP server, you should use [ftpserver](https://github.com/fclairamb/ftpserver). +If you're interested in a fully featured FTP server, you should use [ftpserver](https://github.com/fclairamb/ftpserver) +or [sftpgo](https://github.com/drakkan/). ## Current status of the project @@ -17,8 +18,9 @@ If you're interested in a fully featured FTP server, you should use [ftpserver]( * File and directory deletion and renaming * TLS support (AUTH + PROT) * File download/upload resume support (REST) - * Passive socket connections (EPSV and PASV commands) - * Active socket connections (PORT command) + * Passive socket connections (PASV and EPSV commands) + * Active socket connections (PORT and EPRT commands) + * IPv6 support (EPSV + EPRT) * Small memory footprint * Clean code: No sync, no sleep, no panic * 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]( * [AUTH](https://tools.ietf.org/html/rfc2228#page-6) - Control session protection * [AUTH TLS](https://tools.ietf.org/html/rfc4217#section-4.1) - TLS session * [PROT](https://tools.ietf.org/html/rfc2228#page-8) - Transfer protection + * [EPRT/EPSV](https://tools.ietf.org/html/rfc2428) - IPv6 support * [MDTM](https://tools.ietf.org/html/rfc3659#page-8) - File Modification Time * [SIZE](https://tools.ietf.org/html/rfc3659#page-11) - Size of a file * [REST](https://tools.ietf.org/html/rfc3659#page-13) - Restart of interrupted transfer @@ -124,7 +127,6 @@ type Settings struct { ListenHost string // Host to receive connections on ListenPort int // Port to listen on PublicHost string // Public IP to expose (only an IP address is accepted at this stage) - MaxConnections int // Max number of connections to accept DataPortRange *PortRange // Port Range for data connections. Random one will be used if not specified } ``` diff --git a/server.go b/server.go index eaf9acbc..ab9f1aa8 100644 --- a/server.go +++ b/server.go @@ -76,6 +76,7 @@ var commandsMap = map[string]*CommandDescription{ "PASV": {Fn: (*clientHandler).handlePASV}, "EPSV": {Fn: (*clientHandler).handlePASV}, "PORT": {Fn: (*clientHandler).handlePORT}, + "EPRT": {Fn: (*clientHandler).handlePORT}, } // FtpServer is where everything is stored diff --git a/server_test.go b/server_test.go index 118d63d7..44c56766 100644 --- a/server_test.go +++ b/server_test.go @@ -3,7 +3,7 @@ package ftpserver import "testing" func TestPortCommandFormatOK(t *testing.T) { - net, err := parseRemoteAddr("127,0,0,1,239,163") + net, err := parsePORTAddr("127,0,0,1,239,163") if err != nil { t.Fatal("Problem parsing", err) } @@ -23,7 +23,7 @@ func TestPortCommandFormatInvalid(t *testing.T) { "127,0,0,1,1,1,1", } for _, f := range badFormats { - _, err := parseRemoteAddr(f) + _, err := parsePORTAddr(f) if err == nil { t.Fatal("This should have failed", f) } diff --git a/transfer_active.go b/transfer_active.go index d25bd791..3db9aa21 100644 --- a/transfer_active.go +++ b/transfer_active.go @@ -15,12 +15,20 @@ import ( func (c *clientHandler) handlePORT() error { if c.server.settings.DisableActiveMode { c.writeMessage(StatusServiceNotAvailable, "PORT command is disabled") + return nil } - raddr, err := parseRemoteAddr(c.param) + var err error + var raddr *net.TCPAddr + + if c.command == "EPRT" { + raddr, err = parseEPRTAddr(c.param) + } else { // PORT + raddr, err = parsePORTAddr(c.param) + } if err != nil { - c.writeMessage(StatusSyntaxErrorNotRecognised, fmt.Sprintf("Problem parsing PORT: %v", err)) + c.writeMessage(StatusSyntaxErrorNotRecognised, fmt.Sprintf("Problem parsing %s: %v", c.command, err)) return nil } @@ -34,7 +42,7 @@ func (c *clientHandler) handlePORT() error { } } - c.writeMessage(StatusOK, "PORT command successful") + c.writeMessage(StatusOK, c.command+" command successful") c.transfer = &activeTransferHandler{ raddr: raddr, settings: c.server.settings, @@ -93,13 +101,13 @@ var remoteAddrRegex = regexp.MustCompile(`^([0-9]{1,3},){5}[0-9]{1,3}$`) // ErrRemoteAddrFormat is returned when the remote address has a bad format var ErrRemoteAddrFormat = errors.New("remote address has a bad format") -// parseRemoteAddr parses remote address of the client from param. This address +// parsePORTAddr parses remote address of the client from param. This address // is used for establishing a connection with the client. // // Param Format: 192,168,150,80,14,178 // Host: 192.168.150.80 // Port: (14 * 256) + 148 -func parseRemoteAddr(param string) (*net.TCPAddr, error) { +func parsePORTAddr(param string) (*net.TCPAddr, error) { if !remoteAddrRegex.Match([]byte(param)) { return nil, fmt.Errorf("could not parse %s: %w", param, ErrRemoteAddrFormat) } @@ -123,3 +131,39 @@ func parseRemoteAddr(param string) (*net.TCPAddr, error) { return net.ResolveTCPAddr("tcp", fmt.Sprintf("%s:%d", ip, port)) } + +// Parse EPRT parameter. Full EPRT command format: +// - IPv4 : "EPRT |1|h1.h2.h3.h4|port|\r\n" +// - IPv6 : "EPRT |2|h1::h2:h3:h4:h5|port|\r\n" +func parseEPRTAddr(param string) (addr *net.TCPAddr, err error) { + params := strings.Split(param, "|") + if len(params) != 5 { + return nil, ErrRemoteAddrFormat + } + + netProtocol := params[1] + remoteIP := params[2] + remotePort := params[3] + + // check port is valid + var portI int + if portI, err = strconv.Atoi(remotePort); err != nil || portI <= 0 || portI > 65535 { + return nil, ErrRemoteAddrFormat + } + + var ip net.IP + + switch netProtocol { + case "1", "2": + // use protocol 1 means IPv4. 2 means IPv6 + // net.ParseIP for validate IP + if ip = net.ParseIP(remoteIP); ip == nil { + return nil, ErrRemoteAddrFormat + } + default: + // wrong network protocol + return nil, ErrRemoteAddrFormat + } + + return net.ResolveTCPAddr("tcp", net.JoinHostPort(ip.String(), strconv.Itoa(portI))) +} diff --git a/transfer_test.go b/transfer_test.go index db0486c7..a44d5853 100644 --- a/transfer_test.go +++ b/transfer_test.go @@ -111,6 +111,22 @@ func ftpDelete(t *testing.T, ftp *goftp.Client, filename string) { } } +func TestTransferIPv6(t *testing.T) { + s := NewTestServerWithDriver( + t, + &TestServerDriver{ + Debug: true, + Settings: &Settings{ + ActiveTransferPortNon20: true, + ListenAddr: "[::1]:0", + }, + }, + ) + + t.Run("active", func(t *testing.T) { testTransferOnConnection(t, s, true, false, false) }) + t.Run("passive", func(t *testing.T) { testTransferOnConnection(t, s, false, false, false) }) +} + // TestTransfer validates the upload of file in both active and passive mode func TestTransfer(t *testing.T) { t.Run("without-tls", func(t *testing.T) { @@ -268,6 +284,86 @@ func TestFailedTransfer(t *testing.T) { } } +func TestBogusTransferStart(t *testing.T) { + s := NewTestServer(t, true) + + c, err := goftp.DialConfig(goftp.Config{User: "test", Password: "test"}, s.Addr()) + if err != nil { + t.Fatal(err) + } + + rc, err := c.OpenRawConn() + if err != nil { + t.Fatal(err) + } + + { // Completely bogus port declaration + status, resp, err := rc.SendCommand("PORT something") + if err != nil { + t.Fatal(err) + } + + if status != StatusSyntaxErrorNotRecognised { + t.Fatal("Bad status:", status, resp) + } + } + + { // Completely bogus port declaration + status, resp, err := rc.SendCommand("EPRT something") + if err != nil { + t.Fatal(err) + } + + if status != StatusSyntaxErrorNotRecognised { + t.Fatal("Bad status:", status, resp) + } + } + + { // Bad port number: 0 + status, resp, err := rc.SendCommand("EPRT |2|::1|0|") + if err != nil { + t.Fatal(err) + } + + if status != StatusSyntaxErrorNotRecognised { + t.Fatal("Bad status:", status, resp) + } + } + + { // Bad IP + status, resp, err := rc.SendCommand("EPRT |1|253.254.255.256|2000|") + if err != nil { + t.Fatal(err) + } + + if status != StatusSyntaxErrorNotRecognised { + t.Fatal("Bad status:", status, resp) + } + } + + { // Bad protocol type: 3 + status, resp, err := rc.SendCommand("EPRT |3|::1|2000|") + if err != nil { + t.Fatal(err) + } + + if status != StatusSyntaxErrorNotRecognised { + t.Fatal("Bad status:", status, resp) + } + } + + { // We end-up on a positive note + status, resp, err := rc.SendCommand("EPRT |1|::1|2000|") + if err != nil { + t.Fatal(err) + } + + if status != StatusOK { + t.Fatal("Bad status:", status, resp) + } + } +} + func TestFailedFileClose(t *testing.T) { driver := &TestServerDriver{ Debug: true,