Package cu.ftpd

Source Code of cu.ftpd.Connection

/**
* Copyright (c) 2007, Markus Jevring <markus@jevring.net>
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions
* are met:
* 1. Redistributions of source code must retain the above copyright
*    notice, this list of conditions and the following disclaimer.
* 2. Redistributions in binary form must reproduce the above copyright
*    notice, this list of conditions and the following disclaimer in the
*    documentation and/or other materials provided with the distribution.
* 3. The names of the contributors may not be used to endorse or promote
*    products derived from this software without specific prior written
*    permission.
*
* THIS SOFTWARE IS PROVIDED BY THE REGENTS AND CONTRIBUTORS ``AS IS'' AND
* ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
* IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
* ARE DISCLAIMED.  IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE LIABLE
* FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
* DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS
* OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
* HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
* LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY
* OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
* SUCH DAMAGE.
*
*/

package cu.ftpd;

import cu.authentication.AuthenticationRequest;
import cu.ftpd.commands.site.SiteCommandHandler;
import cu.ftpd.commands.transfer.CommandPASV;
import cu.ftpd.commands.transfer.CommandRETR;
import cu.ftpd.commands.transfer.CommandSTOR;
import cu.ftpd.commands.transfer.DataConnectionListing;
import cu.ftpd.commands.transfer.TransferController;
import cu.ftpd.events.Event;
import cu.ftpd.events.EventFactory;
import cu.ftpd.filesystem.FileSystem;
import cu.ftpd.filesystem.PermissionException;
import cu.ftpd.filesystem.permissions.PermissionConfigurationException;
import cu.ftpd.filesystem.permissions.PermissionDeniedException;
import cu.ftpd.logging.Formatter;
import cu.ftpd.logging.Logging;
import cu.ftpd.user.User;
import cu.ftpd.user.UserPermission;
import cu.ftpd.user.groups.NoSuchGroupException;
import cu.ftpd.user.userbases.AuthenticationResponses;
import cu.ftpd.user.userbases.NoSuchUserException;
import cu.ident.IdentClient;
import cu.ssl.DefaultSSLSocketFactory;
import java.io.BufferedReader;
import java.io.BufferedWriter;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.OutputStreamWriter;
import java.io.PrintWriter;
import java.net.Inet4Address;
import java.net.Inet6Address;
import java.net.InetAddress;
import java.net.InetSocketAddress;
import java.net.NetworkInterface;
import java.net.ServerSocket;
import java.net.Socket;
import java.net.SocketException;
import java.net.SocketTimeoutException;
import java.net.UnknownHostException;
import java.security.AccessControlException;
import java.security.NoSuchAlgorithmException;
import java.text.ParseException;
import java.util.Date;
import java.util.Enumeration;
import java.util.List;
import java.util.concurrent.Semaphore;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import javax.net.ssl.SSLSocket;

/**
* @author Markus Jevring
* @since 2007-maj-07 : 18:52:43
* @version $Id: Connection.java 312 2011-09-03 16:00:17Z jevring $
*/
public class Connection extends Thread {
    /**
     * Note: this *must* be a semaphore. It cannot be a lock, as it is taken on one
     * thread and released on another.
     */
    public final Semaphore dataConnectionSemaphore = new Semaphore(1);
 

  public enum DataConnectionProtocolFamily {
        IPV4, IPV6, DEFAULT
    }
    protected final Pattern checksumParameters = Pattern.compile("(.+?)(?:\\s(\\d+))?(?:\\s(\\d+))?"); // yay for reluctant quantifiers
    protected final long connectionId;
    protected Socket controlConnection;
    protected Socket dataConnection;
    protected ServerSocket passiveServerSocket;
    protected PrintWriter controlOut;
    protected BufferedReader controlIn;
    protected boolean encryptedDataConnection = false;
    protected User user;
    protected FileSystem fs;
    protected long timeOfLastCommand;
    protected String lastCommand = "";
    protected boolean sscn = false;
    /**
     * cpsv has the same semantic as sscn, however since sscn can be set explicitly,
     * we need a special value to indicate that cpsv was used. This way cpsv can be
     * reset when a PORT command is received. This was due to a bug that was discovered
     * that involved only being able to transfer using SSL in one direction when using
     * CPSV but not SSCN.
     */
    protected boolean cpsv = false;
    protected String client = "unknown";
    protected int xdupe = 0;
    protected String ident;
    protected boolean useEpsvOnly = false;
    protected final SiteCommandHandler siteCommandHandler;
    protected AuthenticationRequest auth;
    protected boolean shuttingDown = false;
    protected boolean extendedEpsvResponse = false;
    private TransferController transfer;

    // to handle IDNT via bouncer:
    protected InetAddress clientHost = null;
    // must do (.*) here, since there could be no ident sent
    protected final Pattern idnt = Pattern.compile("IDNT (.*)@(.+):(.+)");
    protected final int timeout;
  /**
   * This is needed in cubnc mode, as the slave calls involved in the RNTO even creation messes up the flow for glftpd.
   */
  protected Event renameFromEvent;

    // NOTE: we only allow ONE command to be running in the background. Otherwise people could start as many transfers as they wanted.
    // Also, the control information wouldn't make any sense if we waited for multiple commands to return.
   
    public Connection(Socket controlConnection, long connectionId) {
        super();
        this.controlConnection = controlConnection;
        this.connectionId = connectionId;
        this.siteCommandHandler = ServiceManager.getServices().getSiteCommandHandler();
        if (this.siteCommandHandler == null) {
            throw new IllegalStateException("Parameter siteCommandHandler in Connection may not be null");
        }
        timeout = ServiceManager.getServices().getSettings().getInt("/main/idle_timeout") * 1000; // this is ok, since the setting is reachable in both cubnc and cuftpd
        extendedEpsvResponse = "force".equalsIgnoreCase(ServiceManager.getServices().getSettings().get("/epsv/extended_epsv_response"));
    }

    public void run() {
        // _todo: why does it take so long to connect? (1 second on localhost. it used to be much faster)
        // this is due to not getting an ident response. with ident turned on it is blazingly fast!
        createControlStreams();
        // now, since we're doing ident here, we can easily do IDNT instead, when needed
        // this needs to be done after the streams are created, so it can read the IDNT response
        try {
            handleIdent();
        } catch (IOException e) {
            // anything that's big enough to throw an exception from here is a quitting offense
            terminate(true);
            return;
        }
        try {
            ServiceManager.getServices().initializePermissions(); // re-initialization. checks if needed.
        } catch (IOException e) {
            respond("421 Could not read permission file, quitting");
            terminate(false);
            return;
        } catch (PermissionConfigurationException e) {
            Logging.getErrorLog().reportCritical("Failed to update permissions, unknown permission encountered: " + e.getMessage() + " on line: " + e.getLineNumber() + ':' + e.getLine());
        }
        if (!Server.getInstance().isSuspended()) {
            respond("220 " + ServiceManager.getServices().getSettings().get("/main/greeting") + (ServiceManager.getServices().getSettings().getBoolean("/main/show_version") ? ' ' + Server.getInstance().getVersion() : ""));
            try {
                String command;
                while ((command = controlIn.readLine()) != null) {
                    Logging.getCommandLog().logCommand(user, connectionId, clientHost, command);
                    lastCommand = command; // this is done so that lastCommand is never equal to null
                    if (Server.getInstance().isSuspended()) {
                        respond("430 Server is currently suspended because the remote userbase is down. please contact a siteop.");
                        // 4** reply here, since it is transient negative
                        // *3* since it has to do with accounting and userbase etc
                    } else {
                        handleCommand(lastCommand);
                    }
                }
                // if command was null, it falls through. This happens when the connection was abruptly closed. see below for reaction
            } catch (IOException e) {
                // connection terminated
            }
        } else {
            respond("430 Server is currently suspended the remote userbase is down. please contact a siteop.");
        }
        terminate(true);
    }

    protected void handleIdent() throws IOException {
        // any exception thrown from here is reason enough to drop the connection

        // if the connection is coming from a host specified in the pre_bouncer_ip setting in bnc.xml, delay for a couple of seconds, waiting for and IDNT command.

        boolean doNormalIdent;
        if (ServiceManager.getServices().getSettings().isBouncer(controlConnection)) {
            // just wait the default timeout for IDNT, or is that too much?
            // what are the odds that someone will be connecting from that sitecommands without using the bnc?
            // set this wait time to something like 5 seconds, instead of whatever idle-time is
            controlConnection.setSoTimeout(5000);
            try {
                // if we got an IDNT, that means we didn't time out
                String input = controlIn.readLine();
                Logging.getCommandLog().logCommand(null, connectionId, controlConnection.getInetAddress(), input);
                Matcher m = idnt.matcher(input);
                if (m.matches()) {
                    String ident = m.group(1);
                    if (ident != null) {
                        this.ident = ident;
                    } else {
                        this.ident = "*";
                    }
                    try {
                        clientHost = InetAddress.getByName(m.group(2));
                    } catch (UnknownHostException e) {
                        // if this fails, try the second variable
                        clientHost = InetAddress.getByName(m.group(3));
                    }
                    doNormalIdent = false;
                } else {
                    // we got something, but it wasn't what we were looking for. what was it?
                    System.err.println("hoping for IDNT command, but instead we got: " + input);
                    throw new IOException("Wrong IDNT command: " + input);
                }
            } catch (SocketTimeoutException e) {
                doNormalIdent = true;
            }
        } else {
            // it's not a bouncer, ask the connection for its ident
            doNormalIdent = true;
            // if it is not a bouncer, and we only allow connection from bouncers, throw an exception to se terminate
            // always allow from localhost
            if (ServiceManager.getServices().getSettings().getBoolean("/main/bouncer_only") && !controlConnection.getInetAddress().isLoopbackAddress()) {
                // technically it might not be an IOException, but we're catching them outside anyway, so it is convenient
                throw new IOException("Host is not a bouncer");
            }
        }
        if (doNormalIdent) {
            this.ident = new IdentClient().getIdent(controlConnection);
            clientHost = controlConnection.getInetAddress();
        }
        this.setName(ident + '@' + controlConnection.getInetAddress().getHostName() + ':' + connectionId);
        // since we set the timeout to 5 seconds before, we need to set it back here. the fastest way to do that is to just recreate the streams.
        createControlStreams();
    }

