Package cu.ftpd

Source Code of cu.ftpd.Server

/**
* 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.ftpd.commands.transfer.CommandRETR;
import cu.ftpd.filesystem.FileSystem;
import cu.ftpd.filesystem.filters.ForbiddenFilesFilter;
import cu.ftpd.filesystem.permissions.UnknownPermissionException;
import cu.ftpd.filesystem.permissions.PermissionConfigurationException;
import cu.ftpd.logging.Formatter;
import cu.ftpd.logging.Logging;
import cu.ftpd.user.User;
import cu.settings.ConfigurationException;
import cu.ssl.DefaultSSLSocketFactory;

import java.io.*;
import java.net.*;
import java.rmi.ConnectIOException;
import java.rmi.NotBoundException;
import java.security.*;
import java.security.cert.CertificateException;
import java.util.*;
import java.util.concurrent.Semaphore;
import java.util.concurrent.atomic.AtomicLong;

/*
todo: make an installer (java -jar install.jar)

todo: move permissions.acl to permission.acl.example

todo: make the whole installation procedure more user friendly
*/

/**
* @author Markus Jevring <markus@jevring.net>
* @since 2007-maj-07 : 19:09:36
* @version $Id: Server.java 294 2009-03-04 22:10:42Z jevring $
*/
public class Server implements Runnable {
    protected static Server instance;
    protected static Thread listener;
    protected static volatile boolean running = true;
    protected volatile boolean online = true;
    protected final Object forever = new Object();
    protected final Set<Connection> connections = Collections.synchronizedSet(new HashSet<Connection>());
    protected final AtomicLong clientId = new AtomicLong(0);
    protected final Timer timer = new Timer(true);
    protected long startTime;
    protected boolean suspended = false;
    protected ServerSocket serverSocket;
    protected String configurationFile;

    private Semaphore uploadSemaphore;
    private Semaphore downloadSemaphore;

    protected Server(String configurationFile) {
        this.configurationFile = configurationFile;
        // trusted zone
        startTime = System.currentTimeMillis();
        // note: this is done so that the resolution to canonical names is cached.
        System.setProperty("sun.io.useCanonCaches", "true");
        System.setProperty("sun.io.useCanonPrefixCache", "true");
    }

    public static Server getInstance() {
        return instance;
    }

    protected void createConnection(Socket client) {
        Connection conn = new Connection(client, clientId.incrementAndGet());
        // add it to the set of connections
        connections.add(conn); // thread safe via synchronized set above
        // start it
        conn.start();
    }

    protected void configure() throws IOException, ConfigurationException, NoSuchAlgorithmException, KeyManagementException, KeyStoreException, CertificateException, UnrecoverableKeyException, NotBoundException, ClassNotFoundException, IllegalAccessException, InstantiationException, PermissionConfigurationException {
        FtpdSettings settings = new FtpdSettings("/ftpd", configurationFile, "/cuftpd.xsd");

        System.setProperty("javax.net.ssl.trustStore", settings.get("/ssl/truststore/location"));
        System.setProperty("javax.net.ssl.trustStorePassword", settings.get("/ssl/truststore/password"));
        System.setProperty("javax.net.ssl.keyStore", settings.get("/ssl/keystore/location"));
        System.setProperty("javax.net.ssl.keyStorePassword", settings.get("/ssl/keystore/password"));

        ServiceManager.setServices(new Services(settings));

        this.uploadSemaphore = new Semaphore(settings.getInt("/main/max_uploaders"));
        this.downloadSemaphore = new Semaphore(settings.getInt("/main/max_downloaders"));
       
        // todo: remove all static stuff except logging (and possibly the stuff below)
        Logging.initialize(settings);

        // These COULD all be lazily initialized on first use of the class, but then we'd need synchronization and stuff, which is uncecessary.
        DefaultSSLSocketFactory.createFactory(settings.get("/ssl/keystore/location"), settings.get("/ssl/keystore/password"));
        // verify the truststore settings right away
        DefaultSSLSocketFactory.checkKeystorePassword(settings.get("/ssl/truststore/location"), settings.get("/ssl/truststore/password"));

        CommandRETR.initialize(settings.get("/filesystem/free_files"));
        ForbiddenFilesFilter.initialize(settings.get("/filesystem/forbidden_files"));

        FileSystem.initialize(settings);
        Formatter.initialize(settings);

        //createServerSocket(settings);
        createServerSocket(settings.get("/main/bind_address"), settings.getInt("/main/port"), settings.getInt("/ssl/mode"));
    }

