package cu.ftpd;
import cu.ftpd.commands.site.SiteCommandHandler;
import cu.ftpd.commands.site.actions.*;
import cu.ftpd.commands.transfer.TransferPostProcessor;
import cu.ftpd.commands.transfer.TransferPostProcessorAdapter;
import cu.ftpd.events.*;
import cu.ftpd.filesystem.metadata.MetadataHandler;
import cu.ftpd.filesystem.permissions.PermissionConfigurationException;
import cu.ftpd.filesystem.permissions.Permissions;
import cu.ftpd.logging.Logging;
import cu.ftpd.modules.Module;
import cu.ftpd.modules.zipscript.ZipscriptModule;
import cu.ftpd.persistence.Saver;
import cu.ftpd.user.statistics.UserStatistics;
import cu.ftpd.user.statistics.local.LocalUserStatistics;
import cu.ftpd.user.statistics.none.NoUserStatistics;
import cu.ftpd.user.statistics.remote.client.RmiUserStatisticsClient;
import cu.ftpd.user.userbases.Userbase;
import cu.ftpd.user.userbases.actions.*;
import cu.ftpd.user.userbases.anonymous.AnonymousUserbase;
import cu.ftpd.user.userbases.changetracking.AsynchronousMessageQueueChangeTracker;
import cu.ftpd.user.userbases.changetracking.Change;
import cu.ftpd.user.userbases.changetracking.ChangeApplicator;
import cu.ftpd.user.userbases.changetracking.ChangeTracker;
import cu.ftpd.user.userbases.local.LocalUserbase;
import cu.ftpd.user.userbases.remote.client.RmiRemoteUserbase;
import cu.settings.ConfigurationException;
import cu.settings.XMLSettings;
import java.io.*;
import java.net.MalformedURLException;
import java.net.URI;
import java.net.URL;
import java.net.URLClassLoader;
import java.rmi.NotBoundException;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
/**
* @author captain
* @version $Id: Services.java 296 2009-03-06 23:22:31Z jevring $
* @since 2008-okt-25 - 01:27:17
*/
public class Services {
protected FtpdSettings settings;
protected SiteCommandHandler siteCommandHandler;
protected URLClassLoader customClassLoader;
protected EventHandler eventHandler;
protected Userbase userbase;
protected Map<String, Module> modules = new HashMap<>();
protected Permissions permissions;
protected MetadataHandler metadataHandler;
protected UserStatistics userStatistics;
protected TransferPostProcessor transferPostProcessor = new TransferPostProcessorAdapter(); // this is a fallback in case we don't load any other module for this purpose
protected AsynchronousMessageQueueChangeTracker changeTracker;
protected Services(){} // For use by CubncServices
public Services(FtpdSettings settings) throws IOException, ClassNotFoundException, InstantiationException, IllegalAccessException, NotBoundException, ConfigurationException, PermissionConfigurationException {
this.settings = settings;
configureMetadataHandler();
configureCustomClassLoader();
this.siteCommandHandler = new SiteCommandHandler(settings);
initializeCustomSiteCommands();
configureEventHandler();
configureUserbase();
siteCommandHandler.registerAction("chown", new Chown()); // note: only use this when we have disk (cuftpd) AND a cuftpd userbase. (create a different method body when we're doing a CubncServices object)
configureUserStatistics();
configureModules();
if (modules.containsKey("zipscript")) {
Module zipscript = modules.get("zipscript");
if (zipscript instanceof ZipscriptModule) {
ZipscriptModule zm = (ZipscriptModule)zipscript;
setTransferPostProcessor(zm.getZipscript());
}
}
initializePermissions();
initializeLocalhostCommands();
}
/**
* Commands that only make sense on localhost, since they involve things related to the disk.
*/
private void initializeLocalhostCommands() {
siteCommandHandler.registerAction("wipe", new Wipe());
siteCommandHandler.registerAction("stat", new Stat()); // statline: section, credits, etc.
siteCommandHandler.registerAction("traffic", new Traffic());
}
// Settings
public FtpdSettings getSettings() {
return settings;
}
// SiteCommandHandler
public SiteCommandHandler getSiteCommandHandler() {
return siteCommandHandler;
}
public void initializeCustomSiteCommands() throws ClassNotFoundException, IllegalAccessException, InstantiationException {
// reads the commands from cuftpd.xml and adds them as available commands.
// NOTE: commands with the same name as built in commands will be overwritten
String path;
String name;
String type;
boolean addResponseCode;
int i = 1;
Map<String, Action> instances = new HashMap<>();
while(true) {
// loop over the sections in the file
name = settings.get("/commands/command[" + i + "]/name");
if (name == null || "".equals(name)) {
break;
}
type = settings.get("/commands/command[" + i + "]/type");
if (type == null || "".equals(type)) {
type = "shell";
}
path = settings.get("/commands/command[" + i + "]/path");
addResponseCode = settings.getBoolean("/commands/command[" + i + "]/add_response_code");
//System.out.println(System.getProperty("user.dir"));
i++;
if ("java".equals(type)) {
Action action = instances.get(path);
if (action == null) {
// use a custom class loader to load the file, in case we are using classes from another jar.
Class c = customClassLoader.loadClass(path);
//action = (Action)Class.forName(path).newInstance();
action = (Action)c.newInstance();
instances.put(path, action);
}
siteCommandHandler.registerAction(name, action);
} else if ("shell".equals(type)){
File f = new File(path);
siteCommandHandler.registerAction(name, new ShellExecute(name, f.getAbsolutePath(), addResponseCode));
} else {
siteCommandHandler.registerCustomAction(name, path, type, addResponseCode);
}
// todo: we should log that this has been added
}
}
// Transfer post processor
/**
* Sets the TransferPostProcessor to be used by the system.
* NOTE: This does NOT change the transfer post processors already in use by connections
* but only the one used for new connections.
*
* @param tpp the post processor to be used. <code>null</code> if a default adapter that does nothing is to be used.
*/
public void setTransferPostProcessor(TransferPostProcessor tpp) {
if (tpp == null) {
this.transferPostProcessor = new TransferPostProcessorAdapter();
} else {
this.transferPostProcessor = tpp;
}
}
public TransferPostProcessor getTransferPostProcessor() {
return transferPostProcessor;
}
// Userbase
protected void configureUserbase() throws ConfigurationException, NotBoundException, IOException {
switch(settings.getInt("/user/authentication/type")) {
case Userbase.SQL:
case Userbase.RMI:
System.out.println("Initializing REMOTE userbase");
if (settings.get("/user/authentication/remote/host") == null || settings.get("/user/authentication/remote/port") == null || settings.get("/user/authentication/remote/retry_interval") == null) {
throw new ConfigurationException("must specify {host, port, retry_interval} when using remote statistics");
}
userbase = new RmiRemoteUserbase(settings.get("/user/authentication/remote/host"), settings.getInt("/user/authentication/remote/port"), settings.getInt("/user/authentication/remote/retry_interval"));
initializeCuftpdUserbaseActions();
break;
case Userbase.ANONYMOUS:
System.out.println("Initializing ANONYMOUS userbase");
userbase = new AnonymousUserbase();
break;
case Userbase.ASYNCHRONOUS:
System.out.println("Initializing ASYNCHRONOUS userbase");
try {
String name = settings.get("/user/authentication/asynchronous/name");
String uri = settings.get("/user/authentication/asynchronous/uri");
AsynchronousMessageQueueChangeTracker changeTracker = new AsynchronousMessageQueueChangeTracker(URI.create(uri), name);
this.changeTracker = changeTracker;
userbase = new LocalUserbase(settings.getDataDirectory(), true, changeTracker);
ChangeApplicator changeApplicator = new ChangeApplicator((LocalUserbase)userbase);
int i = 1;
String peerName;
String peerUri;
while(true) {
// loop over the sections in the file
peerName = settings.get("/user/authentication/asynchronous/peers/peer[" + i + "]/name");
if (peerName == null || "".equals(peerName)) {
break;
}
peerUri = settings.get("/user/authentication/asynchronous/peers/peer[" + i + "]/uri");
i++;
if (!peerUri.startsWith("failover:")) {
peerUri = "failover:" + peerUri;
}
changeTracker.addPeer(peerName, URI.create(peerUri), changeApplicator);
// todo: log each peer we connect to
}
initializeCuftpdUserbaseActions();
break;
} catch (Exception e) {
shutdown();
throw new ConfigurationException("Asynchronous userbase failure", e);
}
case Userbase.DEFAULT:
default:
System.out.println("Initializing LOCAL userbase");
userbase = new LocalUserbase(settings.getDataDirectory(), true, new ChangeTracker() {
@Override
public void addChange(Change change) {
// do nothing...
}
});
initializeCuftpdUserbaseActions();
break;
}
}
protected void initializeCuftpdUserbaseActions() {
// todo: "site help" should depend on which commands are actually added (same for "feat", actually, but that'll be harder, since we don't load commands like classes
Action seen = new Seen();
siteCommandHandler.registerAction("seen", seen);
siteCommandHandler.registerAction("laston", seen);
siteCommandHandler.registerAction("lastlog", seen);
siteCommandHandler.registerAction("users", new Users());
siteCommandHandler.registerAction("give", new Credits(true));
siteCommandHandler.registerAction("take", new Credits(false));
siteCommandHandler.registerAction("addgadmin", new Gadmin(true));
siteCommandHandler.registerAction("delgadmin", new Gadmin(false));
siteCommandHandler.registerAction("addgroup", new AddGroup());
siteCommandHandler.registerAction("delgroup", new DelGroup());
siteCommandHandler.registerAction("autg", new AddUserToGroup());
siteCommandHandler.registerAction("rufg", new RemoveUserFromGroup());
siteCommandHandler.registerAction("user", new ViewUser(new File(settings.getDataDirectory(), "/templates/user_template.txt")));
siteCommandHandler.registerAction("addip", new AddIP());
siteCommandHandler.registerAction("delip", new DelIP());
siteCommandHandler.registerAction("group", new ViewGroup(new File(settings.getDataDirectory(), "/templates/group_template.txt")));
siteCommandHandler.registerAction("groups", new Groups());
siteCommandHandler.registerAction("groupchange", new GroupChange());
siteCommandHandler.registerAction("gadduser", new Gadduser());
siteCommandHandler.registerAction("adduser", new AddUser());
siteCommandHandler.registerAction("deluser", new DelUser());
siteCommandHandler.registerAction("change", new UserChange());
siteCommandHandler.registerAction("passwd", new Passwd());
siteCommandHandler.registerAction("tagline", new Tagline());
siteCommandHandler.registerAction("allotments", new Allotments());
siteCommandHandler.registerAction("leechers", new Leechers());
siteCommandHandler.registerAction("primarygroup", new PrimaryGroup());
siteCommandHandler.registerAction("permissions", new cu.ftpd.user.userbases.actions.Permissions());
siteCommandHandler.registerAction("addpermissions", new ModifyPermissions(true));
siteCommandHandler.registerAction("delpermissions", new ModifyPermissions(false));
}
public Userbase getUserbase() {
return userbase;
}
// User Statistics
protected void configureUserStatistics() throws ConfigurationException, IOException {
boolean registerStatisticsCommands = true;
switch (settings.getInt("/user/statistics/type")) {
case 0: // none
registerStatisticsCommands = false;
userStatistics = new NoUserStatistics();
break;
case 2:
if (settings.get("/user/statistics/remote/host") == null || settings.get("/user/statistics/remote/port") == null || settings.get("/user/statistics/remote/retry_interval") == null) {
throw new ConfigurationException("must specify {host, port, retry_interval} when using remote statistics");
}
userStatistics = new RmiUserStatisticsClient(settings.get("/user/statistics/remote/host"), settings.getInt("/user/statistics/remote/port"), settings.getInt("/user/statistics/remote/retry_interval"));
break;
case 1:
default:
userStatistics = new LocalUserStatistics(new File(settings.getDataDirectory(), "/logs/userstatistics"));
break;
}
if (registerStatisticsCommands) {
// without any statistics, we don't want these commands here.
// otherwise, things like cubnc using cuftpd will have commands blocking
// the slave commands.
initializeUserStatisticsActions();
}
}
protected void initializeUserStatisticsActions() {
siteCommandHandler.registerAction("allup", new Statistics(UserStatistics.ALLUP_BYTES, false, "allup"));
siteCommandHandler.registerAction("alldn", new Statistics(UserStatistics.ALLDN_BYTES, false, "alldn"));
siteCommandHandler.registerAction("mnup", new Statistics(UserStatistics.MNUP_BYTES, false, "mnup"));
siteCommandHandler.registerAction("mndn", new Statistics(UserStatistics.MNDN_BYTES, false, "mndn"));
siteCommandHandler.registerAction("wkup", new Statistics(UserStatistics.WKUP_BYTES, false, "wkup"));
siteCommandHandler.registerAction("wkdn", new Statistics(UserStatistics.WKDN_BYTES, false, "wkdn"));
siteCommandHandler.registerAction("dayup", new Statistics(UserStatistics.DAYUP_BYTES, false, "dayup"));
siteCommandHandler.registerAction("daydn", new Statistics(UserStatistics.DAYDN_BYTES, false, "daydn"));
siteCommandHandler.registerAction("gpallup", new Statistics(UserStatistics.ALLUP_BYTES, true, "gpallup"));
siteCommandHandler.registerAction("gpalldn", new Statistics(UserStatistics.ALLDN_BYTES, true, "gpalldn"));
siteCommandHandler.registerAction("gpmnup", new Statistics(UserStatistics.MNUP_BYTES, true, "gpmnup"));
siteCommandHandler.registerAction("gpmndn", new Statistics(UserStatistics.MNDN_BYTES, true, "gpmndn"));
siteCommandHandler.registerAction("gpwkup", new Statistics(UserStatistics.WKUP_BYTES, true, "gpwkup"));
siteCommandHandler.registerAction("gpwkdn", new Statistics(UserStatistics.WKDN_BYTES, true, "gpwkdn"));
siteCommandHandler.registerAction("gpdayup", new Statistics(UserStatistics.DAYUP_BYTES, true, "gpdayup"));
siteCommandHandler.registerAction("gpdaydn", new Statistics(UserStatistics.DAYDN_BYTES, true, "gpdaydn"));
}
public UserStatistics getUserStatistics() {
return userStatistics;
}
// Permissions
public synchronized void initializePermissions() throws PermissionConfigurationException, IOException {
// check the time of last load
// if the modification time of the file is later than that, load the new file.
// else just skip
File permissionFile = new File(settings.getDataDirectory(), "permissions.acl");
if (permissions == null || permissionFile.lastModified() > permissions.getLoadTime()) {
permissions = new Permissions(permissionFile);
}
}
public Permissions getPermissions() {
return permissions;
}
// MetadataHandler
protected void configureMetadataHandler() {
metadataHandler = new MetadataHandler();
Server.getInstance().getTimer().schedule(new Saver(metadataHandler), 15000, 15000); // this doesn't actually save anything, it just clears the cache of dead things
}
public MetadataHandler getMetadataHandler() {
return metadataHandler;
}
// Modules
protected void configureModules() throws ConfigurationException {
Module module;
// _todo: use this style of configuration whereever we have enumerations, like the site-commands and the event-handlers
// actually, NO, we're not going to use the .getNode() and then iterate over them approach, because we can't get named access to any nodes, which is utterly useless.
// Node modulesNode = settings.getNode("/modules");
// The only thing we get that kind of access from is an attribute list
String moduleName;
try {
int i = 1;
String clazz;
boolean active;
while(true) {
// loop over the sections in the file
moduleName = settings.get("/modules/module[" + i + "]/name");
if (moduleName == null || "".equals(moduleName)) {
break;
}
clazz = settings.get("/modules/module[" + i + "]/class");
active = settings.getBoolean("/modules/module[" + i + "]/active");
if (active) {
// use a custom class loader to load the file, in case we are using classes from another jar.
Class c = customClassLoader.loadClass(clazz);
if (Module.class.isAssignableFrom(c)) {
module = (Module)c.newInstance();
module.initialize(new XMLSettings(settings.getNode("/modules/module[" + i + "]/settings")));
module.registerActions(siteCommandHandler);
module.registerEventHandlers(eventHandler);
modules.put(moduleName, module);
} else {
throw new ConfigurationException("Class " + clazz + " does not implement interface " + Module.class.toString());
}
}
i++;
}
} catch (ClassNotFoundException | IllegalAccessException | InstantiationException e) {
throw new ConfigurationException("Could not load class: " + e.getMessage(), e);
}
}
protected void shutdownModules() {
for (Module m : modules.values()) {
m.stop();
}
}
// Custom Class Loader
protected void configureCustomClassLoader() throws IOException {
/*
If I load a class via a class loader, then load other classes inside
that class using "new", do they then use the classloader that created
the first class? (i.e. Can we create top-level objects via out special
class loader and then use "new" in everything else?)
*/
File classpath = new File(settings.getDataDirectory(), "classpath.txt");
try (BufferedReader in = new BufferedReader(new InputStreamReader(new FileInputStream(classpath)))) {
List<URL> jars = new ArrayList<>();
String classpathEntry;
while ((classpathEntry = in.readLine()) != null) {
if (classpathEntry.startsWith("#")) {
continue;
}
if (!classpathEntry.startsWith("jar:file://")) {
classpathEntry = "jar:file://" + classpathEntry;
}
if (!classpathEntry.endsWith("!/")) {
classpathEntry += "!/";
}
jars.add(new URL(classpathEntry));
}
URL[] urls = new URL[jars.size()];
jars.toArray(urls);
customClassLoader = new URLClassLoader(urls);
} catch (MalformedURLException e) {
// while this is immediately thrown, we can't use the catch-all for this. We want it thrown all the way out.
throw e;
} catch (IOException e) {
System.err.println("Could not read " + classpath.getAbsolutePath() + ". This is not a problem unless you are loading custom scripts. Creating...");
@SuppressWarnings("unused")
final boolean created = classpath.createNewFile();
}
}
public URLClassLoader getCustomClassLoader() {
return customClassLoader;
}
// Event Handler
protected void configureEventHandler() throws ClassNotFoundException, IllegalAccessException, InstantiationException {
// EventHandler eventHandler = new EventHandler();
eventHandler = new EventHandler();
String event;
String type;
String path;
String time;
Map<String, BeforeEventHandler> beforeEventHandlers = new HashMap<>();
Map<String, AfterEventHandler> afterEventHandlers = new HashMap<>();
int i = 1;
while(true) {
event = settings.get("/events/handler[" + i + "]/event");
if (event == null || "".equals(event)) {
break;
}
type = settings.get("/events/handler[" + i + "]/type");
path = settings.get("/events/handler[" + i + "]/path");
time = settings.get("/events/handler[" + i + "]/time");
i++;
if ("before".equalsIgnoreCase(time)) {
BeforeEventHandler beh = beforeEventHandlers.get(path);
if (beh == null) {
// we didn't already have an instance of this handler, lets create one
if ("java".equalsIgnoreCase(type)) {
// use a custom class loader to load the file, in case we are using classes from another jar.
Class c = customClassLoader.loadClass(path);
//Class c = Class.forName(path);
// if we have an instance of SiteCommand (if it's some other class, we don't want to load it)
if (BeforeEventHandler.class.isAssignableFrom(c)) {
beh = (BeforeEventHandler)c.newInstance();
beforeEventHandlers.put(path, beh);
} else {
throw new ClassCastException("Class " + path + " does not implement interface cu.ftpd.events.BeforeEventHandler");
}
} else if ("shell".equalsIgnoreCase(type)) {
File f = new File(path);
beh = new ShellEventHandler(f.getAbsolutePath());
} // can't be anything else, because we'll have the XSD check it for us
}
// add it in any case, since we only care about the class, not which event it catches
eventHandler.addBeforeEventHandler(Event.resolve(event.toLowerCase()),beh);
} else if ("after".equalsIgnoreCase(time)) {
AfterEventHandler aeh = afterEventHandlers.get(path);
if (aeh == null) {
// we didn't already have an instance of this handler, lets create one
if ("java".equalsIgnoreCase(type)) {
// use a custom class loader to load the file, in case we are using classes from another jar.
Class c = customClassLoader.loadClass(path);
//Class c = Class.forName(path);
if (AfterEventHandler.class.isAssignableFrom(c)) {
aeh = (AfterEventHandler)c.newInstance();
afterEventHandlers.put(path, aeh);
} else {
throw new ClassCastException("Class " + path + " does not implement interface cu.ftpd.events.AfterEventHandler");
}
} else if ("shell".equalsIgnoreCase(type)) {
File f = new File(path);
aeh = new ShellEventHandler(f.getAbsolutePath());
} // can't be anything else, because we'll have the XSD check it for us
}
eventHandler.addAfterEventHandler(Event.resolve(event.toLowerCase()), aeh);
}
}
}
public EventHandler getEventHandler() {
return eventHandler;
}
public void shutdown() {
shutdownModules();
if (changeTracker != null) {
try {
changeTracker.stop();
} catch (Exception e) {
Logging.getErrorLog().reportException("Shutdown failure change tracker", e);
}
}
}
}