// 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)) return net.DialTimeout("tcp", addr, c.timeout) } // 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 } return c.conn.ReadResponse(expected) } // 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 } var errUnsupportedListLine = errors.New("Unsupported LIST line") // parseRFC3659ListLine parses the style of directory line defined in RFC 3659. func parseRFC3659ListLine(line string) (*Entry, error) { iSemicolon := strings.Index(line, ";") iWhitespace := strings.Index(line, " ") if iSemicolon < 0 || iSemicolon > iWhitespace { return nil, errUnsupportedListLine } e := &Entry{ Name: line[iWhitespace+1:], } for _, field := range strings.Split(line[:iWhitespace-1], ";") { i := strings.Index(field, "=") if i < 1 { return nil, errUnsupportedListLine } key := field[:i] value := field[i+1:] switch key { case "modify": var err error e.Time, err = time.Parse("20060102150405", value) if err != nil { return nil, err } case "type": switch value { case "dir", "cdir", "pdir": e.Type = EntryTypeFolder case "file": e.Type = EntryTypeFile } case "size": e.setSize(value) } } return e, nil } // parseLsListLine parses a directory line in a format based on the output of // the UNIX ls command. func parseLsListLine(line string) (*Entry, error) { 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, errUnsupportedListLine } e := &Entry{} switch fields[0][0] { case '-': e.Type = EntryTypeFile if err := e.setSize(fields[4]); err != nil { return nil, err } case 'd': e.Type = EntryTypeFolder case 'l': e.Type = EntryTypeLink default: return nil, errors.New("Unknown entry type") } if err := e.setTime(fields[5:8]); err != nil { return nil, err } e.Name = strings.Join(fields[8:], " ") return e, nil } var dirTimeFormats = []string{ "01-02-06 03:04PM", "2006-01-02 15:04", } // parseDirListLine parses a directory line in a format based on the output of // the MS-DOS DIR command. func parseDirListLine(line string) (*Entry, error) { e := &Entry{} var err error // Try various time formats that DIR might use, and stop when one works. for _, format := range dirTimeFormats { if len(line) > len(format) { e.Time, err = time.Parse(format, line[:len(format)]) if err == nil { line = line[len(format):] break } } } if err != nil { // None of the time formats worked. return nil, errUnsupportedListLine } line = strings.TrimLeft(line, " ") if strings.HasPrefix(line, "