    /**
     * Takes a string including line breaks and presents it to the user in a proper format.
     * Note that this does NOT "finish" the output by doing leaving out the '-' after the last set of digits.
     *
     * @param code the response code we want to reply with.
     * @param message the message itself.
     * @param lateFlush false if we flush the stream after each line, true if we only flush the stream at the end.
     */
    protected void reply(int code, String[] message, boolean lateFlush) {
        if (message == null || message.length == 0) {
            controlOut.print(code + " ERROR IN REPLY"); // this covers broken zip-scripts and stuff
            Logging.getCommandLog().logResponse(user, connectionId, clientHost, code + " ERROR IN REPLY");
        } else {
            for (String line : message) {
                controlOut.print(code + "- " + line + "\r\n");
                Logging.getCommandLog().logResponse(user, connectionId, clientHost, code + "- " + line);
                if (!lateFlush) {
                    controlOut.flush();
                }
            }
        }
        controlOut.flush();
    }

    /**
     * Takes a string including linebreaks and presents it to the user in a proper format.
     * Note that this does NOT "finish" the output by doing leaving out the '-' after the last set of digits.
     *
     * @param code the response code we want to reply with.
     * @param message the message itself.
     * @param lateFlush false if we flush the stream after each line, true if we only flush the stream at the end.
     */
    public void reply(int code, String message, boolean lateFlush) {
        if (controlOut == null) {
            terminate(true);
        } else {
            // split the message up into lines
            // send each line to the recipient
            if (message != null) {
                String [] lines = message.split("(?:\n|\r\n)");
                reply(code, lines, lateFlush);
            }
        }
    }

    public void respond(String message) {
        if (controlOut != null) {
            //System.out.println("responding: " + message);
            controlOut.print(message + "\r\n");
            controlOut.flush();
            Logging.getCommandLog().logResponse(user, connectionId, clientHost, message);
        } else {
            terminate(true);
        }
    }

    protected void handleCommand(String commandString) {
        //System.out.println("  handling: " + commandString);
        timeOfLastCommand = System.currentTimeMillis();
        String[] cp = commandString.split(" ", 2);
        String command = cp[0].toUpperCase();
        String parameters = null;
        if (cp.length > 1) {
            parameters = cp[1];
        }
        if (auth != null && auth.isAuthenticated()) { // the only way this can be true is if the user has been authenticated in the pass() method
            // _todo: maybe we shouldn't do an IF-ELSE, but rather just keep the authenticated commands in an if, and keep everything that doesn't require a user outside
            if (command.endsWith("ABOR")) {
                abor();
            } else {
                handlePrivilegedCommand(command, parameters);
            }
        } else {
            if (ServiceManager.getServices().getSettings().getInt("/ssl/mode") == 2 && !(controlConnection instanceof SSLSocket || "AUTH".equalsIgnoreCase(command))) {
                // only allow encrypted connections
                respond("523 Must issue AUTH {TLS|SSL} before logging in.");
                return;
            }

            // these are executable no matter if we are logged in or not
            if ("USER".equals(command)) {
                user(parameters);
            } else if ("PASS".equals(command)) {
                pass(parameters);
            } else if ("AUTH".equals(command)) {
                // _todo: this way, we can't execute AUTH TLS after we have logged in, which may be desired
                // this is mostly cool, since we don't support CCC anyway
                auth(parameters);
            } else if ("PBSZ".equals(command)) {
                pbsz(parameters);
            } else if ("ACCT".equals(command)) {
                respond("502 Command not implemented.");
            } else if ("FEAT".equals(command)) {
                feat();
            } else if ("QUIT".equals(command)) {
                quit();
            } else if ("PROT".equals(command)) {
                // NOTE: cubnc sends this before logging in, so we need to allow it here too
                prot(parameters);
            } else {
                respond("500 Please log in first.");
            }
        }
    }

    protected void handlePrivilegedCommand(String command, String parameters) {
        // NOTE: we removed ABOR checking from here, since we check that as a special case in handleCommand(...)
        if ("NOOP".equals(command)) {
            noop();
        } else if ("RETR".equals(command)) {
            retr(parameters);
        } else if ("STOR".equals(command)) {
            stor(parameters, false);
        } else if ("PORT".equals(command)) {
            port(parameters);
        } else if ("EPRT".equals(command)) {
            eprt(parameters);
        } else if ("PASV".equals(command)) {
            pasv();
        } else if ("EPSV".equals(command)) {
            epsv(parameters);
        } else if ("PROT".equals(command)) {
            prot(parameters);
        } else if ("SIZE".equals(command)) {
            size(parameters);
        } else if ("LIST".equals(command)) {
            list(parameters, DataConnectionListing.LIST);
        } else if ("MLST".equals(command)) {
            mlst(parameters);
        } else if ("MLSD".equals(command)) {
            list(parameters, DataConnectionListing.MLSD);
        } else if ("MODE".equals(command)) {
            mode(parameters);
        } else if ("MDTM".equals(command)) {
            mdtm(parameters);
        } else if ("MFMT".equals(command)) {
            mfmt(parameters);
        } else if ("MFF".equals(command)) {
            mff(parameters);
        } else if ("CWD".equals(command) || "XCWD".equals(command)) {
            cwd(parameters);
        } else if ("PWD".equals(command) || "XPWD".equals(command)) {
            pwd();
        } else if ("CDUP".equals(command) || "XCUP".equals(command)) {
            cwd("..");
        } else if ("APPE".equals(command)) {
            stor(parameters, true);
        } else if ("REST".equals(command)) {
            rest(parameters);
        } else if ("TYPE".equals(command)) {
            type(parameters);
        } else if ("STAT".equals(command)) {
            stat(parameters);
        } else if ("CPSV".equals(command)) {
            cpsv();
        } else if ("SSCN".equals(command)) {
            sscn(parameters);
        } else if ("QUIT".equals(command)) {
            quit();
        } else if ("DELE".equals(command)) {
            dele(parameters);
        } else if ("RMD".equals(command)) {
            rmd(parameters);
        } else if ("MKD".equals(command) || "XMKD".equals(command)) {
            mkd(parameters);
        } else if ("SITE".equals(command)) {
            site(parameters);
        } else if ("XCRC".equals(command)) {
            xcrc(parameters);
        } else if ("XMD5".equals(command)) {
            xmd5(parameters);
        } else if ("XSHA1".equals(command)) {
            xsha1(parameters);
        } else if ("SYST".equals(command)) {
            syst();
        } else if ("FEAT".equals(command)) {
            // we had to put this here, otherwise we're get a 500-reply too many, and everything would be skewed
            feat();
        } else if ("HELP".equals(command)) {
            help();
        } else if ("NLST".equals(command)) {
            list(parameters, DataConnectionListing.NLST);
        } else if ("CLNT".equals(command)) {
            clnt(parameters);
        } else if ("RNFR".equals(command)) {
            rnfr(parameters);
        } else if ("RNTO".equals(command)) {
            rnto(parameters);
        } else if ("STOU".equals(command)) {
            stou();
        } else if ("STRU".equals(command)) {
            stru(parameters);
        } else if ("PBSZ".equals(command)) {
            // this needs to be available after login too
            pbsz(parameters);
        } else {
            //System.out.println("unknown command was: _" + command + '_');
            respond("500 Unknown command: " + command);
        }
    }

    public String getCurrentDir() {
        return fs.getFtpParentWorkingDirectory();
    }

    public long getIdleTime() {
      // todo: this should take transfers into account
        return (System.currentTimeMillis() - timeOfLastCommand) / 1000;
    }

    public long getCurrentSpeed() {
        if (transfer != null) {
            // oops, forgot to check null.
            return transfer.getSpeed();
        } else {
            return 0;
        }
        //return asynchronousCommand.getCurrentSpeed();
    }

    public void resetControlConnectionTimeout() {
        setControlConnectionTimeout(timeout);
    }

