b33f6e0005
We can not use the DNS name to open the data connection, as we might get another IP than the current host from the DNS resolution.
610 lines
14 KiB
Go
610 lines
14 KiB
Go
// Package ftp implements a FTP client as described in RFC 959.
|
|
package ftp
|
|
|
|
import (
|
|
"bufio"
|
|
"errors"
|
|
"io"
|
|
"net"
|
|
"net/textproto"
|
|
"strconv"
|
|
"strings"
|
|
"time"
|
|
)
|
|
|
|
// EntryType describes the different types of an Entry.
|
|
type EntryType int
|
|
|
|
// The differents types of an Entry
|
|
const (
|
|
EntryTypeFile EntryType = iota
|
|
EntryTypeFolder
|
|
EntryTypeLink
|
|
)
|
|
|
|
// ServerConn represents the connection to a remote FTP server.
|
|
type ServerConn struct {
|
|
conn *textproto.Conn
|
|
host string
|
|
timeout time.Duration
|
|
features map[string]string
|
|
}
|
|
|
|
// Entry describes a file and is returned by List().
|
|
type Entry struct {
|
|
Name string
|
|
Type EntryType
|
|
Size uint64
|
|
Time time.Time
|
|
}
|
|
|
|
// response represent a data-connection
|
|
type response struct {
|
|
conn net.Conn
|
|
c *ServerConn
|
|
}
|
|
|
|
// Connect is an alias to Dial, for backward compatibility
|
|
func Connect(addr string) (*ServerConn, error) {
|
|
return Dial(addr)
|
|
}
|
|
|
|
// Dial is like DialTimeout with no timeout
|
|
func Dial(addr string) (*ServerConn, error) {
|
|
return DialTimeout(addr, 0)
|
|
}
|
|
|
|
// DialTimeout initializes the connection to the specified ftp server address.
|
|
//
|
|
// It is generally followed by a call to Login() as most FTP commands require
|
|
// an authenticated user.
|
|
func DialTimeout(addr string, timeout time.Duration) (*ServerConn, error) {
|
|
tconn, err := net.DialTimeout("tcp", addr, timeout)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
// Use the resolved IP address in case addr contains a domain name
|
|
// If we use the domain name, we might not resolve to the same IP.
|
|
remoteAddr := tconn.RemoteAddr().String()
|
|
host, _, err := net.SplitHostPort(remoteAddr)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
conn := textproto.NewConn(tconn)
|
|
|
|
c := &ServerConn{
|
|
conn: conn,
|
|
host: host,
|
|
timeout: timeout,
|
|
features: make(map[string]string),
|
|
}
|
|
|
|
_, _, err = c.conn.ReadResponse(StatusReady)
|
|
if err != nil {
|
|
c.Quit()
|
|
return nil, err
|
|
}
|
|
|
|
err = c.feat()
|
|
if err != nil {
|
|
c.Quit()
|
|
return nil, err
|
|
}
|
|
|
|
return c, nil
|
|
}
|
|
|
|
// Login authenticates the client with specified user and password.
|
|
//
|
|
// "anonymous"/"anonymous" is a common user/password scheme for FTP servers
|
|
// that allows anonymous read-only accounts.
|
|
func (c *ServerConn) Login(user, password string) error {
|
|
code, message, err := c.cmd(-1, "USER %s", user)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
switch code {
|
|
case StatusLoggedIn:
|
|
case StatusUserOK:
|
|
_, _, err = c.cmd(StatusLoggedIn, "PASS %s", password)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
default:
|
|
return errors.New(message)
|
|
}
|
|
|
|
// Switch to binary mode
|
|
_, _, err = c.cmd(StatusCommandOK, "TYPE I")
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// feat issues a FEAT FTP command to list the additional commands supported by
|
|
// the remote FTP server.
|
|
// FEAT is described in RFC 2389
|
|
func (c *ServerConn) feat() error {
|
|
code, message, err := c.cmd(-1, "FEAT")
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
if code != StatusSystem {
|
|
// The server does not support the FEAT command. This is not an
|
|
// error: we consider that there is no additional feature.
|
|
return nil
|
|
}
|
|
|
|
lines := strings.Split(message, "\n")
|
|
for _, line := range lines {
|
|
if !strings.HasPrefix(line, " ") {
|
|
continue
|
|
}
|
|
|
|
line = strings.TrimSpace(line)
|
|
featureElements := strings.SplitN(line, " ", 2)
|
|
|
|
command := featureElements[0]
|
|
|
|
var commandDesc string
|
|
if len(featureElements) == 2 {
|
|
commandDesc = featureElements[1]
|
|
}
|
|
|
|
c.features[command] = commandDesc
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// epsv issues an "EPSV" command to get a port number for a data connection.
|
|
func (c *ServerConn) epsv() (port int, err error) {
|
|
_, line, err := c.cmd(StatusExtendedPassiveMode, "EPSV")
|
|
if err != nil {
|
|
return
|
|
}
|
|
|
|
start := strings.Index(line, "|||")
|
|
end := strings.LastIndex(line, "|")
|
|
if start == -1 || end == -1 {
|
|
err = errors.New("Invalid EPSV response format")
|
|
return
|
|
}
|
|
port, err = strconv.Atoi(line[start+3 : end])
|
|
return
|
|
}
|
|
|
|
// pasv issues a "PASV" command to get a port number for a data connection.
|
|
func (c *ServerConn) pasv() (port int, err error) {
|
|
_, line, err := c.cmd(StatusPassiveMode, "PASV")
|
|
if err != nil {
|
|
return
|
|
}
|
|
|
|
// PASV response format : 227 Entering Passive Mode (h1,h2,h3,h4,p1,p2).
|
|
start := strings.Index(line, "(")
|
|
end := strings.LastIndex(line, ")")
|
|
if start == -1 || end == -1 {
|
|
err = errors.New("Invalid PASV response format")
|
|
return
|
|
}
|
|
|
|
// We have to split the response string
|
|
pasvData := strings.Split(line[start+1:end], ",")
|
|
// Let's compute the port number
|
|
portPart1, err1 := strconv.Atoi(pasvData[4])
|
|
if err1 != nil {
|
|
err = err1
|
|
return
|
|
}
|
|
|
|
portPart2, err2 := strconv.Atoi(pasvData[5])
|
|
if err2 != nil {
|
|
err = err2
|
|
return
|
|
}
|
|
|
|
// Recompose port
|
|
port = portPart1*256 + portPart2
|
|
return
|
|
}
|
|
|
|
// openDataConn creates a new FTP data connection.
|
|
func (c *ServerConn) openDataConn() (net.Conn, error) {
|
|
var port int
|
|
var err error
|
|
|
|
// If features contains nat6 or EPSV => EPSV
|
|
// else -> PASV
|
|
_, nat6Supported := c.features["nat6"]
|
|
_, epsvSupported := c.features["EPSV"]
|
|
|
|
if !nat6Supported && !epsvSupported {
|
|
port, _ = c.pasv()
|
|
}
|
|
if port == 0 {
|
|
port, err = c.epsv()
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
}
|
|
|
|
// Build the new net address string
|
|
addr := net.JoinHostPort(c.host, strconv.Itoa(port))
|
|
|
|
conn, err := net.DialTimeout("tcp", addr, c.timeout)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
return conn, nil
|
|
}
|
|
|
|
// cmd is a helper function to execute a command and check for the expected FTP
|
|
// return code
|
|
func (c *ServerConn) cmd(expected int, format string, args ...interface{}) (int, string, error) {
|
|
_, err := c.conn.Cmd(format, args...)
|
|
if err != nil {
|
|
return 0, "", err
|
|
}
|
|
|
|
code, line, err := c.conn.ReadResponse(expected)
|
|
return code, line, err
|
|
}
|
|
|
|
// cmdDataConnFrom executes a command which require a FTP data connection.
|
|
// Issues a REST FTP command to specify the number of bytes to skip for the transfer.
|
|
func (c *ServerConn) cmdDataConnFrom(offset uint64, format string, args ...interface{}) (net.Conn, error) {
|
|
conn, err := c.openDataConn()
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
if offset != 0 {
|
|
_, _, err := c.cmd(StatusRequestFilePending, "REST %d", offset)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
}
|
|
|
|
_, err = c.conn.Cmd(format, args...)
|
|
if err != nil {
|
|
conn.Close()
|
|
return nil, err
|
|
}
|
|
|
|
code, msg, err := c.conn.ReadResponse(-1)
|
|
if err != nil {
|
|
conn.Close()
|
|
return nil, err
|
|
}
|
|
if code != StatusAlreadyOpen && code != StatusAboutToSend {
|
|
conn.Close()
|
|
return nil, &textproto.Error{Code: code, Msg: msg}
|
|
}
|
|
|
|
return conn, nil
|
|
}
|
|
|
|
// parseListLine parses the various non-standard format returned by the LIST
|
|
// FTP command.
|
|
func parseListLine(line string) (*Entry, error) {
|
|
var err error
|
|
|
|
// RFC3659 style
|
|
if i := strings.Index(line, ";"); i > 0 && i < strings.Index(line, " ") {
|
|
e := &Entry{}
|
|
arr := strings.Split(line, "; ")
|
|
e.Name = arr[1]
|
|
|
|
for _, field := range strings.Split(arr[0], ";") {
|
|
i := strings.Index(field, "=")
|
|
if i < 1 {
|
|
return nil, errors.New("Unsupported LIST line")
|
|
}
|
|
|
|
key := field[:i]
|
|
value := field[i+1:]
|
|
|
|
switch key {
|
|
case "modify":
|
|
e.Time, _ = time.Parse("20060102150405", value)
|
|
case "type":
|
|
switch value {
|
|
case "dir", "cdir", "pdir":
|
|
e.Type = EntryTypeFolder
|
|
case "file":
|
|
e.Type = EntryTypeFile
|
|
}
|
|
case "size":
|
|
e.Size, _ = strconv.ParseUint(value, 0, 64)
|
|
}
|
|
}
|
|
return e, nil
|
|
|
|
} else {
|
|
fields := strings.Fields(line)
|
|
if len(fields) >= 7 && fields[1] == "folder" && fields[2] == "0" {
|
|
e := &Entry{
|
|
Type: EntryTypeFolder,
|
|
Name: strings.Join(fields[6:], " "),
|
|
}
|
|
if err = e.setTime(fields[3:6]); err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
return e, nil
|
|
}
|
|
|
|
if fields[1] == "0" {
|
|
e := &Entry{
|
|
Type: EntryTypeFile,
|
|
Name: strings.Join(fields[7:], " "),
|
|
}
|
|
|
|
if err = e.setSize(fields[2]); err != nil {
|
|
return nil, err
|
|
}
|
|
if err = e.setTime(fields[4:7]); err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
return e, nil
|
|
}
|
|
|
|
if len(fields) < 9 {
|
|
return nil, errors.New("Unsupported LIST line")
|
|
}
|
|
|
|
e := &Entry{}
|
|
switch fields[0][0] {
|
|
case '-':
|
|
e.Type = EntryTypeFile
|
|
case 'd':
|
|
e.Type = EntryTypeFolder
|
|
case 'l':
|
|
e.Type = EntryTypeLink
|
|
default:
|
|
return nil, errors.New("Unknown entry type")
|
|
}
|
|
|
|
if e.Type == EntryTypeFile {
|
|
if err = e.setSize(fields[4]); err != nil {
|
|
return nil, err
|
|
}
|
|
}
|
|
|
|
if err = e.setTime(fields[5:8]); err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
e.Name = strings.Join(fields[8:], " ")
|
|
return e, nil
|
|
}
|
|
}
|
|
|
|
func (e *Entry) setSize(str string) (err error) {
|
|
e.Size, err = strconv.ParseUint(str, 10, 0)
|
|
return
|
|
}
|
|
|
|
func (e *Entry) setTime(fields []string) (err error) {
|
|
var timeStr string
|
|
if strings.Contains(fields[2], ":") { // this year
|
|
thisYear, _, _ := time.Now().Date()
|
|
timeStr = fields[1] + " " + fields[0] + " " + strconv.Itoa(thisYear)[2:4] + " " + fields[2] + " GMT"
|
|
} else { // not this year
|
|
if len(fields[2]) != 4 {
|
|
return errors.New("Invalid year format in time string")
|
|
}
|
|
timeStr = fields[1] + " " + fields[0] + " " + fields[2][2:4] + " " + "00:00" + " GMT"
|
|
}
|
|
e.Time, err = time.Parse("_2 Jan 06 15:04 MST", timeStr)
|
|
return
|
|
}
|
|
|
|
// NameList issues an NLST FTP command.
|
|
func (c *ServerConn) NameList(path string) (entries []string, err error) {
|
|
conn, err := c.cmdDataConnFrom(0, "NLST %s", path)
|
|
if err != nil {
|
|
return
|
|
}
|
|
|
|
r := &response{conn, c}
|
|
defer r.Close()
|
|
|
|
scanner := bufio.NewScanner(r)
|
|
for scanner.Scan() {
|
|
entries = append(entries, scanner.Text())
|
|
}
|
|
if err = scanner.Err(); err != nil {
|
|
return entries, err
|
|
}
|
|
return
|
|
}
|
|
|
|
// List issues a LIST FTP command.
|
|
func (c *ServerConn) List(path string) (entries []*Entry, err error) {
|
|
conn, err := c.cmdDataConnFrom(0, "LIST %s", path)
|
|
if err != nil {
|
|
return
|
|
}
|
|
|
|
r := &response{conn, c}
|
|
defer r.Close()
|
|
|
|
bio := bufio.NewReader(r)
|
|
for {
|
|
line, e := bio.ReadString('\n')
|
|
if e == io.EOF {
|
|
break
|
|
} else if e != nil {
|
|
return nil, e
|
|
}
|
|
entry, err := parseListLine(line)
|
|
if err == nil {
|
|
entries = append(entries, entry)
|
|
}
|
|
}
|
|
return
|
|
}
|
|
|
|
// ChangeDir issues a CWD FTP command, which changes the current directory to
|
|
// the specified path.
|
|
func (c *ServerConn) ChangeDir(path string) error {
|
|
_, _, err := c.cmd(StatusRequestedFileActionOK, "CWD %s", path)
|
|
return err
|
|
}
|
|
|
|
// ChangeDirToParent issues a CDUP FTP command, which changes the current
|
|
// directory to the parent directory. This is similar to a call to ChangeDir
|
|
// with a path set to "..".
|
|
func (c *ServerConn) ChangeDirToParent() error {
|
|
_, _, err := c.cmd(StatusRequestedFileActionOK, "CDUP")
|
|
return err
|
|
}
|
|
|
|
// CurrentDir issues a PWD FTP command, which Returns the path of the current
|
|
// directory.
|
|
func (c *ServerConn) CurrentDir() (string, error) {
|
|
_, msg, err := c.cmd(StatusPathCreated, "PWD")
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
|
|
start := strings.Index(msg, "\"")
|
|
end := strings.LastIndex(msg, "\"")
|
|
|
|
if start == -1 || end == -1 {
|
|
return "", errors.New("Unsuported PWD response format")
|
|
}
|
|
|
|
return msg[start+1 : end], nil
|
|
}
|
|
|
|
// Retr issues a RETR FTP command to fetch the specified file from the remote
|
|
// FTP server.
|
|
//
|
|
// The returned ReadCloser must be closed to cleanup the FTP data connection.
|
|
func (c *ServerConn) Retr(path string) (io.ReadCloser, error) {
|
|
return c.RetrFrom(path, 0)
|
|
}
|
|
|
|
// Retr issues a RETR FTP command to fetch the specified file from the remote
|
|
// FTP server, the server will not send the offset first bytes of the file.
|
|
//
|
|
// The returned ReadCloser must be closed to cleanup the FTP data connection.
|
|
func (c *ServerConn) RetrFrom(path string, offset uint64) (io.ReadCloser, error) {
|
|
conn, err := c.cmdDataConnFrom(offset, "RETR %s", path)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
r := &response{conn, c}
|
|
return r, nil
|
|
}
|
|
|
|
// Stor issues a STOR FTP command to store a file to the remote FTP server.
|
|
// Stor creates the specified file with the content of the io.Reader.
|
|
//
|
|
// Hint: io.Pipe() can be used if an io.Writer is required.
|
|
func (c *ServerConn) Stor(path string, r io.Reader) error {
|
|
return c.StorFrom(path, r, 0)
|
|
}
|
|
|
|
// Stor issues a STOR FTP command to store a file to the remote FTP server.
|
|
// Stor creates the specified file with the content of the io.Reader, writing
|
|
// on the server will start at the given file offset.
|
|
//
|
|
// Hint: io.Pipe() can be used if an io.Writer is required.
|
|
func (c *ServerConn) StorFrom(path string, r io.Reader, offset uint64) error {
|
|
conn, err := c.cmdDataConnFrom(offset, "STOR %s", path)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
_, err = io.Copy(conn, r)
|
|
conn.Close()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
_, _, err = c.conn.ReadResponse(StatusClosingDataConnection)
|
|
return err
|
|
}
|
|
|
|
// Rename renames a file on the remote FTP server.
|
|
func (c *ServerConn) Rename(from, to string) error {
|
|
_, _, err := c.cmd(StatusRequestFilePending, "RNFR %s", from)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
_, _, err = c.cmd(StatusRequestedFileActionOK, "RNTO %s", to)
|
|
return err
|
|
}
|
|
|
|
// Delete issues a DELE FTP command to delete the specified file from the
|
|
// remote FTP server.
|
|
func (c *ServerConn) Delete(path string) error {
|
|
_, _, err := c.cmd(StatusRequestedFileActionOK, "DELE %s", path)
|
|
return err
|
|
}
|
|
|
|
// MakeDir issues a MKD FTP command to create the specified directory on the
|
|
// remote FTP server.
|
|
func (c *ServerConn) MakeDir(path string) error {
|
|
_, _, err := c.cmd(StatusPathCreated, "MKD %s", path)
|
|
return err
|
|
}
|
|
|
|
// RemoveDir issues a RMD FTP command to remove the specified directory from
|
|
// the remote FTP server.
|
|
func (c *ServerConn) RemoveDir(path string) error {
|
|
_, _, err := c.cmd(StatusRequestedFileActionOK, "RMD %s", path)
|
|
return err
|
|
}
|
|
|
|
// NoOp issues a NOOP FTP command.
|
|
// NOOP has no effects and is usually used to prevent the remote FTP server to
|
|
// close the otherwise idle connection.
|
|
func (c *ServerConn) NoOp() error {
|
|
_, _, err := c.cmd(StatusCommandOK, "NOOP")
|
|
return err
|
|
}
|
|
|
|
// Logout issues a REIN FTP command to logout the current user.
|
|
func (c *ServerConn) Logout() error {
|
|
_, _, err := c.cmd(StatusReady, "REIN")
|
|
return err
|
|
}
|
|
|
|
// Quit issues a QUIT FTP command to properly close the connection from the
|
|
// remote FTP server.
|
|
func (c *ServerConn) Quit() error {
|
|
c.conn.Cmd("QUIT")
|
|
return c.conn.Close()
|
|
}
|
|
|
|
// Read implements the io.Reader interface on a FTP data connection.
|
|
func (r *response) Read(buf []byte) (int, error) {
|
|
n, err := r.conn.Read(buf)
|
|
return n, err
|
|
}
|
|
|
|
// Close implements the io.Closer interface on a FTP data connection.
|
|
func (r *response) Close() error {
|
|
err := r.conn.Close()
|
|
_, _, err2 := r.c.conn.ReadResponse(StatusClosingDataConnection)
|
|
if err2 != nil {
|
|
err = err2
|
|
}
|
|
return err
|
|
}
|