/*
 * Copyright 1994-2008 Sun Microsystems, Inc.  All Rights Reserved.
 * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
 *
 * This code is free software; you can redistribute it and/or modify it
 * under the terms of the GNU General Public License version 2 only, as
 * published by the Free Software Foundation.  Sun designates this
 * particular file as subject to the "Classpath" exception as provided
 * by Sun in the LICENSE file that accompanied this code.
 *
 * This code is distributed in the hope that it will be useful, but WITHOUT
 * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
 * FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License
 * version 2 for more details (a copy is included in the LICENSE file that
 * accompanied this code).
 *
 * You should have received a copy of the GNU General Public License version
 * 2 along with this work; if not, write to the Free Software Foundation,
 * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
 *
 * Please contact Sun Microsystems, Inc., 4150 Network Circle, Santa Clara,
 * CA 95054 USA or visit www.sun.com if you need additional information or
 * have any questions.
 */

package sun.net.ftp;

import java.util.StringTokenizer;
import java.util.regex.*;
import java.io.*;
import java.net.*;
import sun.net.TransferProtocolClient;
import sun.net.TelnetInputStream;
import sun.net.TelnetOutputStream;
import sun.misc.RegexpPool;
import java.security.AccessController;
import java.security.PrivilegedAction;

/**
 * This class implements the FTP client.
 *
 * @author      Jonathan Payne
 */

public class FtpClient extends TransferProtocolClient {
    public static final int FTP_PORT = 21;

    static int  FTP_SUCCESS = 1;
    static int  FTP_TRY_AGAIN = 2;
    static int  FTP_ERROR = 3;

    /** remember the ftp server name because we may need it */
    private String      serverName = null;

    /** socket for data transfer */
    private boolean     replyPending = false;
    private boolean     binaryMode = false;
    private boolean     loggedIn = false;

    /** regexp pool of hosts for which we should connect directly, not Proxy
     *  these are intialized from a property.
     */
    private static RegexpPool nonProxyHostsPool = null;

    /** The string soucre of nonProxyHostsPool
     */
    private static String nonProxyHostsSource = null;

    /** last command issued */
    String              command;

    /** The last reply code from the ftp daemon. */
    int                 lastReplyCode;

    /** Welcome message from the server, if any. */
    public String       welcomeMsg;


    /* these methods are used to determine whether ftp urls are sent to */
    /* an http server instead of using a direct connection to the */
    /* host. They aren't used directly here. */
    /**
     * @return if the networking layer should send ftp connections through
     *          a proxy
     */
    public static boolean getUseFtpProxy() {
        // if the ftp.proxyHost is set, use it!
        return (getFtpProxyHost() != null);
    }

    /**
     * @return the host to use, or null if none has been specified
     */
    public static String getFtpProxyHost() {
        return java.security.AccessController.doPrivileged(
            new java.security.PrivilegedAction<String>() {
            public String run() {
                String result = System.getProperty("ftp.proxyHost");
                if (result == null) {
                    result = System.getProperty("ftpProxyHost");
                }
                if (result == null) {
                    // as a last resort we use the general one if ftp.useProxy
                    // is true
                    if (Boolean.getBoolean("ftp.useProxy")) {
                    result = System.getProperty("proxyHost");
                    }
                }
                return result;
            }
        });
    }

    /**
     * @return the proxy port to use.  Will default reasonably if not set.
     */
    public static int getFtpProxyPort() {
        final int result[] = {80};
        java.security.AccessController.doPrivileged(
            new java.security.PrivilegedAction<Void>() {
                public Void run() {

                String tmp = System.getProperty("ftp.proxyPort");
                if (tmp == null) {
                    // for compatibility with 1.0.2
                    tmp = System.getProperty("ftpProxyPort");
                }
                if (tmp == null) {
                    // as a last resort we use the general one if ftp.useProxy
                    // is true
                    if (Boolean.getBoolean("ftp.useProxy")) {
                        tmp = System.getProperty("proxyPort");
                    }
                }
                if (tmp != null) {
                    result[0] = Integer.parseInt(tmp);
                }
                return null;
            }
        });
        return result[0];
    }