    public void setControlConnectionTimeout(int timeout) {
        try {
            controlConnection.setSoTimeout(timeout);
        } catch (SocketException e) {
            //e.printStackTrace();
            terminate(true);
            // _todo: either don't announce this, or do it with some other tag than ERROR
            //Logging.getErrorLog().reportError("Forcibly terminating a broken connection (could not set timeout): " + e.getMessage());
        }
    }

    public InetAddress getClientHost() {
        return clientHost;
    }

    /**
     * Shuts down the connection nicely. Triggers the terminate function by closing the control connection.
     */
    public void shutdown() {
        // todo: we should interrupt the connection thread here, in case it's waiting for a lock on the transfer
        try {
            controlConnection.close();
        } catch (IOException e) {
            Logging.getErrorLog().reportException("Failed to close control connection", e);
            //e.printStackTrace();
        }
    }

    protected synchronized void terminate(boolean force) {
        // synch and if-statement makes sure that only one thread can shut down the connection,
        // thus when we close the controlConnection, it won't run in to this method twice
        if (!shuttingDown) {
            shuttingDown = true;
            try {
                controlConnection.close();
            } catch (IOException e) {
                Logging.getErrorLog().reportException("Failed to close control connection", e);
                //e.printStackTrace();
            }
            if (user != null) {
                Logging.getSecurityLog().logoff(user);
            }
            if (fs != null) {
                fs.shutdown();
            }
            try {
                if (dataConnection != null) {
                    dataConnection.close();
                }
            } catch (IOException e) {
                // this is fine, we don't care if the socket was already closed or not
                Logging.getErrorLog().reportException("Failed to close data connection", e);
                //e.printStackTrace();
            }
           Server.getInstance().removeConnection(this);
        }
    }

    protected void createControlStreams() {
        try {
            controlIn = new BufferedReader(new InputStreamReader(controlConnection.getInputStream(), "ISO-8859-1"));
            controlOut = new PrintWriter(new BufferedWriter(new OutputStreamWriter(controlConnection.getOutputStream(), "ISO-8859-1")));
            controlConnection.setSoTimeout(timeout);
        } catch (IOException e) {
            Logging.getErrorLog().reportError("Failed to create streams, forcibly terminating a broken connection: " + e.getMessage());
            terminate(true);
        }
    }

    /**
     * Parses passive line to get port and host address. Puts them in an <code>InetSocketAddress</code> object and returns it.
     *
     * @param line passive mode line to parse.
     * @return the host and port in a ncie package.
     */
    public static InetSocketAddress parsePassiveModeString(String line) {
        // NOTE: we now throw all the exceptions out instead, so that we either get a proper value, or an exception
        String portline = line.substring(line.indexOf('(') + 1, line.lastIndexOf(')'));
        String[] data = portline.split(",");
        String ip = data[0] + '.' + data[1] + '.' + data[2] + '.' + data[3];
        // shift is faster than multiply
        int highPort = Integer.parseInt(data[4]) << 8;
        int lowPort = Integer.parseInt(data[5]);
        int passivePort = highPort + lowPort;
        return new InetSocketAddress(ip, passivePort);
    }

    /**
     * Looks at a socket, get creates the appropriate passive mode response line.
     *
     * @return the passive mode string to be relayed to the client.
     * @throws java.net.UnknownHostException thrown if the host address specified in /ftpd/main/pasv_address does not resolvePath.
     */
    public String getPassiveModeString() throws UnknownHostException {
        if (passiveServerSocket == null) {
            throw new IllegalArgumentException("ServerSocket was null! We couldn't create a server socket, check permissions (or use ports > 1024) or check ip-stack.");
        }
        String passiveModeString = passiveServerSocket.getInetAddress().getHostAddress();
        int highPort = passiveServerSocket.getLocalPort() >> 8;
        int lowPort = passiveServerSocket.getLocalPort() & 0xff;

        // _todo: maybe we should keep a cached version of this around, to speed things up
        // no, because we want to resolve it each time, in case we have a host that changes ip(dns) frequently (as with cubnc, in some cases)
        // note: this is NOT used to bind the pasv address. This is used to make things work with NAT, so we can specify the "outside" address here
        String pasvAddress = ServiceManager.getServices().getSettings().get("/main/pasv_address");
        if (pasvAddress != null && !"".equals(pasvAddress)) {
            InetAddress pasvAddr = InetAddress.getByName(pasvAddress);
            if (pasvAddr != null) {
                passiveModeString = pasvAddr.getHostAddress();
            }
        }
        passiveModeString = passiveModeString.replace('.',',');
        passiveModeString += "," + highPort + ',' + lowPort;
        return passiveModeString;
    }

    private boolean createActiveSocket(String host, int port, int connectionTimeout) {
        stopListening();
        try {
            /* No clean up. This is not done properly by the file transfer, so this shouldn't be a problem anymore
            if (dataConnection != null) {
                dataConnection.close(); // clean up before we start a new one
            }
            */
            if (encryptedDataConnection) {
                dataConnection = DefaultSSLSocketFactory.getFactory().createSocket(sscn||cpsv);
            } else {
                dataConnection = new Socket();
            }
            // bind to the address they connected to. they couldn't connect to the address if it wasn't the bind address anyway, so that problem is solved
            dataConnection.bind(new InetSocketAddress(controlConnection.getLocalAddress(),0));
            //dataConnection.bind(new InetSocketAddress(Server.getInstance().getBindAddress(),0));
            dataConnection.connect(new InetSocketAddress(host, port), connectionTimeout); // having the connectionTimeout here guarantees that faulty sockets will die after a while
            dataConnection.setSoTimeout(15000); // NOTE: this doesn't need to be as long as the timeout for the control connection.
            return true;
        } catch (SocketTimeoutException e) {
            Logging.getErrorLog().reportError("Failed to connect to remote host for active mode listing: " + e.getMessage());
        } catch (IOException e) {
            Logging.getErrorLog().reportError("Failed to create Socket for active mode connection: " + e.getMessage());
        }
        return false;
    }

    public Socket getDataConnection() {
        if (portisa != null) {
            //System.err.println("creating active connection to " + portisa.getHostName() + " on port " + portisa.getPort());
            createActiveSocket(portisa.getHostName(), portisa.getPort(), ServiceManager.getServices().getSettings().getInt("/main/connection_timeout") * 1000);
            //System.err.println("done");
        }
        if (dataConnection == null || !dataConnection.isConnected() || dataConnection.isClosed()) {
            // if we either didn't get PASV or PORT before this, we need to tell the user
            throw new IllegalStateException("Data connection command {PORT|PASV|EPRT|EPSV} must be issued first");
        }
        return dataConnection;
    }
    public void stopListening() {
        // because we hold a lock while waiting for this to timeout, we have to abort any previous connections before the lock is held
        if (passiveServerSocket != null) {
            try {
                passiveServerSocket.close(); // we can't have 2 open listeners at the same time
            } catch (IOException e) {
                Logging.getErrorLog().reportException("Failed to close server socket", e);
                //e.printStackTrace();
            }
        }
    }
    public ServerSocket createPassiveServerSocket(DataConnectionProtocolFamily protocolFamily) throws IOException, NoCompatibleNetworkInterfaceFoundException {
        /* This is now done before the lock is taken in PassiveDataConnection
        if (passiveServerSocket != null) {
            passiveServerSocket.close(); // we can't have 2 open listeners at the same time
        }
        */
        boolean retry = true;
        if (encryptedDataConnection) {
            passiveServerSocket = DefaultSSLSocketFactory.getFactory().createServerSocket();
        } else {
            passiveServerSocket = new ServerSocket();
        }

        passiveServerSocket.setReuseAddress(true); // we do this because if we don't, we can run out of passive sockets that are in the TIME_WAIT state, if we have specified a range that is too small
        int port;
        int i = 0;
        while (retry) {
            i++;
            // note: this CAN get stuck in an infinite loop, but server sockets should get used up quickly, so there should be little chance of that happening.
            // now it quits after 50 tries
            port = ServiceManager.getServices().getSettings().getNextPassivePort();
            //System.err.println("creating a passive socket on " + (port == 0 ? "RANDOM port" : "port " + port));
            try {
                String ipv4BindAddress = ServiceManager.getServices().getSettings().get("/epsv/ipv4_bind_address");
                String ipv6BindAddress = ServiceManager.getServices().getSettings().get("/epsv/ipv6_bind_address");

                InetAddress bindAddress = controlConnection.getLocalAddress();
                if (protocolFamily == DataConnectionProtocolFamily.IPV4 && controlConnection.getInetAddress() instanceof Inet6Address) {
                    if (!ipv4BindAddress.isEmpty()) {
                        bindAddress = InetAddress.getByName(ipv4BindAddress);
                    } else {
                        bindAddress = getAddressOfType(Inet4Address.class);
                    }
                } else if (protocolFamily == DataConnectionProtocolFamily.IPV6 && controlConnection.getInetAddress() instanceof Inet4Address) {
                    if (!ipv6BindAddress.isEmpty()) {
                        bindAddress = InetAddress.getByName(ipv6BindAddress);
                    } else {
                        bindAddress = getAddressOfType(Inet6Address.class);
                    }
                }

                // this means we bind on whatever address they connected to, which satisfies the bind_address constraint.
                // however, if they connect to "localhost" from "localhost", they can't do fxp. that's fine with me
                passiveServerSocket.bind(new InetSocketAddress(bindAddress, port),1);
                retry = false;
            } catch (IOException e) {
                retry = true;
                // _todo: maybe we should sleep for a couple of ms here?
                // No, because we want this to be fast.
                if (i > 50) {
                    Logging.getErrorLog().reportCritical("FATAL: a client has spent a long time in a loop trying to create a passive socket on an available port. Please make sure that there are enough free ports in the range to support all the users you have. Client has tried " + i + " times. Reason: " + e.getMessage());
                    throw new IOException("No available ports");
                }
            }
        }
        return passiveServerSocket;
    }