    protected void createServerSocket(String bindAddress, int port, int sslMode) throws ConfigurationException, IOException {
        // actually, .getLocalHost() seems to take the external address, which is great!

        InetAddress bindInetAddress;
        if ("localhost".equals(bindAddress)) {
            bindInetAddress = InetAddress.getLocalHost(); // if we don't do this, we get the 0.0.0.0 address, which is useless
        } else if ("".equals(bindAddress) || bindAddress == null) {
            // if we don't do this, then we can't connect to "localhost".
            bindInetAddress = null;
        } else {
            try {
                bindInetAddress = InetAddress.getByName(bindAddress);
            } catch (UnknownHostException e) {
                throw new ConfigurationException("Could not bind to bind_address.", e);
            }
        }
        running = false;
        if (serverSocket != null) {
            serverSocket.close();
        }

        if (sslMode == 3) {
            // this is only true if the ssl-mode indicates ftps.
            serverSocket = DefaultSSLSocketFactory.getFactory().createServerSocket(port, 50, bindInetAddress);
        } else {
            serverSocket = new ServerSocket(port, 50, bindInetAddress);
        }
    }

    @Deprecated
    protected void createServerSocket(FtpdSettings settings) throws ConfigurationException, IOException {
        String tba = settings.get("/main/bind_address");
        // actually, .getLocalHost() seems to take the external address, which is great!

        InetAddress bindAddress;
        if ("localhost".equals(tba)) {
            bindAddress = InetAddress.getLocalHost(); // if we don't do this, we get the 0.0.0.0 address, which is useless
        } else if ("".equals(tba) || tba == null) {
            // if we don't do this, then we can't connect to "localhost".
            bindAddress = null;
        } else {
            try {
                bindAddress = InetAddress.getByName(tba);
            } catch (UnknownHostException e) {
                throw new ConfigurationException("Could not bind to bind_address.", e);
            }
        }
        int port = settings.getInt("/main/port");

        running = false;
        if (serverSocket != null) {
            serverSocket.close();
        }

        if (settings.getInt("/ssl/mode") == 3) {
            // this is only true if the ssl-mode indicates ftps.
            serverSocket = DefaultSSLSocketFactory.getFactory().createServerSocket(port, 50, bindAddress);
        } else {
            serverSocket = new ServerSocket(port, 50, bindAddress);
        }
    }

    private void start() {
        running = true;
        listener = new Thread(this, "Server-Accept");
        listener.start();
        Logging.getSecurityLog().start();
    }

    public void run() {
        listen();
    }

    public void listen() {
        while(running) {
            try {
                // _todo: have a limit on how many users can be logged on at the same time
                // done in Connection
                Socket client = serverSocket.accept();
                createConnection(client);
            } catch (IOException e) {
                // this is ok, this means that we restarted
            }
        }
    }

    public Semaphore getUploadSemaphore() {
        return uploadSemaphore;
    }

    public Semaphore getDownloadSemaphore() {
        return downloadSemaphore;
    }

    public void removeConnection(Connection connection) {
        connections.remove(connection);
    }

    public Set<Connection> getConnections() {
        return connections;
    }

    public long getStartTime() {
        return startTime;
    }

    public long getTotalNumberOfClients() {
        return clientId.longValue();
    }

    /**
     * Counts the number of sessions a user currently has.
     *
     * @param username the username of the user who's sessions we will count.
     * @return the number of active sessions for the specified user.
     */
    public int getNumberOfConnectionsForUser(String username) {
        synchronized(connections) {
            int i = 0;
            for (Connection c : connections) {
                if (c.getUser() != null && c.getUser().getUsername().equals(username)) {
                    // it can be null before the user has managed to log on
                    i++;
                }
            }
            return i;
        }
    }

    /**
     * Terminates all connections where the username matches the supplied username.
     *
     * @param username the username of the user to kick.
     * @param reason the reason for terminating the connection
     */
    public void kick(String username, String reason) {
        synchronized (connections) {
            Iterator<Connection> i = connections.iterator();
            while (i.hasNext()) {
                Connection conn = i.next();
                if (conn.getUser().getUsername().equals(username)) {
                    i.remove();
                    conn.respond("421 Your connection has been terminated by the server " + (reason == null ? "." : reason));
                    conn.shutdown();
                }
            }
        }
    }