    public static boolean matchNonProxyHosts(String host) {
        synchronized (FtpClient.class) {
            String rawList = java.security.AccessController.doPrivileged(
                    new sun.security.action.GetPropertyAction("ftp.nonProxyHosts"));
            if (rawList == null) {
                nonProxyHostsPool = null;
            } else {
                if (!rawList.equals(nonProxyHostsSource)) {
                    RegexpPool pool = new RegexpPool();
                    StringTokenizer st = new StringTokenizer(rawList, "|", false);
                    try {
                        while (st.hasMoreTokens()) {
                            pool.add(st.nextToken().toLowerCase(), Boolean.TRUE);
                        }
                    } catch (sun.misc.REException ex) {
                        System.err.println("Error in http.nonProxyHosts system property: " + ex);
                    }
                    nonProxyHostsPool = pool;
                }
            }
            nonProxyHostsSource = rawList;
        }

        if (nonProxyHostsPool == null) {
            return false;
        }

        if (nonProxyHostsPool.match(host) != null) {
            return true;
        } else {
            return false;
        }
    }

    /**
     * issue the QUIT command to the FTP server and close the connection.
     *
     * @exception       FtpProtocolException if an error occured
     */
    public void closeServer() throws IOException {
        if (serverIsOpen()) {
            issueCommand("QUIT");
            super.closeServer();
        }
    }

    /**
     * Send a command to the FTP server.
     *
     * @param   cmd     String containing the command
     * @return          reply code
     *
     * @exception       FtpProtocolException if an error occured
     */
    protected int issueCommand(String cmd) throws IOException {
        command = cmd;

        int reply;

        while (replyPending) {
            replyPending = false;
            if (readReply() == FTP_ERROR)
                throw new FtpProtocolException("Error reading FTP pending reply\n");
        }
        do {
            sendServer(cmd + "\r\n");
            reply = readReply();
        } while (reply == FTP_TRY_AGAIN);
        return reply;
    }

    /**
     * Send a command to the FTP server and check for success.
     *
     * @param   cmd     String containing the command
     *
     * @exception       FtpProtocolException if an error occured
     */
    protected void issueCommandCheck(String cmd) throws IOException {
        if (issueCommand(cmd) != FTP_SUCCESS)
            throw new FtpProtocolException(cmd + ":" + getResponseString());
    }

    /**
     * Read the reply from the FTP server.
     *
     * @return          FTP_SUCCESS or FTP_ERROR depending on success
     * @exception       FtpProtocolException if an error occured
     */
    protected int readReply() throws IOException {
        lastReplyCode = readServerResponse();

        switch (lastReplyCode / 100) {
        case 1:
            replyPending = true;
            /* falls into ... */

        case 2:
        case 3:
            return FTP_SUCCESS;

        case 5:
            if (lastReplyCode == 530) {
                if (!loggedIn) {
                    throw new FtpLoginException("Not logged in");
                }
                return FTP_ERROR;
            }
            if (lastReplyCode == 550) {
                throw new FileNotFoundException(command + ": " + getResponseString());
            }
        }

        /* this statement is not reached */
        return FTP_ERROR;
    }