    /**
     * Finds the first applicable address of the specified type. Skips loopback and link-local addresses,
     * as well as interfaces that are down.
     *
     * This method does NOT check that the interface in question is the same as the control connection.
     * This is because we might have set up different interfaces for different protocol versions.
     *
     * @param addressType the type of address we want to find.
     * @return an address among the ones that exist on this machine that matches the type.
     * @throws java.net.SocketException if the system cannot be queried about network addresses.
     * @throws NoCompatibleNetworkInterfaceFoundException if no address/interface matching the provided type can be found.
     */
    private <T extends InetAddress> InetAddress getAddressOfType(Class<T> addressType) throws IOException, NoCompatibleNetworkInterfaceFoundException {
        final Enumeration<NetworkInterface> networkInterfaces = NetworkInterface.getNetworkInterfaces();
        while (networkInterfaces.hasMoreElements()) {
            NetworkInterface networkInterface = networkInterfaces.nextElement();
            if (networkInterface.isLoopback() || !networkInterface.isUp()) continue;
            Enumeration<InetAddress> inetAddresses = networkInterface.getInetAddresses();
            while (inetAddresses.hasMoreElements()) {
                InetAddress inetAddress = inetAddresses.nextElement();
                if (!inetAddress.isLoopbackAddress() && !inetAddress.isLinkLocalAddress() && addressType.isAssignableFrom(inetAddress.getClass())) {
                    return inetAddress;
                }
            }
        }
        throw new NoCompatibleNetworkInterfaceFoundException();
    }

    /**
     * Accepts the client connection on the data socket, and enters passive mode. After this, the server can send
     * dirlistings on this socket.
     * @throws java.io.IOException if there is some I/O failure.
     */
    public void enterPassiveMode() throws IOException {
        // the only reason we're separating this from createPassiveSocket() is so that we can return the passiveMode address string, without which this would all be impossible.
        if (passiveServerSocket != null) {
            try {
                // since the socket is guaranteed to be closed by the transfer objects, we don't need to close it here
                passiveServerSocket.setSoTimeout(ServiceManager.getServices().getSettings().getInt("/main/connection_timeout") * 1000);
                dataConnection = passiveServerSocket.accept();
                dataConnection.setSoTimeout(15000); // NOTE: this doesn't need to be as long as the timeout for the control connection.
                //System.out.println(System.nanoTime() +  " enterPassiveMode: " + dataConnection.hashCode() + " cmd=" + lastCommand);
            } catch (IOException e) {
                // don't log this, it happens if a socket is aborted (ABOR just after PASV)
                //Logging.getErrorLog().reportError("Failed to enter passive mode. " + e.getMessage());
                // we should close the socket here, in case the error wasn't related to closing, just to be on the safe side
                if (dataConnection != null) {
                    dataConnection.close();
                    //NOTE: this can throw a "socket already closed" exception
                    // which isn't a problem, because we don't report the error anyway, we just handle it
                }
                throw e;
            }
        } else {
            throw new IllegalArgumentException("Could not enter passive mode, server socket not created yet");
        }
    }

    //public void reportTransferSuccess()

    public void reportTransferFailure(String message) {
        if (client.contains("FlashFXP 3.4") || "FlashFXP 3.5.1.1200".equals(client)) {
            respond("226- Please get a client that can handle ABOR properly.");
            respond("226- Transfer aborted: " + message);
        } else {
            // 426 for PASV and 425 for PORT, but the meaning is the same
/*
            if (portisa != null) {
                respond("425 Can't open data connection: " + message);
            } else {
*/
                respond("426 Connection closed; transfer aborted: " + message);
//            }
        }
    }

    public User getUser() {
        return user;
    }

    public long getTimeOfLastCommand() {
        return timeOfLastCommand;
    }

    public String getLastCommand() {
        return lastCommand;
    }

    public String getClient() {
        return client;
    }

    public boolean isUseEpsvOnly() {
        return useEpsvOnly;
    }

    public void setUseEpsvOnly() {
        this.useEpsvOnly = true;
    }

    public void setExtendedEpsvResponse(boolean extendedEpsvResponse) {
        this.extendedEpsvResponse = extendedEpsvResponse;
    }

    public boolean useExtendedEpsvResponse() {
        return extendedEpsvResponse;
    }

    public int getXdupe() {
        return xdupe;
    }

    public void setXdupe(int xdupe) {
        this.xdupe = xdupe;
    }

    public void fileExists() {
        //long xdupeStart = System.currentTimeMillis();
        if (xdupe > 0) {
            try {
                // 553
                //String[] dupes = fs.getRealParentWorkingDirectory().list(FileSystem.forbiddenFiles);
                // this will now obey the permission system, however this change should be cosmetic at best, as the scenarios where this will throw an exception are rare 8if not impossible to find)
                List<String> dupes = fs.nlst(null);
                int lineLength = 66;
                boolean onePerLine = false;
                switch (xdupe) {
                    case 1:
                        lineLength = 66; onePerLine = false; break;
                    case 2:
                        lineLength = 66; onePerLine = true; break;
                    case 3:
                        lineLength = Integer.MAX_VALUE; onePerLine = true; break;
                    case 4:
                        lineLength = 1024; onePerLine = false; break;
                }
                int currentLength = 0;
                StringBuilder sb = new StringBuilder(32);
                for (String dupe : dupes) {
                    currentLength += dupe.length() + 1;
                    if (currentLength > lineLength || onePerLine) {
                        sb.append("\r\n").append("X-DUPE: ");
                        currentLength = 0;
                        if (xdupe == 4) break; // we're not creating anymore lines. (but I doubt anybody ever uses this mode anyway)
                    }
                    sb.append(dupe).append(' ');
                }
                reply(553, sb.toString(), true);
                //System.out.println("xdupe handling for " + file.getName() + " took " + (System.currentTimeMillis() - xdupeStart) + " milliseconds");
            } catch (PermissionDeniedException e) {
                Logging.getErrorLog().reportException("Permission denied to list files for X-DUPE", e);
                // this is pretty unlikely to happen, but if it does, we are saved by the 553 below.
            } catch (FileNotFoundException e) {
                Logging.getErrorLog().reportException("Could not find current directory.", e);
            }
        }
        // always end with this.
        respond("553 File exists.");
    }

    protected static final String lineFormat = "%-3d- %-1s %-8s %-11s %-10s %-6s %-12s %-16s    %-1s";
    public void statline(long speed) {
        respond(Formatter.createHeader(226, "stat"));
        respond(String.format(lineFormat, 226, Formatter.getBar(), "Section", "Credits", "Speed", "Ratio" /* "leech" if applicable */, "Free space", "Total space" /* space */, Formatter.getBar()));
        respond(Formatter.createLine(226));
        respond(String.format(lineFormat, 226, Formatter.getBar(), fs.getCurrentSection().getName(), Formatter.size(user.getCredits()), (speed == -1 ? "N/A" : Formatter.speedFromKBps(speed)), (user.hasLeech() ? "leech" : "1:" + fs.getCurrentSection().getRatio()), Formatter.size(fs.getFreeSpaceInParentWorkingDirectory()), Formatter.size(fs.getTotalSpaceInParentWorkingDirectory()), Formatter.getBar()));
        respond(Formatter.createFooter(226));
    }

    //
    // commands:
    //

    /**
     * The executing code for this method was moved to its own class, for clarity.
     *
     * @param parameters the parameters to the command, including the command name.
     */
    public void site(String parameters) {
        Event event = EventFactory.siteCommand(user, fs, parameters);
        boolean ok = ServiceManager.getServices().getEventHandler().handleBeforeEvent(event, this);
        if (ok) {
            siteCommandHandler.execute(this, user, fs, parameters);
            ServiceManager.getServices().getEventHandler().handleAfterEvent(event);
        }
    }

    protected void help() {
        respond("214-The following commands are recognized");
        respond("ABOR APPE CDUP CWD DELE EPRT EPSV FEAT HELP LIST MDTM MKD");
        respond("MODE NLST NOOP PASS PASV PORT PWD QUIT REST RETR RMD RNFR");
        respond("RNTO SITE SIZE STAT STOR STOU SYST TYPE USER XCUP XCWD XMKD");
        respond("XPWD XRMD PROT MDTM CPSV SSCN AUTH PBSZ CLNT STRU MLST MLSD");
        respond("XCRC XMD5 XSHA1");
        respond("214 Help OK.");
    }

    protected void stou() {
        // http://www.freesoft.org/CIE/RFC/1123/55.htm
        stor(null, false);
    }