    /**
     * Terminates a user connection based on the connection id.
     * These connection ids can be found via "site xwho".
     *
     * @param connectionId the id of the connection to kick.
     * @param reason the reason for terminating the connection
     * @return the username of the user who was connected, or "n/a" if the connection did not yet have a user.
     */
    public String kick(long connectionId, String reason) {
        synchronized (connections) {
            String username = "n/a";
            Iterator<Connection> i = connections.iterator();
            while (i.hasNext()) {
                Connection conn = i.next();
                if (conn.getConnectionId() == connectionId) {
                    i.remove();
                    User user = conn.getUser();
                    if (user != null) {
                        username = user.getUsername();
                    }
                    conn.respond("421 Your connection has been terminated by the server " + (reason == null ? "." : ". Reason: " + reason));
                    conn.shutdown();
                    break;
                }
            }
            return username;
        }
    }

    /**
     * This method suspends the server. While suspended the server will not service any requests.
     * This is used when the connection to a remote userbase is severed.
     * When the server is suspended, users wil be greeted with a specified message.
     *
     */
    public void suspend() {
        suspended = true;
    }

    public void unsuspend() {
        suspended = false;
    }

    public boolean isSuspended() {
        return suspended;
    }

    public Timer getTimer() {
        return timer;
    }

    public String getVersion() {
        final Package pkg = Server.class.getPackage();
        return pkg.getSpecificationTitle() + "-" + pkg.getSpecificationVersion() + " (" + pkg.getImplementationVersion() + " @ " + pkg.getImplementationTitle() + ")";
    }
   
    public synchronized void shutdown(User user) {
        // stop more connections from coming in
        if (running) { // only shut down if we are not already shutting down.
            running = false;
            if (serverSocket != null) {
                // this happens if the server socket hasn't yet been created (due to it already being bound), and the shutdown hook is invoked (because it shuts down)
                try {
                    serverSocket.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
            // kick everybody (and account for credit gain/loss)
            synchronized (connections) {
                Iterator<Connection> i = connections.iterator();
                while (i.hasNext()) {
                    Connection conn = i.next();
                    // drop all connections
                    i.remove(); // note, it MUST be removed first, before it is terminated. Otherwise we get a ConcurrentModificationException
                    conn.shutdown();
                    //conn.terminate(false);
                }
            }

            if (Logging.getSecurityLog() != null) {
                Logging.getSecurityLog().stop(user);
                // it can be null in rare cases if we crash in the middle of starting up
            }
            // shut down
            Logging.shutdown(); // NOTE: always save logging last, since we might want to log complications when saving

            ServiceManager.shutdown(); // Shuts down all the services

            timer.cancel();
            online = false;
            synchronized (forever) {
                forever.notify();
            }
        }
    }

    public void init() {
        try {
            Runtime.getRuntime().addShutdownHook(new ServerShutdownHook(this));
            configure();
            start();
            System.out.println(getVersion() + " online");
            // we have to wait after, otherwise we might throw an exception in start() when we've already started waiting
            // but no, .start() can't throw exceptions ya dummy!
            // waiting after the catch hangs cuftpd if a configuration exception occurs
            synchronized (forever) {
                try {
                    while(online) {
                        // todo: while this works just fine, why do we do it? Why don't we have the wait loop on the main thread?
                        forever.wait();
                    }
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        } catch (UnknownPermissionException e) {
            System.err.println("You have tried to use an undefined permission in data/permissions.acl");
            System.err.println(e.getMessage());
        } catch (ConnectIOException e) {
            System.err.println("Something went wrong when connecting to the remote userbase. Try verifying the /ftpd/ssl/truststore/password setting.");
            System.err.println("Error was: " + e.getMessage());
            if (e.getCause() != null) {
                System.err.println("Caused by: " + e.getCause().getMessage());
            }
        } catch (IOException e) {
            System.err.println("An I/O error occurred: " + e.getMessage());
            if (e.getCause() != null) {
                System.err.println("Caused by: " + e.getCause().getMessage());
            }
            e.printStackTrace();
        } catch (ConfigurationException e) {
            System.err.println("Error in configuration: " + e.getMessage());
            e.printStackTrace();
        } catch (PermissionConfigurationException e) {
            System.err.println("There was an error when parsing the permissions.");
            System.err.println("The error was: " + e.getMessage());
            System.err.println("Line number: " + e.getLineNumber());
            System.err.println("The error was on line: " + e.getLine());
        } catch (Throwable e) {
            System.err.println("An unknown error occurred!");
            e.printStackTrace();
        }
        System.out.println(getVersion() + " terminated");
    }
   
    public static void main(String[] args) {
        String configurationFile = "data/cuftpd.xml";
        if (args.length == 1) {
            configurationFile = args[0];
             if ("-version".equalsIgnoreCase(args[0])) {
                Server s = new Server(configurationFile);
                System.out.println(s.getVersion());
                return;
            }
        }
        instance = new Server(configurationFile);
        instance.init();
    }
}

TOP

Related Classes of cu.ftpd.Server

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.