diff --git a/ftp.go b/ftp.go index 02136ae..b92cd1f 100644 --- a/ftp.go +++ b/ftp.go @@ -544,6 +544,7 @@ func (c *ServerConn) pasv() (host string, port int, err error) { func isBogusDataIP(cmdIP, dataIP net.IP) bool { // Logic stolen from lftp (https://github.com/lavv17/lftp/blob/d67fc14d085849a6b0418bb3e912fea2e94c18d1/src/ftpclass.cc#L769) return dataIP.IsMulticast() || + dataIP.IsUnspecified() || // Explicitly handle 0.0.0.0 and :: cmdIP.IsPrivate() != dataIP.IsPrivate() || cmdIP.IsLoopback() != dataIP.IsLoopback() } diff --git a/parse_test.go b/parse_test.go index f53a61e..080ca59 100644 --- a/parse_test.go +++ b/parse_test.go @@ -70,7 +70,7 @@ var listTests = []line{ {"08-10-15 02:04PM Billing", "Billing", 0, EntryTypeFolder, newTime(2015, time.August, 10, 14, 4)}, {"08-07-2015 07:50PM 718 Post_PRR_20150901_1166_265118_13049.dat", "Post_PRR_20150901_1166_265118_13049.dat", 718, EntryTypeFile, newTime(2015, time.August, 7, 19, 50)}, {"08-10-2015 02:04PM Billing", "Billing", 0, EntryTypeFolder, newTime(2015, time.August, 10, 14, 4)}, - + // dir and file names that contain multiple spaces {"drwxr-xr-x 3 110 1002 3 Dec 02 2009 spaces dir name", "spaces dir name", 0, EntryTypeFolder, newTime(2009, time.December, 2)}, {"-rwxr-xr-x 3 110 1002 1234567 Dec 02 2009 file name", "file name", 1234567, EntryTypeFile, newTime(2009, time.December, 2)}, diff --git a/pasv_integration_test.go b/pasv_integration_test.go new file mode 100644 index 0000000..2874af4 --- /dev/null +++ b/pasv_integration_test.go @@ -0,0 +1,92 @@ +package ftp + +import ( + "net" + "net/textproto" + "strings" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +// TestPASVZeroIPIntegration tests the complete PASV flow when server returns 0,0,0,0 +func TestPASVZeroIPIntegration(t *testing.T) { + // Create a mock server that returns 0,0,0,0 in PASV + l, err := net.Listen("tcp", "127.0.0.1:0") + require.NoError(t, err) + defer l.Close() + + serverAddr := l.Addr().String() + + // Start a simple FTP server that returns 0,0,0,0 in PASV + go func() { + conn, err := l.Accept() + if err != nil { + return + } + defer conn.Close() + + proto := textproto.NewConn(conn) + defer proto.Close() + + // Send welcome message + proto.PrintfLine("220 Test FTP Server") + + // Handle commands + for { + line, err := proto.ReadLine() + if err != nil { + break + } + + parts := strings.Fields(line) + if len(parts) == 0 { + continue + } + + cmd := strings.ToUpper(parts[0]) + switch cmd { + case "USER": + proto.PrintfLine("331 User name okay, need password") + case "PASS": + proto.PrintfLine("230 User logged in, proceed") + case "FEAT": + proto.PrintfLine("211-Features:\r\n PASV\r\n EPSV\r\n211 End") + case "TYPE": + proto.PrintfLine("200 Type set") + case "OPTS": + proto.PrintfLine("200 Command okay") + case "PASV": + // Return 0,0,0,0 as IP with a dummy port + proto.PrintfLine("227 Entering Passive Mode (0,0,0,0,20,21).") + case "QUIT": + proto.PrintfLine("221 Goodbye") + return + default: + proto.PrintfLine("500 Unknown command") + } + } + }() + + // Connect to our test server + c, err := Dial(serverAddr) + require.NoError(t, err) + defer c.Quit() + + err = c.Login("test", "test") + require.NoError(t, err) + + // Disable EPSV to force PASV + c.options.disableEPSV = true + + // Test getDataConnPort which internally calls pasv() + host, port, err := c.getDataConnPort() + require.NoError(t, err) + + // Should return 127.0.0.1 (the connection IP) instead of 0.0.0.0 + assert.Equal(t, "127.0.0.1", host, "Should use connection IP when PASV returns 0,0,0,0") + assert.Equal(t, 5141, port, "Should return correct port (20*256 + 21)") + + t.Logf("PASV with 0,0,0,0 correctly returned host=%s, port=%d", host, port) +} diff --git a/pasv_zero_ip_test.go b/pasv_zero_ip_test.go new file mode 100644 index 0000000..0422be5 --- /dev/null +++ b/pasv_zero_ip_test.go @@ -0,0 +1,72 @@ +package ftp + +import ( + "net" + "testing" + + "github.com/stretchr/testify/assert" +) + +// TestBogusDataIPWithUnspecified tests that 0.0.0.0 and :: are treated as bogus +func TestBogusDataIPWithUnspecified(t *testing.T) { + tests := []struct { + name string + cmdIP string + dataIP string + expected bool + }{ + { + name: "0.0.0.0 should be bogus", + cmdIP: "127.0.0.1", + dataIP: "0.0.0.0", + expected: true, + }, + { + name: "IPv6 unspecified should be bogus", + cmdIP: "::1", + dataIP: "::", + expected: true, + }, + { + name: "0.0.0.0 from any IP should be bogus", + cmdIP: "192.168.1.1", + dataIP: "0.0.0.0", + expected: true, + }, + { + name: "Same private IP should not be bogus", + cmdIP: "192.168.1.1", + dataIP: "192.168.1.2", + expected: false, + }, + { + name: "Same loopback IP should not be bogus", + cmdIP: "127.0.0.1", + dataIP: "127.0.0.1", + expected: false, + }, + { + name: "Multicast should be bogus", + cmdIP: "127.0.0.1", + dataIP: "224.0.0.1", + expected: true, + }, + { + name: "Different network types should be bogus", + cmdIP: "127.0.0.1", // loopback + dataIP: "192.168.1.1", // private + expected: true, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + cmdIP := net.ParseIP(tt.cmdIP) + dataIP := net.ParseIP(tt.dataIP) + + result := isBogusDataIP(cmdIP, dataIP) + assert.Equal(t, tt.expected, result, + "isBogusDataIP(%s, %s) should return %v", tt.cmdIP, tt.dataIP, tt.expected) + }) + } +}