    protected void mode(String parameters) {
        if ("S".equalsIgnoreCase(parameters)) {
            fs.setMode("S");
            respond("200 MODE set to S (default)");
        } else if("Z".equalsIgnoreCase(parameters)) {
            respond("500 MODE not supported.");
            // _todo: now that we have slow ascii transfers, we need to implement this mode.
            // NO! The reason why we don't do this is that the GZIPInputStream is not a drop-in replacement for other streams, i.e. you can't just wrap one in the other.
            // for unknown reasons, this is the case, and as a result, we have to suffer.
            //fs.setMode("Z");
            //respond("200 MODE set to Z");
        } else {
            respond("500 MODE not supported.");
        }
    }

    private void mff(String parameters) {

        if (parameters != null) {
            String[] t = parameters.split(" ");
            if (t.length == 2) {
                try {
                    String filename = t[1];
                    String[] facts = t[0].split(";");
                    StringBuilder sb = new StringBuilder(80);
                    sb.append("213 ");
                  for (String fact : facts) {
                    // for each fact we modify, we need to include it in the response
                    String[] keyValue = fact.split("=", 2);
                    if (keyValue.length == 2) {
                      if ("Modify".equalsIgnoreCase(keyValue[0])) {
                        Date date = fs.DATETIME_FORMAT.parse(keyValue[1]);
                        if (date.getTime() < 0) {
                          respond("500 Please use only dates after 1970-01-01");
                          return;
                        }
                        fs.setLastModified(filename, date.getTime());
                        sb.append("Modify=").append(keyValue[1]).append(";");
                      } else if ("UNIX.owner".equalsIgnoreCase(keyValue[0])) {
                        fs.chown(filename, keyValue[1], null, false);
                        sb.append("UNIX.owner=").append(keyValue[1]).append(";");
                      } else if ("UNIX.group".equalsIgnoreCase(keyValue[0])) {
                        fs.chown(filename, null, keyValue[1], false);
                        sb.append("UNIX.group=").append(keyValue[1]).append(";");
                      }
                    } // if not, then we just skip that value
                  }
                    sb.append(" ").append(filename);
                    respond(sb.toString());
                } catch (PermissionDeniedException e) {
                    respond("531 " + e.getMessage());
                } catch (FileNotFoundException e) {
                    respond("550 File not found.");
                } catch (ParseException | NoSuchUserException | NoSuchGroupException e) {
                    respond("500 " + e.getMessage());
                }
            } else {
                respond("501 Incorrect set of parameters.");
            }
        } else {
            respond("501 Incorrect set of parameters.");
        }

    }

    private void mfmt(String parameters) {
        if (parameters != null) {
            String[] t = parameters.split(" ", 2);
            if (t.length > 1) {
                try {
                    Date d = fs.DATETIME_FORMAT.parse(t[0]);
                  if (d.getTime() < 0) {
                    respond("500 Please use only dates after 1970-01-01");
                    return;
                  }
                  fs.setLastModified(t[1], d.getTime());
                    respond("213 Modify=" + t[0] + "; " + t[1]);
                } catch (ParseException e1) {
                    respond("500 " + t[0] + " does not follow the correct format.");
                } catch (PermissionDeniedException e1) {
                    respond("531 " + e1.getMessage());
                } catch (FileNotFoundException e) {
                    respond("550 File not found.");
                }
            } else {
                respond("550 File not found.");
            }
        } else {
            respond("501 Incorrect set of parameters.");
        }
    }

    protected void mdtm(String parameters) {
        // http://www.landfield.com/wu-ftpd/mail-archive/wuftpd-questions/2001/Apr/0217.html
        // we currently only support query
        // MODIFY: http://osdir.com/ml/ietf.ftpext/2004-02/msg00011.html (and also add a permission for it)
        // MDTM 20060323210950 somefile.txt (to set, but we can't support it. what if there's a file called "20060323210950 somefile.txt" and "somefile.txt" in the same dir, what then?
        if (parameters != null) {
            long time;
            try {
                time = fs.lastModified(parameters);
                respond("213 " + fs.DATETIME_FORMAT.format(new Date(time)));
            } catch (FileNotFoundException e) {
                // we couldn't find the whole thing, check if the first part is a date, and then try to find the second part
                String[] t = parameters.split(" ", 2);
                if (t.length > 1) {
                    try {
                        Date d = fs.DATETIME_FORMAT.parse(t[0]);
                      if (d.getTime() < 0) {
                        respond("500 Please use only dates after 1970-01-01");
                        return;
                      }
                        fs.setLastModified(t[1], d.getTime());
                        respond("200 Time set.");
                    } catch (ParseException e1) {
                        respond("500 " + t[0] + " does not follow the correct format.");
                    } catch (PermissionDeniedException e1) {
                        respond("531 " + e1.getMessage());
                    } catch (FileNotFoundException e1) {
                        respond("550 File not found.");
                    }
                } else {
                    respond("550 File not found.");
                }
            }
        } else {
            respond("501 Incorrect set of parameters.");
        }
    }

    protected void size(String file) {
        if (file != null) {
            try {
                long filesize = fs.length(file);
                respond("213 " + filesize);
            } catch (FileNotFoundException e) {
                respond("550 File not found: " + file);
            }
        } else {
            respond("501 Must provide a filename.");
        }
    }

    protected void clnt(String client) {
        this.client = client;
        if (client.contains("FlashFXP 3.4") || "FlashFXP 3.5.1.1200".equals(client)) {
            respond("200- NOTE: Your ftp client is unable to handle ABOR in an acceptable manner!");
        }
        respond("200 Client set to " + client);
    }

    protected void cpsv() {
        cpsv = true;
        pasv();
    }

    protected void sscn(String parameters) {
        if (parameters == null || "".equals(parameters)) {
            // respond with what sscn is currently set to
            if (sscn) {
                respond("200 SSCN:CLIENT METHOD");
            } else {
                // server method
                respond("200 SSCN:SERVER METHOD");
            }
        } else {
            // set it to what the parameters indicated.
            if ("ON".equalsIgnoreCase(parameters)) {
                // set client method
                sscn = true;
                respond("200 SSCN:CLIENT METHOD");
            } else {
                sscn = false;
                respond("200 SSCN:SERVER METHOD");
            }
        }
    }

    protected void prot(String mode) {
        if ("C".equalsIgnoreCase(mode)) {
            encryptedDataConnection = false;
        } else if ("P".equalsIgnoreCase(mode)) {
            encryptedDataConnection = true;
        }
        createControlStreams();
        respond("200 Protection set to " + mode);
    }

    protected void auth(String algorithm) {

        if ("TLS".equalsIgnoreCase(algorithm) || "SSL".equalsIgnoreCase(algorithm)) {
            // upgrade control socket
            try {
                // we can do this because if it fails, then the response will be over the normal socket anyway
                SSLSocket tSocket = DefaultSSLSocketFactory.getFactory().wrapSocket(controlConnection, false);
                respond("234 AUTH " + algorithm + " successful, handshaking...");
                //tSocket.startHandshake();

                controlConnection = tSocket;
                createControlStreams();
            } catch (IOException e) {
                respond("431 AUTH Failed! " + e.getMessage());
                terminate(false);
            }
            // if error, respond to the old socket.
            // if success, create new streams and answer through them
        } else {
            respond("500 Unknown algorithm: " + algorithm);
        }
    }

    protected void pbsz(String size) {
        /*
      If the server cannot parse the argument, or if it will not fit in
      32 bits, it should respond with a 501 reply code.

      If the server has not completed a security data exchange with the
      client, it should respond with a 503 reply code.

      Otherwise, the server must reply with a 200 reply code.  If the
      size provided by the client is too large for the server, it must
      use a string of the form "PBSZ=number" in the text part of the
      reply to indicate a smaller buffer size.  The client and the
      server must use the smaller of the two buffer sizes if both buffer
      sizes are specified.
         */
        // it should be noted that we only accept 0, i.e. meaning a stream encryption.
        if (controlConnection instanceof SSLSocket) {
            try {
                int s = Integer.parseInt(size);
                if (s == 0) {
                    respond("200 PBSZ 0 successful.");
                } else {
                    respond("200 PBSZ=0");
                }
            } catch (NumberFormatException e) {
                respond("501 \"" + size + "\" is not a 32-bit integer.");
            }
        } else {
            respond("503 Security parameters not established yet (send AUTH {TLS, SSL} first).");
        }
    }

    protected void syst() {
        respond("215 UNIX");
    }

    protected void rest(String parameters) {
        if (parameters != null) {
            try {
                long offset = Long.parseLong(parameters);
                fs.rest(offset);
                respond("350 Restarting at " + parameters);
            } catch (NumberFormatException e) {
                respond("501 " + parameters + " is not a number");
            }
        } else {
            respond("501 Incorrect set of parameters.");
        }
    }