    /**
     * Tries to open a Data Connection in "PASSIVE" mode by issuing a EPSV or
     * PASV command then opening a Socket to the specified address & port
     *
     * @return          the opened socket
     * @exception       FtpProtocolException if an error occurs when issuing the
     *                  PASV command to the ftp server.
     */
    protected Socket openPassiveDataConnection() throws IOException {
        String serverAnswer;
        int port;
        InetSocketAddress dest = null;

        /**
         * Here is the idea:
         *
         * - First we want to try the new (and IPv6 compatible) EPSV command
         *   But since we want to be nice with NAT software, we'll issue the
         *   EPSV ALL cmd first.
         *   EPSV is documented in RFC2428
         * - If EPSV fails, then we fall back to the older, yet OK PASV command
         * - If PASV fails as well, then we throw an exception and the calling method
         *   will have to try the EPRT or PORT command
         */
        if (issueCommand("EPSV ALL") == FTP_SUCCESS) {
            // We can safely use EPSV commands
            if (issueCommand("EPSV") == FTP_ERROR)
                throw new FtpProtocolException("EPSV Failed: " + getResponseString());
            serverAnswer = getResponseString();

            // The response string from a EPSV command will contain the port number
            // the format will be :
            //  229 Entering Extended Passive Mode (|||58210|)
            //
            // So we'll use the regular expresions package to parse the output.

            Pattern p = Pattern.compile("^229 .* \\(\\|\\|\\|(\\d+)\\|\\)");
            Matcher m = p.matcher(serverAnswer);
            if (! m.find())
                throw new FtpProtocolException("EPSV failed : " + serverAnswer);
            // Yay! Let's extract the port number
            String s = m.group(1);
            port = Integer.parseInt(s);
            InetAddress add = serverSocket.getInetAddress();
            if (add != null) {
                dest = new InetSocketAddress(add, port);
            } else {
                // This means we used an Unresolved address to connect in
                // the first place. Most likely because the proxy is doing
                // the name resolution for us, so let's keep using unresolved
                // address.
                dest = InetSocketAddress.createUnresolved(serverName, port);
            }
        } else {
            // EPSV ALL failed, so Let's try the regular PASV cmd
            if (issueCommand("PASV") == FTP_ERROR)
                throw new FtpProtocolException("PASV failed: " + getResponseString());
            serverAnswer = getResponseString();

            // Let's parse the response String to get the IP & port to connect to
            // the String should be in the following format :
            //
            // 227 Entering Passive Mode (A1,A2,A3,A4,p1,p2)
            //
            // Note that the two parenthesis are optional
            //
            // The IP address is A1.A2.A3.A4 and the port is p1 * 256 + p2
            //
            // The regular expression is a bit more complex this time, because the
            // parenthesis are optionals and we have to use 3 groups.

            Pattern p = Pattern.compile("227 .* \\(?(\\d{1,3},\\d{1,3},\\d{1,3},\\d{1,3}),(\\d{1,3}),(\\d{1,3})\\)?");
            Matcher m = p.matcher(serverAnswer);
            if (! m.find())
                throw new FtpProtocolException("PASV failed : " + serverAnswer);
            // Get port number out of group 2 & 3
            port = Integer.parseInt(m.group(3)) + (Integer.parseInt(m.group(2)) << 8);
            // IP address is simple
            String s = m.group(1).replace(',','.');
            dest = new InetSocketAddress(s, port);
        }
        // Got everything, let's open the socket!
        Socket s;
        if (proxy != null) {
            if (proxy.type() == Proxy.Type.SOCKS) {
                s = AccessController.doPrivileged(
                    new PrivilegedAction<Socket>() {
                        public Socket run() {
                                  return new Socket(proxy);
                              }});
            } else
                s = new Socket(Proxy.NO_PROXY);
        } else
            s = new Socket();
        // Bind the socket to the same address as the control channel. This
        // is needed in case of multi-homed systems.
        s.bind(new InetSocketAddress(serverSocket.getLocalAddress(),0));
        if (connectTimeout >= 0) {
            s.connect(dest, connectTimeout);
        } else {
            if (defaultConnectTimeout > 0) {
                s.connect(dest, defaultConnectTimeout);
            } else {
                s.connect(dest);
            }
        }
        if (readTimeout >= 0)
            s.setSoTimeout(readTimeout);
        else
            if (defaultSoTimeout > 0) {
                s.setSoTimeout(defaultSoTimeout);
        }
        return s;
    }

