ftp/conn_test.go

445 lines
11 KiB
Go
Raw Normal View History

2019-04-10 20:20:50 +02:00
package ftp
import (
2020-03-10 11:41:46 +01:00
"bytes"
2019-04-10 20:20:50 +02:00
"errors"
"io"
"net"
"net/textproto"
"strconv"
"strings"
"sync"
"testing"
"time"
2022-08-18 01:24:40 +02:00
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
2019-04-10 20:20:50 +02:00
)
type ftpMock struct {
2022-03-09 00:35:30 +01:00
t *testing.T
2019-04-10 20:20:50 +02:00
address string
modtime string // no-time, std-time, vsftpd
2019-04-10 20:20:50 +02:00
listener *net.TCPListener
proto *textproto.Conn
commands []string // list of received commands
lastFull string // full last command
2019-04-10 20:20:50 +02:00
rest int
2020-03-10 11:41:46 +01:00
fileCont *bytes.Buffer
2019-04-10 20:20:50 +02:00
dataConn *mockDataConn
sync.WaitGroup
}
// newFtpMock returns a mock implementation of a FTP server
// For simplication, a mock instance only accepts a signle connection and terminates afer
func newFtpMock(t *testing.T, address string) (*ftpMock, error) {
return newFtpMockExt(t, address, "no-time")
}
func newFtpMockExt(t *testing.T, address, modtime string) (*ftpMock, error) {
2019-04-10 20:20:50 +02:00
var err error
mock := &ftpMock{
2022-03-09 00:35:30 +01:00
t: t,
address: address,
modtime: modtime,
}
2019-04-10 20:20:50 +02:00
l, err := net.Listen("tcp", address+":0")
if err != nil {
return nil, err
}
tcpListener, ok := l.(*net.TCPListener)
if !ok {
return nil, errors.New("listener is not a net.TCPListener")
}
mock.listener = tcpListener
2022-03-09 00:35:30 +01:00
go mock.listen()
2019-04-10 20:20:50 +02:00
return mock, nil
}
2022-03-09 00:35:30 +01:00
func (mock *ftpMock) listen() {
2019-04-10 20:20:50 +02:00
// Listen for an incoming connection.
conn, err := mock.listener.Accept()
if err != nil {
2022-03-09 00:35:30 +01:00
mock.t.Errorf("can not accept: %s", err)
2019-04-10 20:20:50 +02:00
return
}
// Do not accept incoming connections anymore
mock.listener.Close()
mock.Add(1)
defer mock.Done()
defer conn.Close()
mock.proto = textproto.NewConn(conn)
2022-03-09 00:35:30 +01:00
mock.printfLine("220 FTP Server ready.")
2019-04-10 20:20:50 +02:00
for {
fullCommand, _ := mock.proto.ReadLine()
mock.lastFull = fullCommand
2019-04-10 20:20:50 +02:00
cmdParts := strings.Split(fullCommand, " ")
// Append to list of received commands
mock.commands = append(mock.commands, cmdParts[0])
// At least one command must have a multiline response
switch cmdParts[0] {
case "FEAT":
features := "211-Features:\r\n FEAT\r\n PASV\r\n EPSV\r\n UTF8\r\n SIZE\r\n MLST\r\n"
switch mock.modtime {
case "std-time":
features += " MDTM\r\n MFMT\r\n"
case "vsftpd":
features += " MDTM\r\n"
}
features += "211 End"
2022-03-09 00:35:30 +01:00
mock.printfLine(features)
2019-04-10 20:20:50 +02:00
case "USER":
if cmdParts[1] == "anonymous" {
2022-03-09 00:35:30 +01:00
mock.printfLine("331 Please send your password")
2019-04-10 20:20:50 +02:00
} else {
2022-03-09 00:35:30 +01:00
mock.printfLine("530 This FTP server is anonymous only")
2019-04-10 20:20:50 +02:00
}
case "PASS":
2022-03-09 00:35:30 +01:00
mock.printfLine("230-Hey,\r\nWelcome to my FTP\r\n230 Access granted")
2019-04-10 20:20:50 +02:00
case "TYPE":
2022-03-09 00:35:30 +01:00
mock.printfLine("200 Type set ok")
2019-04-10 20:20:50 +02:00
case "CWD":
if cmdParts[1] == "missing-dir" {
2022-03-09 00:35:30 +01:00
mock.printfLine("550 %s: No such file or directory", cmdParts[1])
2019-04-10 20:20:50 +02:00
} else {
2022-03-09 00:35:30 +01:00
mock.printfLine("250 Directory successfully changed.")
2019-04-10 20:20:50 +02:00
}
case "DELE":
2022-03-09 00:35:30 +01:00
mock.printfLine("250 File successfully removed.")
2019-04-10 20:20:50 +02:00
case "MKD":
2022-03-09 00:35:30 +01:00
mock.printfLine("257 Directory successfully created.")
2019-04-10 20:20:50 +02:00
case "RMD":
if cmdParts[1] == "missing-dir" {
2022-03-09 00:35:30 +01:00
mock.printfLine("550 No such file or directory")
2019-04-10 20:20:50 +02:00
} else {
2022-03-09 00:35:30 +01:00
mock.printfLine("250 Directory successfully removed.")
2019-04-10 20:20:50 +02:00
}
case "PWD":
2022-03-09 00:35:30 +01:00
mock.printfLine("257 \"/incoming\"")
2019-04-10 20:20:50 +02:00
case "CDUP":
2022-03-09 00:35:30 +01:00
mock.printfLine("250 CDUP command successful")
2019-04-10 20:20:50 +02:00
case "SIZE":
if cmdParts[1] == "magic-file" {
2022-03-09 00:35:30 +01:00
mock.printfLine("213 42")
2019-04-10 20:20:50 +02:00
} else {
2022-03-09 00:35:30 +01:00
mock.printfLine("550 Could not get file size.")
2019-04-10 20:20:50 +02:00
}
case "PASV":
p, err := mock.listenDataConn()
if err != nil {
2022-03-09 00:35:30 +01:00
mock.printfLine("451 %s.", err)
2019-04-10 20:20:50 +02:00
break
}
p1 := int(p / 256)
p2 := p % 256
2022-03-09 00:35:30 +01:00
mock.printfLine("227 Entering Passive Mode (127,0,0,1,%d,%d).", p1, p2)
2019-04-10 20:20:50 +02:00
case "EPSV":
p, err := mock.listenDataConn()
if err != nil {
2022-03-09 00:35:30 +01:00
mock.printfLine("451 %s.", err)
2019-04-10 20:20:50 +02:00
break
}
2022-03-09 00:35:30 +01:00
mock.printfLine("229 Entering Extended Passive Mode (|||%d|)", p)
2019-04-10 20:20:50 +02:00
case "STOR":
if mock.dataConn == nil {
2022-03-09 00:35:30 +01:00
mock.printfLine("425 Unable to build data connection: Connection refused")
2019-04-10 20:20:50 +02:00
break
}
2022-03-09 00:35:30 +01:00
mock.printfLine("150 please send")
2020-03-10 11:43:17 +01:00
mock.recvDataConn(false)
case "APPE":
if mock.dataConn == nil {
2022-03-09 00:35:30 +01:00
mock.printfLine("425 Unable to build data connection: Connection refused")
2020-03-10 11:43:17 +01:00
break
}
2022-03-09 00:35:30 +01:00
mock.printfLine("150 please send")
2020-03-10 11:43:17 +01:00
mock.recvDataConn(true)
2019-04-10 20:20:50 +02:00
case "LIST":
if mock.dataConn == nil {
2022-03-09 00:35:30 +01:00
mock.printfLine("425 Unable to build data connection: Connection refused")
2019-04-10 20:20:50 +02:00
break
}
mock.dataConn.Wait()
2022-03-09 00:35:30 +01:00
mock.printfLine("150 Opening ASCII mode data connection for file list")
mock.dataConn.write([]byte("-rw-r--r-- 1 ftp wheel 0 Jan 29 10:29 lo\r\ntotal 1"))
2022-03-09 00:35:30 +01:00
mock.printfLine("226 Transfer complete")
2019-04-10 20:20:50 +02:00
mock.closeDataConn()
case "MLSD":
if mock.dataConn == nil {
mock.printfLine("425 Unable to build data connection: Connection refused")
break
}
mock.dataConn.Wait()
mock.printfLine("150 Opening data connection for file list")
mock.dataConn.write([]byte("Type=file;Size=0;Modify=20201213202400; lo\r\n"))
mock.printfLine("226 Transfer complete")
mock.closeDataConn()
case "MLST":
if cmdParts[1] == "multiline-dir" {
mock.printfLine("250-File data\r\n Type=dir;Size=0; multiline-dir\r\n Modify=20201213202400; multiline-dir\r\n250 End")
} else {
mock.printfLine("250-File data\r\n Type=file;Size=42;Modify=20201213202400; magic-file\r\n \r\n250 End")
}
2019-04-10 20:20:50 +02:00
case "NLST":
if mock.dataConn == nil {
2022-03-09 00:35:30 +01:00
mock.printfLine("425 Unable to build data connection: Connection refused")
2019-04-10 20:20:50 +02:00
break
}
mock.dataConn.Wait()
2022-03-09 00:35:30 +01:00
mock.printfLine("150 Opening ASCII mode data connection for file list")
mock.dataConn.write([]byte("/incoming"))
mock.printfLine("226 Transfer complete")
2019-04-10 20:20:50 +02:00
mock.closeDataConn()
case "RETR":
if mock.dataConn == nil {
2022-03-09 00:35:30 +01:00
mock.printfLine("425 Unable to build data connection: Connection refused")
2019-04-10 20:20:50 +02:00
break
}
mock.dataConn.Wait()
2022-03-09 00:35:30 +01:00
mock.printfLine("150 Opening ASCII mode data connection for file list")
mock.dataConn.write(mock.fileCont.Bytes()[mock.rest:])
2019-04-10 20:20:50 +02:00
mock.rest = 0
2022-03-09 00:35:30 +01:00
mock.printfLine("226 Transfer complete")
2019-04-10 20:20:50 +02:00
mock.closeDataConn()
case "RNFR":
2022-03-09 00:35:30 +01:00
mock.printfLine("350 File or directory exists, ready for destination name")
2019-04-10 20:20:50 +02:00
case "RNTO":
2022-03-09 00:35:30 +01:00
mock.printfLine("250 Rename successful")
2019-04-10 20:20:50 +02:00
case "REST":
if len(cmdParts) != 2 {
2022-03-09 00:35:30 +01:00
mock.printfLine("500 wrong number of arguments")
2019-04-10 20:20:50 +02:00
break
}
rest, err := strconv.Atoi(cmdParts[1])
if err != nil {
2022-03-09 00:35:30 +01:00
mock.printfLine("500 REST: %s", err)
2019-04-10 20:20:50 +02:00
break
}
mock.rest = rest
2022-03-09 00:35:30 +01:00
mock.printfLine("350 Restarting at %s. Send STORE or RETRIEVE to initiate transfer", cmdParts[1])
case "MDTM":
var answer string
switch {
case mock.modtime == "no-time":
answer = "500 Unknown command MDTM"
case len(cmdParts) == 3 && mock.modtime == "vsftpd":
answer = "213 UTIME OK"
_, err := time.ParseInLocation(timeFormat, cmdParts[1], time.UTC)
if err != nil {
answer = "501 Can't get a time stamp"
}
case len(cmdParts) == 2:
answer = "213 20201213202400"
default:
answer = "500 wrong number of arguments"
}
2022-03-09 00:35:30 +01:00
mock.printfLine(answer)
case "MFMT":
var answer string
switch {
case mock.modtime == "std-time" && len(cmdParts) == 3:
answer = "213 UTIME OK"
_, err := time.ParseInLocation(timeFormat, cmdParts[1], time.UTC)
if err != nil {
answer = "501 Can't get a time stamp"
}
default:
answer = "500 Unknown command MFMT"
}
2022-03-09 00:35:30 +01:00
mock.printfLine(answer)
2019-04-10 20:20:50 +02:00
case "NOOP":
2022-03-09 00:35:30 +01:00
mock.printfLine("200 NOOP ok.")
2020-05-05 13:34:35 +02:00
case "OPTS":
if len(cmdParts) != 3 {
2022-03-09 00:35:30 +01:00
mock.printfLine("500 wrong number of arguments")
2020-05-05 13:34:35 +02:00
break
}
if (strings.Join(cmdParts[1:], " ")) == "UTF8 ON" {
2022-03-09 00:35:30 +01:00
mock.printfLine("200 OK, UTF-8 enabled")
2020-05-05 13:34:35 +02:00
}
2019-04-10 20:20:50 +02:00
case "REIN":
2022-03-09 00:35:30 +01:00
mock.printfLine("220 Logged out")
2019-04-10 20:20:50 +02:00
case "QUIT":
2022-03-09 00:35:30 +01:00
mock.printfLine("221 Goodbye.")
2019-04-10 20:20:50 +02:00
return
default:
2022-03-09 00:35:30 +01:00
mock.printfLine("500 Unknown command %s.", cmdParts[0])
2019-04-10 20:20:50 +02:00
}
}
}
2022-03-09 00:35:30 +01:00
func (mock *ftpMock) printfLine(format string, args ...interface{}) {
if err := mock.proto.Writer.PrintfLine(format, args...); err != nil {
mock.t.Fatal(err)
}
}
func (mock *ftpMock) closeDataConn() {
2019-04-10 20:20:50 +02:00
if mock.dataConn != nil {
2022-03-09 00:35:30 +01:00
if err := mock.dataConn.Close(); err != nil {
mock.t.Fatal(err)
}
2019-04-10 20:20:50 +02:00
mock.dataConn = nil
}
}
type mockDataConn struct {
2022-03-09 00:35:30 +01:00
t *testing.T
2019-04-10 20:20:50 +02:00
listener *net.TCPListener
conn net.Conn
// WaitGroup is done when conn is accepted and stored
sync.WaitGroup
}
func (d *mockDataConn) Close() (err error) {
if d.listener != nil {
err = d.listener.Close()
}
if d.conn != nil {
err = d.conn.Close()
}
return
}
2022-03-09 00:35:30 +01:00
func (d *mockDataConn) write(b []byte) {
if d.conn == nil {
d.t.Fatal("data conn is not opened")
}
if _, err := d.conn.Write(b); err != nil {
d.t.Fatal(err)
}
}
2019-04-10 20:20:50 +02:00
func (mock *ftpMock) listenDataConn() (int64, error) {
mock.closeDataConn()
l, err := net.Listen("tcp", mock.address+":0")
if err != nil {
return 0, err
}
tcpListener, ok := l.(*net.TCPListener)
if !ok {
return 0, errors.New("listener is not a net.TCPListener")
}
addr := tcpListener.Addr().String()
_, port, err := net.SplitHostPort(addr)
if err != nil {
return 0, err
}
p, err := strconv.ParseInt(port, 10, 32)
if err != nil {
return 0, err
}
2022-03-09 00:35:30 +01:00
dataConn := &mockDataConn{
t: mock.t,
listener: tcpListener,
}
2019-04-10 20:20:50 +02:00
dataConn.Add(1)
go func() {
// Listen for an incoming connection.
conn, err := dataConn.listener.Accept()
if err != nil {
2022-03-09 00:35:30 +01:00
// mock.t.Fatalf("can not accept data conn: %s", err)
2019-04-10 20:20:50 +02:00
return
}
dataConn.conn = conn
dataConn.Done()
}()
mock.dataConn = dataConn
return p, nil
}
2020-03-10 11:43:17 +01:00
func (mock *ftpMock) recvDataConn(append bool) {
2019-04-10 20:20:50 +02:00
mock.dataConn.Wait()
2020-03-10 11:43:17 +01:00
if !append {
mock.fileCont = new(bytes.Buffer)
}
2022-03-09 00:35:30 +01:00
if _, err := io.Copy(mock.fileCont, mock.dataConn.conn); err != nil {
mock.t.Fatal(err)
}
mock.printfLine("226 Transfer Complete")
2019-04-10 20:20:50 +02:00
mock.closeDataConn()
}
func (mock *ftpMock) Addr() string {
return mock.listener.Addr().String()
}
// Closes the listening socket
func (mock *ftpMock) Close() {
mock.listener.Close()
}
// Helper to return a client connected to a mock server
func openConn(t *testing.T, addr string, options ...DialOption) (*ftpMock, *ServerConn) {
return openConnExt(t, addr, "no-time", options...)
}
func openConnExt(t *testing.T, addr, modtime string, options ...DialOption) (*ftpMock, *ServerConn) {
mock, err := newFtpMockExt(t, addr, modtime)
2022-08-18 01:24:40 +02:00
require.NoError(t, err)
2019-04-10 20:20:50 +02:00
defer mock.Close()
c, err := Dial(mock.Addr(), options...)
2022-08-18 01:24:40 +02:00
require.NoError(t, err)
2019-04-10 20:20:50 +02:00
err = c.Login("anonymous", "anonymous")
2022-08-18 01:24:40 +02:00
require.NoError(t, err)
2019-04-10 20:20:50 +02:00
return mock, c
}
// Helper to close a client connected to a mock server
func closeConn(t *testing.T, mock *ftpMock, c *ServerConn, commands []string) {
2020-10-20 20:16:16 +02:00
expected := []string{"USER", "PASS", "FEAT", "TYPE", "OPTS"}
2019-04-10 20:20:50 +02:00
expected = append(expected, commands...)
expected = append(expected, "QUIT")
if err := c.Quit(); err != nil {
t.Fatal(err)
}
// Wait for the connection to close
mock.Wait()
2022-08-18 01:24:40 +02:00
assert.Equal(t, expected, mock.commands, "unexpected sequence of commands")
2019-04-10 20:20:50 +02:00
}
func TestConn4(t *testing.T) {
mock, c := openConn(t, "127.0.0.1")
closeConn(t, mock, c, nil)
}
func TestConn6(t *testing.T) {
mock, c := openConn(t, "[::1]")
closeConn(t, mock, c, nil)
}