    protected void type(String parameters) {
        if ("A".equalsIgnoreCase(parameters)) {
            fs.setType("ASCII");
/* This is checked for STOR and RETR anyway, so we don't need to check it here
            if (fs.getOffset() > 0) {
                respond("503 Bad sequence of commands (cannot resume transfers in ASCII mode)");
            } else {
                respond("200 Type set to ASCII");
            }
*/
            respond("200 Type set to ASCII");
        } else if("I".equalsIgnoreCase(parameters)) {
            fs.setType("IMAGE");
            respond("200 Type set to IMAGE");
        } else {
            respond("504 Unknown type " + parameters);
        }
    }

/*
    public final Object dataConnectionLock = new Object();
    private Thread asyncThread = null;
    protected void runAsynchronousCommand(AsynchronousCommand async) {
        asynchronousCommand = async;
        asyncThread = new Thread(asynchronousCommand);
        asyncThread.start();
    }
*/   

    protected void stat(String parameters) {
        if ("-l".equalsIgnoreCase(parameters) ||
                "-la".equalsIgnoreCase(parameters) ||
                "-al".equalsIgnoreCase(parameters)) {
            try {
                List<String> lines = fs.list(null);
                respond("213- status of " + parameters + ':');
                respond("total 1337"); // for the benefit of pftp, who doesn't know that this is a file listing without it...
                for (String line : lines) {
                    respond(line);
                }
                respond("213 End of status.");
                // if we do this here, ffxp 3.6rc3 (at least) shows funny characters in the listing, so we are skipping it
                // statline(-1, 213);
            } catch (AccessControlException | FileNotFoundException e) {
                respond("550 " + e.getMessage());
            } catch (PermissionDeniedException e) {
                respond("531 Permission denied.");
            }
        } else {
            respond("500 Unknown stat parameters.");
        }
    }

    protected void epsv(String parameters) {
        portisa = null; // disable active mode
        CommandPASV pasv = new CommandPASV(true, this, parameters);
        pasv.prepareAndStart();
    }

    protected void pasv() {
        if (useEpsvOnly) {
            respond("500 'EPSV ALL' was issued, please use only EPSV or disconnect and reconnect to use another mode of transfer.");
        } else {
            portisa = null; // disable active mode
            CommandPASV pasv = new CommandPASV(false, this, null);
            pasv.prepareAndStart();
        }
    }

    // according to section 3.2 of RFC959, we shouldn't initiate this connection until we get a transfer command. that makes it a lot easier for us, since we can remove one more asynchronous command
    protected InetSocketAddress portisa = null;
    protected void port(String parameters) {
        cpsv = false;
        if (useEpsvOnly) {
            respond("500 'EPSV ALL' was issued, please use only EPSV or disconnect and reconnect to use another mode of transfer.");
        } else {
            try {
                portisa = parsePassiveModeString('(' + parameters + ')');
                respond("200 PORT command sucessful.");
            } catch (IllegalArgumentException e) {
                respond("500 " + e.getMessage());
            } catch (SecurityException e) {
                respond("531 " + e.getMessage());
            }
        }
    }

    /**
     * http://www.zvon.org/tmRFC/RFC2428/Output/index.html
     * @param parameters information about the host and port to use, in the same format as returned from the PASV/EPSV command.
     */
    protected void eprt(String parameters) {
        if (parameters != null) {
            if (useEpsvOnly) {
                respond("500 'EPSV ALL' was issued, please use only EPSV or disconnect and reconnect to use another mode of transfer.");
            } else {
                String[] data = parameters.split("\\|");
                if (data.length < 4) {
                    respond("550 syntax error found in EPRT command");
                } else {
                    if ("1".equals(data[1]) /* ipv4 */ || "2".equals(data[1])/* ipv6 */) {
                        try {
                            portisa = new InetSocketAddress(data[2], Integer.parseInt(data[3]));
                            respond("200 EPRT command sucessful.");
                        } catch (NumberFormatException e) {
                            respond("500 " + data[3] + " is not a number");
                        } catch (IllegalArgumentException e) {
                            respond("500 " + e.getMessage());
                        } catch (SecurityException e) {
                            respond("531 " + e.getMessage());
                        }
                    } else {
                        respond("522 Network protocol not supported, use (1,2)"); // NOTE: the '(' and ')' are needed here, to indicate the available protocols.
                    }
                }
            }
        } else {
            respond("501 Incorrect set of parameters.");
        }
    }

    @Deprecated
    private void mlsd(String path) {
        if (ServiceManager.getServices().getSettings().getInt("/ssl/data_connection_mode") == 2 && !encryptedDataConnection) {
            // according to RFC 4217, this should be here and not in PASV/PORT
            respond("521 data connection cannot be opened with this PROT setting");
        } else {
            transfer = new DataConnectionListing(DataConnectionListing.MLSD, fs, this, path, encryptedDataConnection);
            transfer.start();
        }
    }

    private void mlst(String filename) {
        try {
            if (filename == null) {
                filename = "";
            }
            String fact = fs.mlst(filename);
            respond("250- Listing " + filename);
            respond(" " + fact); // the standard includes a leading space for MLST
            respond("250 End");
        } catch (FileNotFoundException e) {
            respond("550 File not found: " + e.getMessage());
        }
    }

    /**
     * Handles the LIST and NLST commands, listing either the current or a specified directory.
     * @param parameters the parameters to the list command, including possible directories.
     * @param listMode the list mode, DataConnectionListing.LIST or DataConnectionListing.NLST.
     */
    protected void list(String parameters, int listMode) {
        if (ServiceManager.getServices().getSettings().getInt("/ssl/data_connection_mode") == 2 && !encryptedDataConnection) {
            // according to RFC 4217, this should be here and not in PASV/PORT
            respond("521 data connection cannot be opened with this PROT setting");
        } else {
            if (parameters != null && parameters.startsWith("-")) {
                // remove any command-line switches that may be present. Do this only once.
                // if there's a directory called "-al", well then shit, someone is out of luck
                int startOfPath = parameters.indexOf(" ");
                if (startOfPath > -1) {
                    parameters = parameters.substring(startOfPath).trim(); // use trim() here instead of adding +1 to the index.
                    // just in case someone does "LIST -al              /mydir"
                } else {
                    // otherwise we just got a "LIST -la" or something. This is by far the most common occurance
                    parameters = null;
                }
            }
            transfer = new DataConnectionListing(listMode, fs, this, parameters, encryptedDataConnection);
            transfer.start();
        }
    }

    @Deprecated
    protected void list(String parameters) {
        if (ServiceManager.getServices().getSettings().getInt("/ssl/data_connection_mode") == 2 && !encryptedDataConnection) {
            // according to RFC 4217, this should be here and not in PASV/PORT
            respond("521 data connection cannot be opened with this PROT setting");
        } else {
            if (parameters != null && parameters.startsWith("-")) {
                // remove any command-line switches that may be present. Do this only once.
                // if there's a directory called "-al", well then shit, someone is out of luck
                int startOfPath = parameters.indexOf(" ");
                if (startOfPath > -1) {
                    parameters = parameters.substring(startOfPath).trim(); // use trim() here instead of adding +1 to the index.
                    // just in case someone does "LIST -al              /mydir"
                } else {
                    // otherwise we just got a "LIST -la" or something. This is by far the most common occurance
                    parameters = null;
                }
            }
            transfer = new DataConnectionListing(DataConnectionListing.LIST, fs, this, parameters, encryptedDataConnection);
            transfer.start();
        }
    }

    @Deprecated
    protected void nlst(String parameters) {
        if (ServiceManager.getServices().getSettings().getInt("/ssl/data_connection_mode") == 2 && !encryptedDataConnection) {
            // according to RFC 4217, this should be here and not in PASV/PORT
            respond("521 data connection cannot be opened with this PROT setting");
        } else {
            if (parameters != null && parameters.startsWith("-")) {
                // remove any command-line switches that may be present. Do this only once.
                // if there's a directory called "-al", well then shit, someone is out of luck
                int startOfPath = parameters.indexOf(" ");
                if (startOfPath > -1) {
                    parameters = parameters.substring(startOfPath).trim(); // use trim() here instead of adding +1 to the index.
                    // just in case someone does "LIST -al              /mydir"
                } else {
                    // otherwise we just got a "LIST -la" or something. This is by far the most common occurance
                    parameters = null;
                }
            }
            transfer = new DataConnectionListing(DataConnectionListing.NLST, fs, this, parameters, encryptedDataConnection);
            transfer.start();
        }
    }

    protected void retr(String filename) {
        if (ServiceManager.getServices().getSettings().getInt("/ssl/data_connection_mode") == 2 && !encryptedDataConnection) {
            // according to RFC 4217, this should be here and not in PASV/PORT
            respond("521 data connection cannot be opened with this PROT setting");
        } else if ("ASCII".equals(fs.getType()) && fs.getOffset() > 0) {
            respond("503 Bad sequence of commands (cannot resume transfers in ASCII mode)");
        } else {
            if (filename != null) {
                // NOTE: the before/after triggers for RETR are in CommandRETR.start()
                transfer = new CommandRETR(this, fs, user, filename, encryptedDataConnection, sscn||cpsv, ServiceManager.getServices().getSettings().getBoolean("/main/fast_ascii_transfer"));
                transfer.start();
            } else {
                respond("501 Must specify a file.");
            }
        }
    }