    /**
     * Tries to open a Data Connection with the server. It will first try a passive
     * mode connection, then, if it fails, a more traditional PORT command
     *
     * @param   cmd     the command to execute (RETR, STOR, etc...)
     * @return          the opened socket
     *
     * @exception       FtpProtocolException if an error occurs when issuing the
     *                  PORT command to the ftp server.
     */
    protected Socket openDataConnection(String cmd) throws IOException {
        ServerSocket portSocket;
        Socket  clientSocket = null;
        String      portCmd;
        InetAddress myAddress;
        IOException e;

        // Let's try passive mode first
        try {
            clientSocket = openPassiveDataConnection();
        } catch (IOException ex) {
            clientSocket = null;
        }
        if (clientSocket != null) {
            // We did get a clientSocket, so the passive mode worked
            // Let's issue the command (GET, DIR, ...)
            try {
                if (issueCommand(cmd) == FTP_ERROR) {
                    clientSocket.close();
                    throw new FtpProtocolException(getResponseString());
                } else
                    return clientSocket;
            } catch (IOException ioe) {
                clientSocket.close();
                throw ioe;
            }
        }

        assert(clientSocket == null);

        // Passive mode failed, let's fall back to the good old "PORT"

        if (proxy != null && proxy.type() == Proxy.Type.SOCKS) {
            // We're behind a firewall and the passive mode fail,
            // since we can't accept a connection through SOCKS (yet)
            // throw an exception
            throw new FtpProtocolException("Passive mode failed");
        }
        // Bind the ServerSocket to the same address as the control channel
        // This is needed for multi-homed systems
        portSocket = new ServerSocket(0, 1, serverSocket.getLocalAddress());
        try {
            myAddress = portSocket.getInetAddress();
            if (myAddress.isAnyLocalAddress())
                myAddress = getLocalAddress();
            // Let's try the new, IPv6 compatible EPRT command
            // See RFC2428 for specifics
            // Some FTP servers (like the one on Solaris) are bugged, they
            // will accept the EPRT command but then, the subsequent command
            // (e.g. RETR) will fail, so we have to check BOTH results (the
            // EPRT cmd then the actual command) to decide wether we should
            // fall back on the older PORT command.
            portCmd = "EPRT |" +
                ((myAddress instanceof Inet6Address) ? "2" : "1") + "|" +
                myAddress.getHostAddress() +"|" +
                portSocket.getLocalPort()+"|";
            if (issueCommand(portCmd) == FTP_ERROR ||
                issueCommand(cmd) == FTP_ERROR) {
                // The EPRT command failed, let's fall back to good old PORT
                portCmd = "PORT ";
                byte[] addr = myAddress.getAddress();

                /* append host addr */
                for (int i = 0; i < addr.length; i++) {
                    portCmd = portCmd + (addr[i] & 0xFF) + ",";
                }

                /* append port number */
                portCmd = portCmd + ((portSocket.getLocalPort() >>> 8) & 0xff) + ","
                    + (portSocket.getLocalPort() & 0xff);
                if (issueCommand(portCmd) == FTP_ERROR) {
                    e = new FtpProtocolException("PORT :" + getResponseString());
                    throw e;
                }
                if (issueCommand(cmd) == FTP_ERROR) {
                    e = new FtpProtocolException(cmd + ":" + getResponseString());
                    throw e;
                }
            }
            // Either the EPRT or the PORT command was successful
            // Let's create the client socket
            if (connectTimeout >= 0) {
                portSocket.setSoTimeout(connectTimeout);
            } else {
                if (defaultConnectTimeout > 0)
                    portSocket.setSoTimeout(defaultConnectTimeout);
            }
            clientSocket = portSocket.accept();
            if (readTimeout >= 0)
                clientSocket.setSoTimeout(readTimeout);
            else {
                if (defaultSoTimeout > 0)
                    clientSocket.setSoTimeout(defaultSoTimeout);
            }
        } finally {
            portSocket.close();
        }

        return clientSocket;
    }

    /* public methods */

    /**
     * Open a FTP connection to host <i>host</i>.
     *
     * @param   host    The hostname of the ftp server
     *
     * @exception       FtpProtocolException if connection fails
     */
    public void openServer(String host) throws IOException {
        openServer(host, FTP_PORT);
    }

    /**
     * Open a FTP connection to host <i>host</i> on port <i>port</i>.
     *
     * @param   host    the hostname of the ftp server
     * @param   port    the port to connect to (usually 21)
     *
     * @exception       FtpProtocolException if connection fails
     */
    public void openServer(String host, int port) throws IOException {
        this.serverName = host;
        super.openServer(host, port);
        if (readReply() == FTP_ERROR)
            throw new FtpProtocolException("Welcome message: " +
                                           getResponseString());
    }


