Skip to content

Commit

Permalink
(from #189) Add support for EPRT (Extended Port) according to RFC 242…
Browse files Browse the repository at this point in the history
…8. 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]>
  • Loading branch information
fclairamb authored Dec 10, 2020
1 parent f9a2ef5 commit aed438b
Show file tree
Hide file tree
Showing 5 changed files with 154 additions and 11 deletions.
10 changes: 6 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand All @@ -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:
Expand All @@ -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
Expand Down Expand Up @@ -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
}
```
Expand Down
1 change: 1 addition & 0 deletions server.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
4 changes: 2 additions & 2 deletions server_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
}
Expand All @@ -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)
}
Expand Down
54 changes: 49 additions & 5 deletions transfer_active.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
}

Expand All @@ -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,
Expand Down Expand Up @@ -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)
}
Expand All @@ -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)))
}
96 changes: 96 additions & 0 deletions transfer_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand Down Expand Up @@ -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,
Expand Down

0 comments on commit aed438b

Please sign in to comment.