/**
* 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();
}
}