    /**
     * login user to a host with username <i>user</i> and password
     * <i>password</i>
     *
     * @param   user            Username to use at login
     * @param   password        Password to use at login or null of none is needed
     *
     * @exception       FtpLoginException if login is unsuccesful
     */
    public void login(String user, String password) throws IOException {
        if (!serverIsOpen())
            throw new FtpLoginException("not connected to host");
        if (user == null || user.length() == 0)
            return;
        if (issueCommand("USER " + user) == FTP_ERROR)
            throw new FtpLoginException("user " + user + " : " + getResponseString());
        /*
         * Checks for "331 User name okay, need password." answer
         */

        if (lastReplyCode == 331)
            if ((password == null) || (password.length() == 0) ||
                (issueCommand("PASS " + password) == FTP_ERROR))
                throw new FtpLoginException("password: " + getResponseString());

        // keep the welcome message around so we can
        // put it in the resulting HTML page.
        String l;
        StringBuffer sb = new StringBuffer();
        for (int i = 0; i < serverResponse.size(); i++) {
            l = (String)serverResponse.elementAt(i);
            if (l != null) {
                if (l.length() >= 4 && l.startsWith("230")) {
                    // get rid of the "230-" prefix
                    l = l.substring(4);
                }
                sb.append(l);
            }
        }
        welcomeMsg = sb.toString();
        loggedIn = true;
    }

    /**
     * GET a file from the FTP server
     *
     * @param   filename        name of the file to retrieve
     * @return  the <code>InputStream</code> to read the file from
     *
     * @exception       FileNotFoundException if the file can't be opened
     */
    public TelnetInputStream get(String filename) throws IOException {
        Socket  s;

        try {
            s = openDataConnection("RETR " + filename);
        } catch (FileNotFoundException fileException) {
            /* Well, "/" might not be the file delimitor for this
               particular ftp server, so let's try a series of
               "cd" commands to get to the right place. */
            /* But don't try this if there are no '/' in the path */
            if (filename.indexOf('/') == -1)
                throw fileException;

            StringTokenizer t = new StringTokenizer(filename, "/");
            String          pathElement = null;

            while (t.hasMoreElements()) {
                pathElement = t.nextToken();

                if (!t.hasMoreElements()) {
                    /* This is the file component.  Look it up now. */
                    break;
                }
                try {
                    cd(pathElement);
                } catch (FtpProtocolException e) {
                    /* Giving up. */
                    throw fileException;
                }
            }
            if (pathElement != null) {
                s = openDataConnection("RETR " + pathElement);
            } else {
                throw fileException;
            }
        }

        return new TelnetInputStream(s.getInputStream(), binaryMode);
    }

    /**
     * PUT a file to the FTP server
     *
     * @param   filename        name of the file to store
     * @return  the <code>OutputStream</code> to write the file to
     *
     */
    public TelnetOutputStream put(String filename) throws IOException {
        Socket s = openDataConnection("STOR " + filename);
        TelnetOutputStream out = new TelnetOutputStream(s.getOutputStream(), binaryMode);
        if (!binaryMode)
            out.setStickyCRLF(true);
        return out;
    }

    /**
     * Append to a file on the FTP server
     *
     * @param   filename        name of the file to append to
     * @return  the <code>OutputStream</code> to write the file to
     *
     */
    public TelnetOutputStream append(String filename) throws IOException {
        Socket s = openDataConnection("APPE " + filename);
        TelnetOutputStream out = new TelnetOutputStream(s.getOutputStream(), binaryMode);
        if (!binaryMode)
            out.setStickyCRLF(true);

        return out;
    }

    /**
     * LIST files in the current directory on a remote FTP server
     *
     * @return  the <code>InputStream</code> to read the list from
     *
     */
    public TelnetInputStream list() throws IOException {
        Socket s = openDataConnection("LIST");

        return new TelnetInputStream(s.getInputStream(), binaryMode);
    }

