/* A simple, multithreaded anonymous ftp server as per RFC 959. */ import java.io.*; import java.net.*; import java.util.*; import java.lang.reflect.*; // manage a single ftp connection public class FtpServer { /* Under no circumstances accidentally define public methods whose * name starts with "handle"---they automatically become ftp server * commands and can therefore may become security holes. */ private BufferedReader controlReader; private PrintWriter controlPrintWriter; private AReplier replier; private FtpServerConfig config; private boolean isLoggedIn = false, isAuthenticated = false; private boolean isAnonymous = false; private File rootDirectory; private File currentDirectory; private Socket dataSocket = null; // create a single-connection server public FtpServer(FtpServerConfig config, InputStream inputStream, OutputStream outputStream) throws IOException { this.config = config; setControl(inputStream, outputStream); handleConnection(); } // set the input and output streams for the control connection public void setControl(InputStream inputStream, OutputStream outputStream) throws IOException { try controlReader = new BufferedReader(new InputStreamReader(inputStream, "8859_1")); catch (UnsupportedEncodingException exception) { // this cannot happen } controlPrintWriter = new PrintWriter(outputStream); replier = new Replier(controlPrintWriter); } // handle a connection public void handleConnection() throws IOException { displayBanner(); handleCommands(); } // display a greeting message public void displayBanner() { registerReply (220, "Scheme Untergrund ftp server (Java version) ready."); } // command handlers throw this when they want the connection terminated class FtpServerQuit extends Exception { } // central command loop public void handleCommands() throws IOException { for (;;) { flushReplies(); try acceptCommand(); catch (FtpServerQuit exception) { flushReplies(); break; } } } // accept a single command public void acceptCommand() throws IOException, FtpServerQuit { String commandLine = CrLf.readLine(controlReader); ACommandPacket commandPacket = new CommandPacket(commandLine); handleCommand(commandPacket.getCommand(), commandPacket.getArgument()); } class FtpServerUnknownCommand extends Exception { } // handlers throw this after registering an error message class FtpServerException extends Exception { } // handle a command public void handleCommand(String command, String argument) throws IOException, FtpServerQuit { try dispatchCommand(command, argument); catch (FtpServerException exception) { // ignore } catch (FtpServerQuit exception) { throw exception; } catch (FtpServerUnknownCommand exception) { registerReply(500, "Unknown command: \"" + command + (argument.equals("") ? "\"." : "\" (argument(s) \"" + argument + "\").")); } catch (IOException exception) { registerReply(451, "I/O error: " + exception.getMessage()); } catch (Exception exception) { registerReply(451, "Internal error: " + exception.getMessage()); } } // dispatch a command XXX to the a handler method handleXXX public void dispatchCommand(String command, String argument) throws IOException, FtpServerException, FtpServerQuit, FtpServerUnknownCommand { final Class thisClass = this.getClass(); final Class[] handlerArgumentTypes = { String.class }; Object [] handlerArguments = { argument }; try { Method handler = thisClass.getMethod("handle" + command, handlerArgumentTypes); handler.invoke(this, handlerArguments); } catch (NoSuchMethodException exception) { throw new FtpServerUnknownCommand(); } catch (SecurityException exception) { throw new FtpServerException(); } catch (InvocationTargetException exception) { Throwable realException = exception.getTargetException(); Class realClass = realException.getClass(); if (realClass == FtpServerQuit.class) throw (FtpServerQuit) realException; else if (realClass == IOException.class) throw (IOException) realException; else throw (FtpServerException) realException; } catch (IllegalAccessException exception) { throw new FtpServerException(); } } // Handlers public void handleNOOP(String argument) throws IOException { registerReply(200, "Done nothing, but successfully."); } public void handleQUIT(String argument) throws IOException, FtpServerQuit { registerReply(221, "Goodbye! Au revoir! Auf Wiedersehen!"); throw new FtpServerQuit(); } public void handleUSER(String name) throws IOException { if (isLoggedIn) registerReply(230, "You are already logged in."); else if (name.equals("anonymous") || name.equals("ftp")) handleAnonymous(); else registerReply(530, "Only anonymous logins allowed."); } public void handleAnonymous() throws IOException { isLoggedIn = true; isAuthenticated = true; isAnonymous = true; rootDirectory = config.anonymousRootDirectory; currentDirectory = new File(""); registerReply(230, "Anonymous user logged in."); } public void handlePASS(String password) throws IOException { if (!isLoggedIn) registerReply(530, "You have not logged in yet."); else if (isAnonymous) registerReply(200, "Thank you."); else registerReply(230, "This can't happen."); } public void handleSYST(String foo) throws IOException { registerReply(215, "UNIX Type: L8"); } public void handlePWD(String foo) throws IOException, FtpServerException { ensureAuthenticatedLogin(); registerReply(257, "Current directory is \"" + currentDirectory.getPath() + "\"."); } public void handleCWD(String path) throws IOException, FtpServerException { ensureAuthenticatedLogin(); { String directoryPath = assemblePath(path); if (!(new File(rootDirectory, directoryPath)).isDirectory()) signalError(550, "Can't change to directory \"" + path + "\"."); currentDirectory = new File(directoryPath); registerReply(250, "Current directory changed to \"" + currentDirectory.getPath() + "\"."); } } private static final int ASCII = 0, IMAGE = 1; private int type = ASCII; public void handleTYPE(String arg) throws FtpServerException { if (arg.equalsIgnoreCase("A")) type = ASCII; else if (arg.equalsIgnoreCase("I")) type = IMAGE; else if (arg.equalsIgnoreCase("L8")) type = IMAGE; else signalError(504, "Unknown TYPE: " + arg + "."); registerReply(200, "TYPE is now " + ((type == ASCII) ? "ASCII" : (type == IMAGE ? "8-bit binary" : "unknown")) + "."); } public void handlePORT(String stuff) throws FtpServerException, IOException { ensureAuthenticatedLogin(); maybeCloseDataConnection(); { APortSpecification portSpecification = new PortSpecification(stuff); dataSocket = new Socket(portSpecification.getInetAddress(), portSpecification.getPort()); registerReply(200, "Connected to " + portSpecification.toString() + "."); } } interface APortSpecification { public int getPort(); public InetAddress getInetAddress(); // for human readers public String toString(); } // represent ftp-style, comma-separated address/port specifications class PortSpecification implements APortSpecification { private int port; private InetAddress inetAddress; public PortSpecification(String description) throws FtpServerException, UnknownHostException { StringTokenizer tokenizer = new StringTokenizer(description, ","); int[] components = new int[6]; int count; for (count = 0; count < 6; ++count) { if (!tokenizer.hasMoreTokens()) signalError(500, "Syntax error in arguments to PORT."); { String componentString = tokenizer.nextToken(); try { int component = (new Integer(componentString)).intValue(); if (component < 0 || component > 255) throw new FtpServerException(); components[count] = component; } catch (Exception exception) { signalError(501, "Invalid arguments to PORT."); throw new FtpServerException(); } } } inetAddress = InetAddress.getByName(String.valueOf(components[0]) + "." + String.valueOf(components[1]) + "." + String.valueOf(components[2]) + "." + String.valueOf(components[3])); port = (components[4] << 8) + components[5]; } public InetAddress getInetAddress() { return inetAddress; } public int getPort() { return port; } // for human readers public String toString() { return inetAddress.getHostAddress() + ", port " + String.valueOf(port); } } public void handleNLST(String directoryPath) throws FtpServerException, IOException { ensureAuthenticatedLogin(); ensureDataConnection(); try { String path = assemblePath(directoryPath); File directory = new File(rootDirectory, path); String[] files = directory.list(); int count; PrintWriter dataPrintWriter = new PrintWriter(dataSocket.getOutputStream()); for (count = 0; count < files.length; ++count) { dataPrintWriter.print(files[count]); CrLf.writeln(dataPrintWriter); } } finally { maybeCloseDataConnection(); } } public void handleRETR(String path) throws FtpServerException, IOException { ensureAuthenticatedLogin(); { String fullPath = assemblePath(path); File file = new File(rootDirectory, fullPath); if (!file.canRead()) signalError(450, "Cannot open \"" + path + "\" for reading."); try { FileInputStream inputStream = new FileInputStream(file); ensureDataConnection(); try { switch (type) { case IMAGE: StreamCopy.copyStreamToStreamBinary(inputStream, dataSocket.getOutputStream()); case ASCII: StreamCopy.copyStreamToStreamCrLf(inputStream, dataSocket.getOutputStream()); } } finally { inputStream.close(); } } finally { maybeCloseDataConnection(); } } } // assemble a path relative to the server root public String assemblePath(String suffix) throws FtpServerException { File path = FileUtils.addPath(currentDirectory, suffix); if (path == null) signalError(550, "\"" + suffix + "\" is an invalid path name."); return path.getPath(); } public void ensureAuthenticatedLogin() throws FtpServerException { if (!isLoggedIn || !isAuthenticated) signalError(530, "You're not logged in yet."); } // data connection managment public void ensureDataConnection() throws FtpServerException, IOException { if (dataSocket == null) signalError(425, "No data connection."); registerReply(150, "Opening data connection."); flushReplies(); } public void maybeCloseDataConnection() throws IOException { if (dataSocket != null) closeDataConnection(); } public void closeDataConnection() throws IOException { dataSocket.close(); registerReply(226, "Closing data connection."); dataSocket = null; } public void registerReply(int code, String message) { replier.registerReply(code, message); } public void signalError(int code, String message) throws FtpServerException { registerReply(code, message); throw new FtpServerException(); } public void flushReplies() throws IOException { replier.flushReplies(); } } // deal with rooted File objects class FileUtils { /* Generally, portable code dealing with paths is not possible in Java. That's mainly because path name normalization is inextricably tied to path canonicalization i.e. symlink chasing. The upshot is that, if you don't get parentName right, you may have a security hole. You should be safe on Unix and DOS, however. */ static final String parentName = ".."; // carefully add a path to file, making sure that the resulting File object // is not above file in the hierarchy. public static File addPath(File file, String path) { Vector componentVector = new Vector(16); final char separator = System.getProperty("file.separator").charAt(0); if (path.equals("")) return file; if (path.charAt(0) == separator) file = new File(""); { int componentIndex = 0; for (;;) { int separatorIndex = path.indexOf(separator, componentIndex); if (separatorIndex == -1) { componentVector.addElement(path.substring(componentIndex)); break; } if (componentIndex == separatorIndex) { ++componentIndex; continue; } componentVector.addElement(path.substring(componentIndex, separatorIndex)); componentIndex = separatorIndex + 1; } } return addComponents(file, componentVector); } public static File addComponents(File file, Vector componentVector) { Enumeration components = componentVector.elements(); while (components.hasMoreElements()) { String component = (String) components.nextElement(); if (component.equals(parentName)) { String parent = file.getParent(); if (parent == null) return null; file = new File(parent); } else file = new File(file, component); } return file; } } // transporting stuff across a data connection class StreamCopy { private static final int windowSize = 4096; private static byte[] window = new byte[windowSize]; public static void copyStreamToStreamBinary(InputStream inputStream, OutputStream outputStream) throws IOException { for (;;) { int length = inputStream.read(window); if (length == -1) break; outputStream.write(window, 0, length); } } public static void copyStreamToStreamCrLf(InputStream inputStream, OutputStream outputStream) throws IOException { /* exercise */ } } // read and write CRLF-separated lines class CrLf { private static final int cr = 13, lf = 10; public static void writeln(PrintWriter printWriter) throws IOException { printWriter.write(cr); printWriter.write(lf); printWriter.flush(); } public static String readLine(BufferedReader bufferedReader) throws IOException { StringBuffer buffer = new StringBuffer(80); boolean seenCr = false; for (;;) { int ch = bufferedReader.read(); switch (ch) { case -1: return (buffer.length() != 0) ? buffer.toString() : null; case CrLf.cr: if (seenCr) buffer.append((char) ch); seenCr = true; break; case CrLf.lf: if (seenCr) return buffer.toString(); else buffer.append((char) ch); break; default: buffer.append((char) ch); } } } } interface AReplier { public void registerReply(int code, String blurb); public void flushReplies() throws IOException; } // ftp-style reply message class Replier implements AReplier { private Vector messageVector; private int replyCode; private PrintWriter printWriter; public Replier(PrintWriter printWriter) { messageVector = new Vector(16); this.printWriter = printWriter; } public void registerReply(int code, String blurb) { messageVector.addElement(blurb); replyCode = code; } public void writeNonFinalReply(String blurb) throws IOException { printWriter.print(Integer.toString(replyCode)); printWriter.print('-'); printWriter.print(blurb); CrLf.writeln(printWriter); } public void writeFinalReply(String blurb) throws IOException { printWriter.print(Integer.toString(replyCode)); printWriter.print(" "); printWriter.print(blurb); CrLf.writeln(printWriter); } public void flushReplies() throws IOException { if (!messageVector.isEmpty()) { Enumeration blurbs = messageVector.elements(); while (blurbs.hasMoreElements()) { String blurb = (String) blurbs.nextElement(); if (blurbs.hasMoreElements()) writeNonFinalReply(blurb); else writeFinalReply(blurb); } } messageVector.removeAllElements(); } } // server configuration options; essentially a record class FtpServerConfig { public File anonymousRootDirectory; public int port; public FtpServerConfig(int port, File anonymousRootDirectory) { this.port = port; this.anonymousRootDirectory = anonymousRootDirectory; } } interface ACommandPacket { public String getCommand(); public String getArgument(); } class CommandPacket implements ACommandPacket { private String command; private String argument; public CommandPacket(String commandLine) { if (commandLine == null) { // Netscape does this command = "QUIT"; argument = ""; } else { commandLine = commandLine.trim(); { int splitPosition = commandLine.indexOf(' '); if (splitPosition != -1) { command = commandLine.substring(0, splitPosition).toUpperCase(); argument = commandLine.substring(splitPosition + 1).trim(); } else { command = commandLine.toUpperCase(); argument = ""; } } } } public String getCommand() { return command; } public String getArgument() { return argument; } } // ftp daemon public class FtpD { // standalone public FtpD(int port, File anonymousRootDirectory) throws IOException { final FtpServerConfig config = new FtpServerConfig(port, anonymousRootDirectory); ServerSocket serverSocket = new ServerSocket(port); for (;;) { final Socket connectionSocket = serverSocket.accept(); class Spawnable implements Runnable { public void run () { try { FtpServer server = new FtpServer(config, connectionSocket.getInputStream(), connectionSocket.getOutputStream()); connectionSocket.close(); } catch (IOException exception) { return; } } } new Thread(new Spawnable()).start(); } } // demo version public static void main(String[] args) throws IOException { new FtpD(8080, new File("/home/sperber")); } }