    /**
     * When we get an APPE-command, and have a REST offset, we start writing the file at that offset.
     * If we DON'T have a REST offset, we continue uploading at the end of the file.
     *
     * @param filename the filename of the file to be uploaded.
     * @param append appends the file instead of overwrite, if the user has the proper permissions.
     */
    protected void stor(String filename, boolean append) {
        if (ServiceManager.getServices().getSettings().getInt("/ssl/data_connection_mode") == 2 && !encryptedDataConnection) {
            // according to RFC 4217, this should be here and not in PASV/PORT 
            respond("521 data connection cannot be opened with this PROT setting");
        } else if ("ASCII".equals(fs.getType()) && fs.getOffset() > 0) {
            respond("503 Bad sequence of commands (cannot resume transfers in ASCII mode)");
        } else {
            // NOTE: the before/after triggers for STOR/APPE are in CommandSTOR.start()
            transfer = new CommandSTOR(this, fs, user, filename, append, encryptedDataConnection, sscn||cpsv, ServiceManager.getServices().getSettings().getBoolean("/site/zipscript/on_the_fly_crc"), ServiceManager.getServices().getSettings().getBoolean("/main/fast_ascii_transfer"));
            transfer.start();
        }
    }

    protected void abor() {
        try {
            if (passiveServerSocket != null) {
                passiveServerSocket.close();
            }
        } catch (IOException e) {
            Logging.getErrorLog().reportException("Failed to close server socket", e);
            //e.printStackTrace();
        }
        try {
            if (dataConnection != null) {
                //System.out.println(dataConnection + " closed: " + dataConnection.isClosed());
                //dataConnection.getInputStream().close(); // closing the input stream throws an exception saying that the socket was closed
                dataConnection.close();
            }
        } catch (IOException e) {
            Logging.getErrorLog().reportException("Failed to close socket", e);
            //e.printStackTrace();
        }
        // doing this dance with the semaphore make sure that we don't send a 226 until after the data connection command has sent its 426
        dataConnectionSemaphore.acquireUninterruptibly();
        try {
            respond("226 command aborted");
        } finally {
            dataConnectionSemaphore.release();
        }
    }

    protected void cwd(String directory) {
        if (directory != null) {
            try {
                fs.cwd(directory);
                String dotMessage = fs.getDotMessageForCurrentDir();
                if (dotMessage != null) {
                    reply(250, dotMessage, true);
                }
                respond("250 CWD successful.");
            } catch (FileNotFoundException | AccessControlException e) {
                respond("451 No such directory.");
            } catch (IllegalArgumentException e) {
                respond("531 " + e.getMessage());
            } catch (PermissionDeniedException e) {
                respond("531 Permission denied.");
            } catch (IOException e) {
                respond("550 " + e.getMessage());
            }
        } else {
            respond("501 Must specify a directory.");
        }
    }

    protected void pwd() {
        respond("257 \"" + fs.getFtpParentWorkingDirectory() + "\" is current directory.");
    }

    protected void quit() {
        // make some ustats available maybe?
        //221-Goodbye. You uploaded 0 and downloaded 0 kbytes
        String goodbyeMsg;
        // fs is null if we get the QUIT command before anything else
        if (fs != null) {
            String goodbyeMessagePath = ServiceManager.getServices().getSettings().get("/filesystem/goodbye_msg");
            try {
                goodbyeMsg = fs.readExternalTextFile(goodbyeMessagePath);
                if (goodbyeMsg != null) {
                    reply(221, goodbyeMsg, true);
                }
            } catch (Exception e){
                Logging.getErrorLog().reportError("Could not read goodbye message: " + goodbyeMessagePath + " because: " + e.getMessage());
            }
            fs.shutdown(); // needed so that cubnc can disconnect slaves etc.
        }
        respond("221 Goodbye!");
        // note: we don't terminate here, since this is taken care of when the socket is closed and it falls through
        //terminate(false);
    }

    protected void dele(String path) {
        if (path != null) {
            try {
                Event event = EventFactory.deleteFile(user, fs, path);

                boolean proceed = ServiceManager.getServices().getEventHandler().handleBeforeEvent(event, this);
                if (proceed) {
                    fs.delete(path);
                    respond("250 File deleted.");
                    ServiceManager.getServices().getEventHandler().handleAfterEvent(event);
                }
            } catch (IllegalArgumentException | PermissionException e) {
                respond("531 " + e.getMessage());
            } catch (IOException e) {
                // also covers FileNotFoundException
                respond("500 " + e.getMessage());
            }
        } else {
            respond("501 Must specify a file.");
        }
    }

    protected void rmd(String path) {
        if (path != null) {
            try {
                Event event = EventFactory.removeDirectory(user, fs, path);

                boolean proceed = ServiceManager.getServices().getEventHandler().handleBeforeEvent(event, this);
                if (proceed) {
                    fs.rmd(path);
                    respond("250 Directory deleted.");
                    ServiceManager.getServices().getEventHandler().handleAfterEvent(event);
                }
            } catch (IOException e) {
                // this also takes care of the FileNotFoundException
                respond("500 " + e.getMessage());
            } catch (IllegalArgumentException e) {
                respond("531 " + e.getMessage());
            } catch (PermissionException e) {
                // includes IPE
                respond("531 Permission denied.");
            }
        } else {
            respond("501 Must specify a directory.");
        }
    }

    protected void mkd(String path) {
        if (path != null) {
            try {
                Event event = EventFactory.createDirectory(user, fs, path);

                boolean proceed = ServiceManager.getServices().getEventHandler().handleBeforeEvent(event, this);
                if (proceed) {
                    String createdDir = fs.mkd(path);
                    respond("257 \"" + createdDir + "\" directory created");
                    ServiceManager.getServices().getEventHandler().handleAfterEvent(event);
                }
            } catch (IllegalArgumentException | PermissionException e) {
                respond("531 " + e.getMessage());
            } catch (IOException e) {
                respond("521 " + e.getMessage());
            }
        } else {
            respond("501 Must specify a directory.");
        }
    }

    protected void rnfr(String source) {
        // NOTE: rename permissions are checked inside the filesystem. this is good if we want to move our functionality to the filesystem (including the stuff that the virtual filesystem does), but it is bad if we are just overriding methods)
        if (source != null) {
            try {
                Event event = EventFactory.renameFrom(user, fs, source);

                boolean proceed = ServiceManager.getServices().getEventHandler().handleBeforeEvent(event, this);
                if (proceed) {
                    fs.rnfr(source);
                  renameFromEvent = event;
                    respond("350 Source ok, please provide destination.");
                    ServiceManager.getServices().getEventHandler().handleAfterEvent(event);
                }
            } catch (PermissionException e) {
                respond("531 You do not have access to that file.");
            } catch (AccessControlException e) {
                respond("531 Permission denied.");
            } catch (FileNotFoundException e) {
                respond("550 File not found: " + e.getMessage());
            } catch (IOException e) {
                respond("550 " + e.getMessage());
            }
        } else {
            respond("501 Must specify a source.");
        }
    }

    protected void rnto(String target) {
        // NOTE: permissions are checked by the filesystem, see comment for rnfr()
        if (target != null) {
            try {
              Event event = EventFactory.renameTo(user, fs, target, renameFromEvent);

                boolean proceed = ServiceManager.getServices().getEventHandler().handleBeforeEvent(event, this);
                if (proceed) {
                    boolean success = fs.rnto(target);
                    if (success) {
                        respond("250 Rename successful.");
                    } else {
                        respond("550 Rename operation failed.");
                    }
                    ServiceManager.getServices().getEventHandler().handleAfterEvent(event);
                }
            } catch (IllegalArgumentException e) {
                respond("531 " + e.getMessage());
            } catch (PermissionException e) {
                respond("531 You do not have access to that file.");
            } catch (AccessControlException e) {
                respond("531 Permission denied.");
            } catch (IOException e) {
                respond("550 " + e.getMessage());
            }
        } else {
            respond("501 Must specify a target.");
        }
    }

    protected void user(String username) {
        if (username == null || "".equals(username)) {
            respond("500 please provide a username.");
        } else if (!username.matches("[\\w-]+")) {
            respond("500 Username contains illegal characters.");
        } else {
            auth = new AuthenticationRequest(username, ident, clientHost);
            respond("331 Password required for " + username);
        }
       
    }