    /**
     * List (NLST) file names on a remote FTP server
     *
     * @param   path    pathname to the directory to list, null for current
     *                  directory
     * @return  the <code>InputStream</code> to read the list from
     * @exception       <code>FtpProtocolException</code>
     */
    public TelnetInputStream nameList(String path) throws IOException {
        Socket s;

        if (path != null)
            s = openDataConnection("NLST " + path);
        else
            s = openDataConnection("NLST");
        return new TelnetInputStream(s.getInputStream(), binaryMode);
    }

    /**
     * CD to a specific directory on a remote FTP server
     *
     * @param   remoteDirectory path of the directory to CD to
     *
     * @exception       <code>FtpProtocolException</code>
     */
    public void cd(String remoteDirectory) throws IOException {
        if (remoteDirectory == null ||
            "".equals(remoteDirectory))
            return;
        issueCommandCheck("CWD " + remoteDirectory);
    }

    /**
     * CD to the parent directory on a remote FTP server
     *
     */
    public void cdUp() throws IOException {
        issueCommandCheck("CDUP");
    }

    /**
     * Print working directory of remote FTP server
     *
     * @exception FtpProtocolException if the command fails
     */
    public String pwd() throws IOException {
        String answ;

        issueCommandCheck("PWD");
        /*
         * answer will be of the following format :
         *
         * 257 "/" is current directory.
         */
        answ = getResponseString();
        if (!answ.startsWith("257"))
            throw new FtpProtocolException("PWD failed. " + answ);
        return answ.substring(5, answ.lastIndexOf('"'));
    }

    /**
     * Set transfer type to 'I'
     *
     * @exception FtpProtocolException if the command fails
     */
    public void binary() throws IOException {
        issueCommandCheck("TYPE I");
        binaryMode = true;
    }

    /**
     * Set transfer type to 'A'
     *
     * @exception FtpProtocolException if the command fails
     */
    public void ascii() throws IOException {
        issueCommandCheck("TYPE A");
        binaryMode = false;
    }

    /**
     * Rename a file on the ftp server
     *
     * @exception FtpProtocolException if the command fails
     */
    public void rename(String from, String to) throws IOException {
        issueCommandCheck("RNFR " + from);
        issueCommandCheck("RNTO " + to);
    }

    /**
     * Get the "System string" from the FTP server
     *
     * @exception       FtpProtocolException if it fails
     */
    public String system() throws IOException {
        String answ;
        issueCommandCheck("SYST");
        answ = getResponseString();
        if (!answ.startsWith("215"))
            throw new FtpProtocolException("SYST failed." + answ);
        return answ.substring(4); // Skip "215 "
    }

    /**
     * Send a No-operation command. It's usefull for testing the connection status
     *
     * @exception FtpProtocolException if the command fails
     */
    public void noop() throws IOException {
        issueCommandCheck("NOOP");
    }

    /**
     * Reinitialize the USER parameters on the FTp server
     *
     * @exception FtpProtocolException if the command fails
     */
    public void reInit() throws IOException {
        issueCommandCheck("REIN");
        loggedIn = false;
    }

    /**
     * New FTP client connected to host <i>host</i>.
     *
     * @param   host    Hostname of the FTP server
     *
     * @exception FtpProtocolException if the connection fails
     */
    public FtpClient(String host) throws IOException {
        super();
        openServer(host, FTP_PORT);
    }

    /**
     * New FTP client connected to host <i>host</i>, port <i>port</i>.
     *
     * @param   host    Hostname of the FTP server
     * @param   port    port number to connect to (usually 21)
     *
     * @exception FtpProtocolException if the connection fails
     */
    public FtpClient(String host, int port) throws IOException {
        super();
        openServer(host, port);
    }

    /** Create an uninitialized FTP client. */
    public FtpClient() {}

    public FtpClient(Proxy p) {
        proxy = p;
    }

    protected void finalize() throws IOException {
        /**
         * Do not call the "normal" closeServer() as we want finalization
         * to be as efficient as possible
         */
        if (serverIsOpen())
            super.closeServer();
    }

}