    protected void pass(String password) {
        // Note: the problem with strings is that they are immutable. as such, they can't be destroyed at will.
        // thus, we SHOULD store the password in a char array, but that's difficult, since we don't want to store everything in a char array.
        if (auth == null) {
            respond("503 Send username first");
            terminate(false);
        } else {
            auth.setPassword(password);
            // _todo: should we request the ident here instead, only if the user has anythign other than '*' as his ident?
            // no, too much hassle.
            // the problem is that it works cleanly with separation now. doing a delayed ident would give away behavior and require a lookup in the user before authenticaton
           
            int authentication = ServiceManager.getServices().getUserbase().authenticate(auth);
            if (authentication == AuthenticationResponses.OK) {
                auth.setAuthenticated();
                try {
                    user = ServiceManager.getServices().getUserbase().getUser(auth.getUsername());
                } catch (NoSuchUserException e) {
                    // it would be very weird if the user doesn't exist after the authentication, but it could still happen.
                    respond("530 User not logged in.");
                    Logging.getSecurityLog().userNotFound(auth);
                    terminate(false);
                    return;
                }

                int connectionLimit = ServiceManager.getServices().getSettings().getInt("/main/max_clients");
                int currentNumberOfConnections = Server.getInstance().getConnections().size();
                if (currentNumberOfConnections >= connectionLimit && !user.hasPermission(UserPermission.PRIVILEGED)) {
                    // we moved this here since we want people with the proper permissions to be able to log in even when the site is full
                    respond("421 Server busy.");
                    Logging.getSecurityLog().siteFull(connectionLimit, currentNumberOfConnections); // make a "note" of this in the log so that admins can analyze this later to see if the site is often full
                    terminate(false);
                    return;
                }

                int logins = user.getLogins(); // do it once, in case we are using a remote db
                if (Server.getInstance().getNumberOfConnectionsForUser(user.getUsername()) > logins && !user.hasPermission(UserPermission.PRIVILEGED)) {
                    // use strictly > when checking the number of current logins, since the user has already
                    // sent the USER command on the current connection, making it count as well.
                    respond("431 Your account is limited to " + logins + " simultaneous connections");
                    Logging.getSecurityLog().tooManyLoginsForUser(auth);
                    terminate(false);
                    return;
                }

                createAndSetFileSystem();
                String welcomeMessagePath = ServiceManager.getServices().getSettings().get("/filesystem/welcome_msg");
                try {
                    String welcomeMsg = fs.readExternalTextFile(welcomeMessagePath);
                    if (welcomeMsg != null) {
                        reply(230, welcomeMsg, true);
                    }
                } catch (Exception e) {
                    Logging.getErrorLog().reportError("Could not read welcome message: " + welcomeMessagePath + " because: " + e.getMessage());
                }
                respond("230 Credentials accepted, welcome!");
                Logging.getSecurityLog().logonSuccessful(auth);
                user.setLastlog(System.currentTimeMillis());
                this.setName(user.getUsername() + ':' + this.getName());
            } else {
                respond("530 User not logged in.");
                switch (authentication) {
                    // _todo: we could probably put this switch in the security log instead
                    // No, keep it here, so we keep the logging semantics the same; always invoke the method name of the event you want to log
                    case AuthenticationResponses.BAD_PASSWORD :
                        Logging.getSecurityLog().badPassword(auth);
                        break;
                    case AuthenticationResponses.BAD_IDENT_OR_HOST:
                        Logging.getSecurityLog().badIdentOrHost(auth);
                        break;
                    case AuthenticationResponses.NO_SUCH_USER :
                        Logging.getSecurityLog().userNotFound(auth);
                        break;
                    case AuthenticationResponses.USER_SUSPENDED :
                        Logging.getSecurityLog().userSuspended(auth);
                        break;
                }
                terminate(false);
            }
        }
    }

    protected void createAndSetFileSystem() {
        // this will be overridden in cubnc
        fs = new FileSystem(user);
    }

    protected void feat() {
        respond("211- Extensions supported");
        respond(" AUTH");
        respond(" SSCN");
        respond(" CPSV");
        respond(" PROT");
        respond(" PBSZ");
        respond(" CLNT");
        respond(" REST");
        respond(" REST STREAM");
        respond(" TVFS");
        respond(" MLSD"); // No need for this, MLST provides the client with this information, but lets include it anyway
        respond(" MLST Type*;Modify*;Size*;UNIX.owner*;UNIX.group*;");
        respond(" SIZE");
        respond(" EPRT");
        respond(" EPSV");
        respond(" NLST");
        respond(" MFF Modify;UNIX.owner;UNIX.group;");
        respond(" MFMT");
        respond(" MDTM");
        respond(" MDTM YYYYMMDDHHMMSS filename");
        respond(" XCRC filename start end");
        respond(" XMD5 filename start end");
        respond(" XSHA1 filename start end");
//        respond(" MODE Z");
        respond(" SITE XDUPE {0,1,2,3,4}");
        respond("211 End.");
    }

    protected void noop() {
        respond("200 NOOP command accepted");
    }

    private void stru(String structure) {
        if ("F".equalsIgnoreCase(structure)) {
            respond("200 STRUcture set to FILE");
        } else {
            respond("500 Unsupported structure");
        }
    }

    private void xmd5(String parameters) {
        if (parameters != null) {
            Matcher m = checksumParameters.matcher(parameters);
            if (m.matches()) {
                long start = 0;
                long end = 0;
                String filename = m.group(1);
                String firstNumber = m.group(2); // we do this to avoid processing m.group(n) multiple times
                String secondNumber = m.group(3);
                if (secondNumber != null) {
                    start = Long.parseLong(firstNumber);
                    end = Long.parseLong(secondNumber);
                } else if (/* secondNumber == null && */ firstNumber != null) {
                    end = Long.parseLong(firstNumber);
                }
                // No need to catch NumberFormatException here, as the regexp will already have taken care of that.
                try {
                    String digest = fs.xmd5(filename, start, end);
                    respond("250 " + digest);
                } catch (NoSuchAlgorithmException e) {
                    respond("500 " + e.getMessage());
                } catch (FileNotFoundException e) {
                    respond("550 File not found: " + filename);
                } catch (IOException e) {
                    respond("500 " + e.getMessage());
                }
            } else {
                respond("501- Invalid parameters, use one of the following: ");
                respond("501- XMD5 <filename (quoted if it contains spaces)>");
                respond("501- XMD5 <filename (quoted if it contains spaces)> <end>");
                respond("501  XMD5 <filename (quoted if it contains spaces)> <start> <end>");
            }
        } else {
            respond("501 Must provide a filename.");
        }
    }

    private void xsha1(String parameters) {
        if (parameters != null) {
            Matcher m = checksumParameters.matcher(parameters);
            if (m.matches()) {
                long start = 0;
                long end = 0;
                String filename = m.group(1);
                String firstNumber = m.group(2); // we do this to avoid processing m.group(n) multiple times
                String secondNumber = m.group(3);
                if (secondNumber != null) {
                    start = Long.parseLong(firstNumber);
                    end = Long.parseLong(secondNumber);
                } else if (/* secondNumber == null && */ firstNumber != null) {
                    end = Long.parseLong(firstNumber);
                }
                // No need to catch NumberFormatException here, as the regexp will already have taken care of that.
                try {
                    String digest = fs.xsha1(filename, start, end);
                    respond("250 " + digest);
                } catch (NoSuchAlgorithmException e) {
                    respond("500 " + e.getMessage());
                } catch (FileNotFoundException e) {
                    respond("550 File not found: " + filename);
                } catch (IOException e) {
                    respond("500 " + e.getMessage());
                }
            } else {
                respond("501- Invalid parameters, use one of the following: ");
                respond("501- XSHA1 <filename (quoted if it contains spaces)>");
                respond("501- XSHA1 <filename (quoted if it contains spaces)> <end>");
                respond("501  XSHA1 <filename (quoted if it contains spaces)> <start> <end>");
            }
        } else {
            respond("501 Must provide a filename.");
        }
    }

    private void xcrc(String parameters) {
        // XCRC <filename, possibly quoted>
        // XCRC <filename, possibly quoted> <end>
        // XCRC <filename, possibly quoted> <start> <end>
        if (parameters != null) {
            Matcher m = checksumParameters.matcher(parameters);
            if (m.matches()) {
                long start = 0;
                long end = 0;
                String filename = m.group(1);
                String firstNumber = m.group(2); // we do this to avoid processing m.group(n) multiple times
                String secondNumber = m.group(3);
                if (secondNumber != null) {
                    start = Long.parseLong(firstNumber);
                    end = Long.parseLong(secondNumber);
                } else if (/* secondNumber == null && */ firstNumber != null) {
                    end = Long.parseLong(firstNumber);
                }
                // No need to catch NumberFormatException here, as the regexp will already have taken care of that.
                try {
                    String xcrc = fs.xcrc(filename, start, end);
                    respond("250 " + xcrc);
                } catch (FileNotFoundException e) {
                    respond("550 File not found: " + filename);
                } catch (IOException e) {
                    respond("500 " + e.getMessage());
                }
            } else {
                respond("501- Invalid parameters, use one of the following: ");
                respond("501- XCRC <filename (quoted if it contains spaces)>");
                respond("501- XCRC <filename (quoted if it contains spaces)> <end>");
                respond("501  XCRC <filename (quoted if it contains spaces)> <start> <end>");
            }
        } else {
            respond("501 Must provide a filename.");
        }
    }

    public boolean isDownloading() {
        return lastCommand.startsWith("RETR") && dataConnection != null && !dataConnection.isClosed();
    }

    public boolean isUploading() {
        return ((lastCommand.startsWith("APPE") || lastCommand.startsWith("STOR")) && dataConnection != null && !dataConnection.isClosed());
    }

    public long getConnectionId() {
        return connectionId;
    }

    public String getIdent() {
        return ident;
    }

    public String getHost() {
        return controlConnection.getInetAddress().getHostAddress();
    }
}
TOP

Related Classes of cu.ftpd.Connection

TOP
Copyright © 2018 www.massapi.com. All rights reserved.
All source code are property of their respective owners. Java is a trademark of Sun Microsystems, Inc and owned by ORACLE Inc. Contact coftware#gmail.com.