/** * Copyright (c) 2009 - 2011 AppWork UG(haftungsbeschränkt) <e-mail@appwork.org> * * This file is part of org.appwork.utils.net.ftpserver * * This software is licensed under the Artistic License 2.0, * see the LICENSE file or http://www.opensource.org/licenses/artistic-license-2.0.php * for details */ package org.appwork.utils.net.ftpserver; import java.io.BufferedReader; import java.io.BufferedWriter; import java.io.IOException; import java.io.InputStreamReader; import java.io.OutputStreamWriter; import java.net.InetSocketAddress; import java.net.ServerSocket; import java.net.Socket; import java.net.SocketAddress; import java.util.ArrayList; import org.appwork.controlling.State; import org.appwork.controlling.StateConflictException; import org.appwork.controlling.StateMachine; import org.appwork.controlling.StateMachineInterface; import org.appwork.utils.Regex; import org.appwork.utils.logging.Log; /** * @author daniel * */ public class FtpConnection implements Runnable, StateMachineInterface { public static enum COMMAND { /* commands starting with X are experimental, see RFC1123 */ ABOR(true, 0), REST(true, 1), RNTO(true, 1, -1), RNFR(true, 1, -1), DELE(true, 1, -1), XRMD(true, 1, -1), RMD(true, 1, -1), SIZE(true, 1, -1), /* rfc3659 */ STRU(true, 1), MODE(true, 1), ALLO(true, 1, -1), APPE(true, 1, -1), STOR(true, 1, -1), XMKD(true, 1, -1), MKD(true, 1, -1), NLST(true, 1, -1), EPRT(true, 1, 1), /* RFC 2428 */ EPSV(true, 0), /* RFC 2428 */ RETR(true, 1, -1), TYPE(true, 1, 2), LIST(true, 0, 1), XCUP(true, 0), CDUP(true, 0), XCWD(true, 1, -1), CWD(true, 1, -1), XPWD(true, 0), PWD(true, 0), NOOP(false, 0), PASV(true, 0), PASS(false, 1), QUIT(true, 0), SYST(true, 0), PORT(true, 1), USER(false, 1); private int paramSize; private int maxSize; private boolean needLogin; private COMMAND(final boolean needLogin, final int paramSize) { this(needLogin, paramSize, paramSize); } private COMMAND(final boolean needLogin, final int paramSize, final int maxSize) { this.paramSize = paramSize; this.needLogin = needLogin; this.maxSize = maxSize; } public boolean match(final int length) { if (length == this.paramSize) { return true; } if (length == this.maxSize) { return true; } if (this.maxSize == -1) { return true; } return false; } public boolean needsLogin() { return this.needLogin; } } private static enum TYPE { ASCII, BINARY; } private static final State IDLE = new State("IDLE"); private static final State USER = new State("USER"); private static final State PASS = new State("USER"); private static final State LOGIN = new State("USER"); private static final State LOGOUT = new State("LOGOUT"); private static final State IDLEEND = new State("IDLEEND"); static { FtpConnection.IDLE.addChildren(FtpConnection.USER); FtpConnection.USER.addChildren(FtpConnection.PASS, FtpConnection.LOGIN, FtpConnection.LOGOUT); FtpConnection.PASS.addChildren(FtpConnection.LOGIN, FtpConnection.LOGOUT); FtpConnection.LOGIN.addChildren(FtpConnection.LOGOUT); FtpConnection.LOGOUT.addChildren(FtpConnection.IDLEEND); } private final FtpServer ftpServer; private final Socket controlSocket; private BufferedReader reader; private BufferedWriter writer; private StateMachine stateMachine = null; private Thread thread = null; private String passiveIP = null; private int passivePort = 0; private TYPE type = TYPE.BINARY; private final FtpConnectionState connectionState; private Socket dataSocket = null; private ServerSocket serverSocket = null; /** * @param ftpServer * @param clientSocket * @throws IOException */ public FtpConnection(final FtpServer ftpServer, final Socket clientSocket) throws IOException { this.stateMachine = new StateMachine(this, FtpConnection.IDLE, FtpConnection.IDLEEND); this.connectionState = ftpServer.getFtpCommandHandler().createNewConnectionState(); this.ftpServer = ftpServer; this.controlSocket = clientSocket; try { this.reader = new BufferedReader(new InputStreamReader(this.controlSocket.getInputStream())); this.writer = new BufferedWriter(new OutputStreamWriter(this.controlSocket.getOutputStream())); this.thread = new Thread(ftpServer.getThreadGroup(), this) { @Override public void interrupt() { /* also close all connections on interrupt */ FtpConnection.this.closeDataConnection(); try { FtpConnection.this.controlSocket.close(); } catch (final Throwable e) { } super.interrupt(); } }; this.thread.setName("FTPConnectionThread: " + this); this.thread.start(); } catch (final IOException e) { try { this.controlSocket.close(); } catch (final Throwable e2) { } this.closeDataConnection(); throw e; } } private String buildParameter(final String[] commandParts) { if (commandParts == null) { return null; } String param = ""; for (int index = 1; index < commandParts.length; index++) { if (param.length() > 0) { param += " "; } param += commandParts[index]; } return param; } private void closeDataConnection() { try { this.dataSocket.close(); } catch (final Throwable e) { } finally { this.dataSocket = null; } try { this.serverSocket.close(); } catch (final Throwable e) { } finally { this.serverSocket = null; } } public StateMachine getStateMachine() { return this.stateMachine; } /** * @param command * @throws IOException */ private void handleCommand(final String command) throws IOException { try { final String commandParts[] = command.split(" "); COMMAND commandEnum = null; try { commandEnum = COMMAND.valueOf(commandParts[0]); } catch (final IllegalArgumentException e) { commandEnum = null; } try { if (commandEnum != null) { if (commandEnum.needLogin) { /* checks if this command needs valid login */ if (!this.stateMachine.isState(FtpConnection.LOGIN)) { throw new FtpNotLoginException(); } } if (!commandEnum.match(commandParts.length - 1)) { /* checks if the parameter syntax is okay */ throw new FtpCommandSyntaxException(); } /* this checks RNFR,RNTO command sequence */ if (this.connectionState.getRenameFile() != null && !commandEnum.equals(COMMAND.RNTO)) { /* when renameFile is set, a RNTO command MUST follow */ this.connectionState.setRenameFile(null); throw new FtpBadSequenceException(); } switch (commandEnum) { case ABOR: this.onABOR(); break; case REST: this.onREST(commandParts); break; case PASV: this.onPASV(); break; case RNTO: this.onRNTO(commandParts); break; case RNFR: this.onRNFR(commandParts); break; case XRMD: case RMD: this.onRMD(commandParts); break; case DELE: this.onDELE(commandParts); break; case SIZE: this.onSIZE(commandParts); break; case STRU: this.onSTRU(commandParts); break; case MODE: this.onMODE(commandParts); break; case ALLO: this.onALLO(); break; case APPE: this.onSTOR(commandParts, true); break; case STOR: this.onSTOR(commandParts, false); break; case XMKD: case MKD: this.onMKD(commandParts); break; case NLST: this.onNLST(commandParts); break; case EPSV: this.onEPSV(commandParts); break; case EPRT: this.onEPRT(commandParts); break; case RETR: this.onRETR(commandParts); break; case LIST: this.onLIST(commandParts); break; case USER: this.onUSER(commandParts); break; case PORT: this.onPORT(commandParts); break; case SYST: this.onSYST(); break; case QUIT: this.onQUIT(); break; case PASS: this.onPASS(commandParts); break; case NOOP: this.onNOOP(); break; case XPWD: case PWD: this.onPWD(); break; case XCWD: case CWD: this.onCWD(commandParts); break; case XCUP: case CDUP: this.onCDUP(); break; case TYPE: this.onTYPE(commandParts); break; } } else { throw new FtpCommandNotImplementedException(); } } catch (final StateConflictException e) { throw new FtpBadSequenceException(); } } catch (final FtpException e) { this.write(e.getCode(), e.getMessage()); } catch (final Throwable e) { this.write(550, e.getMessage()); } } /** * @throws IOException * */ private void onABOR() throws IOException { this.write(226, "Command okay"); } private void onALLO() throws IOException { this.write(200, "Command okay"); } private void onCDUP() throws IOException, FtpException { this.ftpServer.getFtpCommandHandler().onDirectoryUp(this.connectionState); this.write(200, "Command okay."); } private void onCWD(final String params[]) throws IOException, FtpException { this.ftpServer.getFtpCommandHandler().setCurrentDirectory(this.connectionState, this.buildParameter(params)); // this.write(250, "\"" + this.connectionState.getCurrentDir() + // "\" is cwd."); this.write(250, "Directory successfully changed."); } /** * @param commandParts * @throws FtpException * @throws FtpFileNotExistException * @throws IOException */ private void onDELE(final String[] commandParts) throws FtpFileNotExistException, FtpException, IOException { this.ftpServer.getFtpCommandHandler().removeFile(this.connectionState, this.buildParameter(commandParts)); this.write(250, "\"" + this.buildParameter(commandParts) + "\" removed."); } /** * @param commandParts * @throws IOException */ /** * RFC2428 * * @throws FtpException * * @throws FtpNotLoginException **/ private void onEPRT(final String[] commandParts) throws IOException, FtpException { final String parts[] = commandParts[1].split("\\|"); this.closeDataConnection(); if (parts.length != 4) { throw new FtpCommandSyntaxException(); } if (!"1".equals(parts[1])) { /* 2 equals IPV6 */ throw new FtpException(522, "Network protocol not supported, use (1)"); } this.passiveIP = parts[2]; this.passivePort = Integer.parseInt(parts[3]); this.write(200, "PORT command successful"); } /** * @param commandParts * @throws FtpException */ private void onEPSV(final String[] commandParts) throws FtpException { boolean okay = false; this.closeDataConnection(); try { this.serverSocket = new ServerSocket(); SocketAddress socketAddress = null; if (this.ftpServer.isLocalhostOnly()) { /* bind socket to localhost */ socketAddress = new InetSocketAddress(this.ftpServer.getLocalHost(), 0); } this.serverSocket.bind(socketAddress); okay = true; final int port = this.serverSocket.getLocalPort(); this.write(229, "Entering Extended Passive Mode (|||" + port + "|)"); return; } catch (final IOException e) { throw new FtpException(421, "could not open port"); } finally { if (!okay) { this.closeDataConnection(); } } } private void onLIST(final String params[]) throws IOException, FtpException { try { try { this.openDataConnection(); } catch (final IOException e) { throw new FtpException(425, "Can't open data connection"); } this.write(150, "Opening XY mode data connection for file list"); try { final ArrayList<? extends FtpFile> list = this.ftpServer.getFtpCommandHandler().getFileList(this.connectionState, this.buildParameter(params)); this.dataSocket.getOutputStream().write(this.ftpServer.getFtpCommandHandler().formatFileList(list).getBytes("UTF-8")); this.dataSocket.getOutputStream().flush(); } catch (final FtpFileNotExistException e) { /* need another error code here */ throw new FtpException(450, "Requested file action not taken; File unavailable"); } catch (final FtpException e) { throw e; } catch (final Exception e) { throw new FtpException(451, "Requested action aborted: local error in processing"); } /* we close the passive port after command */ this.write(226, "Transfer complete."); } finally { this.closeDataConnection(); } } /** * @param commandParts * @throws IOException * @throws FtpFileNotExistException */ private void onMKD(final String[] commandParts) throws IOException, FtpException { this.ftpServer.getFtpCommandHandler().makeDirectory(this.connectionState, this.buildParameter(commandParts)); this.write(257, "\"" + this.buildParameter(commandParts) + "\" created."); } private void onMODE(final String[] commandParts) throws IOException, FtpCommandParameterException { if ("S".equalsIgnoreCase(commandParts[1])) { this.write(200, "Command okay."); } else { throw new FtpCommandParameterException(); } } /** * @param commandParts * @throws IOException * @throws FtpException */ private void onNLST(final String[] commandParts) throws IOException, FtpException { try { try { this.openDataConnection(); } catch (final IOException e) { throw new FtpException(425, "Can't open data connection"); } this.write(150, "Opening XY mode data connection for file list"); try { final ArrayList<? extends FtpFile> list = this.ftpServer.getFtpCommandHandler().getFileList(this.connectionState, this.buildParameter(commandParts)); final StringBuilder sb = new StringBuilder(); for (final FtpFile file : list) { sb.append(file.getName()); sb.append("\r\n"); } this.dataSocket.getOutputStream().write(sb.toString().getBytes("UTF-8")); this.dataSocket.getOutputStream().flush(); } catch (final FtpFileNotExistException e) { /* need another error code here */ throw new FtpException(450, "Requested file action not taken; File unavailable"); } catch (final FtpException e) { throw e; } catch (final Exception e) { throw new FtpException(451, "Requested action aborted: local error in processing"); } /* we close the passive port after command */ this.write(226, "Transfer complete."); } finally { this.closeDataConnection(); } } private void onNOOP() throws IOException { this.write(200, "Command okay"); } private void onPASS(final String params[]) throws IOException, FtpException { this.stateMachine.setStatus(FtpConnection.PASS); if (this.connectionState.getUser() == null) { throw new FtpBadSequenceException(); } else { if (this.connectionState.getUser().getPassword() != null) { if (this.connectionState.getUser().getPassword().equals(params[1])) { final String message = this.ftpServer.getFtpCommandHandler().onLoginSuccessRequest(this.connectionState); if (message != null) { this.write(230, message, true); } this.write(230, "User logged in, proceed"); this.stateMachine.setStatus(FtpConnection.LOGIN); } else { final String message = this.ftpServer.getFtpCommandHandler().onLoginFailedMessage(this.connectionState); if (message != null) { this.write(530, message, true); } this.stateMachine.setStatus(FtpConnection.LOGOUT); this.stateMachine.setStatus(FtpConnection.IDLEEND); this.stateMachine.reset(); throw new FtpNotLoginException(); } } else { throw new RuntimeException("THIS MUST NOT HAPPEN!"); } } } /** * @throws FtpException * @throws IOException * */ private void onPASV() throws FtpException { boolean okay = false; this.closeDataConnection(); try { this.serverSocket = new ServerSocket(); SocketAddress socketAddress = null; if (this.ftpServer.isLocalhostOnly()) { /* bind socket to localhost */ socketAddress = new InetSocketAddress(this.ftpServer.getLocalHost(), 0); } this.serverSocket.bind(socketAddress); okay = true; final int port = this.serverSocket.getLocalPort(); final int p1 = port / 256; final int p2 = port - p1 * 256; if (this.ftpServer.isLocalhostOnly()) { /* localhost only */ this.write(227, "Entering Passive Mode. (127,0,0,1," + p1 + "," + p2 + ")."); } else { if (this.controlSocket.getLocalAddress().isLoopbackAddress()) { this.write(227, "Entering Passive Mode. (127,0,0,1," + p1 + "," + p2 + ")."); } else { String ip = this.controlSocket.getLocalAddress().getHostAddress(); ip = ip.replaceAll("\\.", ","); this.write(227, "Entering Passive Mode. (" + ip + "," + p1 + "," + p2 + ")."); } } return; } catch (final IOException e) { throw new FtpException(421, "could not open port"); } finally { if (!okay) { this.closeDataConnection(); } } } private void onPORT(final String params[]) throws IOException, FtpCommandSyntaxException { try { /* close old maybe existing data connection */ this.dataSocket.close(); } catch (final Throwable e) { } finally { this.dataSocket = null; } final String parts[] = params[1].split(","); if (parts.length != 6) { throw new FtpCommandSyntaxException(); } this.passiveIP = parts[0] + "." + parts[1] + "." + parts[2] + "." + parts[3]; this.passivePort = Integer.parseInt(parts[4]) * 256 + Integer.parseInt(parts[5]); this.write(200, "PORT command successful"); } private void onPWD() throws IOException, FtpException { this.write(257, "\"" + this.connectionState.getCurrentDir() + "\" is cwd."); } private void onQUIT() throws IOException, FtpException { this.stateMachine.setStatus(FtpConnection.LOGOUT); this.write(221, this.ftpServer.getFtpCommandHandler().onLogoutRequest(this.connectionState)); this.stateMachine.setStatus(FtpConnection.IDLEEND); } /** * @param commandParts * @throws FtpException * @throws IOException */ private void onREST(final String[] commandParts) throws FtpException, IOException { try { final long position = Long.parseLong(commandParts[1]); this.ftpServer.getFtpCommandHandler().onREST(this.connectionState, position); this.write(350, "Restarting at " + position + ". Send STORE or RETRIEVE"); } catch (final NumberFormatException e) { this.write(554, "Requested action not taken: invalid REST parameter."); } } private void onRETR(final String[] commandParts) throws IOException, FtpException { try { try { this.openDataConnection(); } catch (final IOException e) { throw new FtpException(425, "Can't open data connection"); } /* * we need to make sure that the file exists before opening data * connection, see http://cr.yp.to/ftp/retr.html, RFC 959 * * this will cause the 550 file not found before opening the data * connection */ this.ftpServer.getFtpCommandHandler().getSize(this.connectionState, this.buildParameter(commandParts)); this.write(150, "Opening XY mode data connection for transfer"); long bytesWritten = 0; try { bytesWritten = this.ftpServer.getFtpCommandHandler().onRETR(this.dataSocket.getOutputStream(), this.connectionState, this.buildParameter(commandParts)); this.dataSocket.getOutputStream().flush(); this.dataSocket.shutdownOutput(); } catch (final FtpFileNotExistException e) { /* need another error code here */ throw new FtpException(450, "Requested file action not taken; File unavailable"); } catch (final FtpException e) { throw e; } catch (final IOException e) { throw new FtpException(426, e.getMessage()); } catch (final Exception e) { throw new FtpException(451, e.getMessage()); } /* we close the passive port after command */ this.write(226, "Transfer complete. " + bytesWritten + " bytes transfered!"); } finally { this.closeDataConnection(); } } /** * @param commandParts * @throws IOException * @throws FtpException * @throws FtpFileNotExistException */ private void onRMD(final String[] commandParts) throws IOException, FtpException { this.ftpServer.getFtpCommandHandler().removeDirectory(this.connectionState, this.buildParameter(commandParts)); this.write(250, "\"" + this.buildParameter(commandParts) + "\" removed."); } /** * @param commandParts * @throws FtpBadSequenceException * @throws FtpFileNotExistException * @throws IOException */ private void onRNFR(final String[] commandParts) throws FtpException, IOException { if (this.connectionState.getRenameFile() != null) { this.connectionState.setRenameFile(null); throw new FtpBadSequenceException(); } try { this.ftpServer.getFtpCommandHandler().renameFile(this.connectionState, this.buildParameter(commandParts)); } catch (final FtpException e) { this.connectionState.setRenameFile(null); throw e; } this.write(350, "\"" + this.buildParameter(commandParts) + "\" rename pending."); } /** * @param commandParts * @throws FtpBadSequenceException * @throws FtpFileNotExistException * @throws IOException */ private void onRNTO(final String[] commandParts) throws IOException, FtpException { if (this.connectionState.getRenameFile() == null) { /* a renameFile must exist, RNFR must be the command before RNTO */ this.connectionState.setRenameFile(null); throw new FtpBadSequenceException(); } try { this.ftpServer.getFtpCommandHandler().renameFile(this.connectionState, this.buildParameter(commandParts)); } finally { this.connectionState.setRenameFile(null); } this.write(250, "\"" + this.buildParameter(commandParts) + "\" rename successful."); } /** * @param commandParts * @throws IOException * @throws FtpFileNotExistException */ private void onSIZE(final String[] commandParts) throws FtpException, IOException { this.write(213, "" + this.ftpServer.getFtpCommandHandler().getSize(this.connectionState, this.buildParameter(commandParts))); } private void onSTOR(final String[] commandParts, final boolean append) throws IOException, FtpException { try { try { this.openDataConnection(); } catch (final IOException e) { throw new FtpException(425, "Can't open data connection"); } this.write(150, "Opening XY mode data connection for transfer"); long bytesRead = 0; try { bytesRead = this.ftpServer.getFtpCommandHandler().onSTOR(this.dataSocket.getInputStream(), this.connectionState, append, this.buildParameter(commandParts)); this.dataSocket.shutdownInput(); } catch (final FtpFileNotExistException e) { /* need another error code here */ throw new FtpException(450, "Requested file action not taken; File unavailable"); } catch (final FtpException e) { throw e; } catch (final IOException e) { throw new FtpException(426, e.getMessage()); } catch (final Exception e) { throw new FtpException(451, e.getMessage()); } /* we close the passive port after command */ this.write(226, "Transfer complete. " + bytesRead + " bytes received!"); } finally { this.closeDataConnection(); } } private void onSTRU(final String[] commandParts) throws IOException, FtpCommandParameterException { if ("F".equalsIgnoreCase(commandParts[1])) { this.write(200, "Command okay."); } else { throw new FtpCommandParameterException(); } } private void onSYST() throws IOException { this.write(215, "UNIX Type: L8"); } private void onTYPE(final String[] commandParts) throws IOException, FtpCommandParameterException { final String type = commandParts[1]; if ("A".equalsIgnoreCase(type)) { this.type = TYPE.ASCII; } else if ("I".equalsIgnoreCase(type)) { this.type = TYPE.BINARY; } else if ("L".equalsIgnoreCase(type)) { if (commandParts.length == 3 && "8".equals(commandParts[2])) { this.type = TYPE.BINARY; } else { throw new FtpCommandParameterException(); } } else { throw new FtpCommandParameterException(); } this.write(200, "Command okay"); } private void onUSER(final String params[]) throws IOException, FtpException { if (this.stateMachine.isFinal()) { this.stateMachine.reset(); } this.stateMachine.setStatus(FtpConnection.USER); this.connectionState.setUser(this.ftpServer.getFtpCommandHandler().getUser(params[1])); if (this.connectionState.getUser() != null) { if (this.connectionState.getUser().getPassword() == null) { final String message = this.ftpServer.getFtpCommandHandler().onLoginSuccessRequest(this.connectionState); if (message != null) { this.write(230, message, true); } this.write(230, "User logged in, proceed"); this.stateMachine.setStatus(FtpConnection.LOGIN); } else { this.write(331, "User name okay, need password"); } } else { final String message = this.ftpServer.getFtpCommandHandler().onLoginFailedMessage(this.connectionState); if (message != null) { this.write(530, message, true); } this.stateMachine.setStatus(FtpConnection.LOGOUT); this.stateMachine.setStatus(FtpConnection.IDLEEND); this.stateMachine.reset(); throw new FtpNotLoginException(); } } private void openDataConnection() throws IOException { if (this.dataSocket == null || !this.dataSocket.isConnected()) { if (this.serverSocket != null && this.serverSocket.isBound()) { /* PASV */ this.dataSocket = this.serverSocket.accept(); } else { /* PORT */ this.dataSocket = new Socket(this.passiveIP, this.passivePort); } } } public void run() { try { this.writeMultiLineAuto(220, this.ftpServer.getFtpCommandHandler().getWelcomeMessage(this.connectionState)); while (true) { final String command = this.reader.readLine(); if (command == null) { break; } if (this.ftpServer.isDebug()) { Log.L.info("REQ: " + command); } this.handleCommand(command); } } catch (final IOException e) { } finally { this.closeDataConnection(); try { this.controlSocket.close(); } catch (final Throwable e2) { } } } private void write(final int code, final String message) throws IOException { if (this.ftpServer.isDebug()) { Log.L.info("RESP: " + code + " " + message); } this.write(code, message, false); } private void write(final int code, final String message, final boolean multiLine) throws IOException { if (multiLine) { this.writer.write(code + "-" + message + "\r\n"); } else { this.writer.write(code + " " + message + "\r\n"); } this.writer.flush(); } private void writeMultiLineAuto(final int code, final String message) throws IOException { final String lines[] = Regex.getLines(message); if (lines != null) { for (int line = 0; line < lines.length; line++) { if (line == lines.length - 1) { this.writer.write(code + " " + lines[line] + "\r\n"); } else { this.writer.write(code + "-" + lines[line] + "\r\n"); } } } this.writer.flush(); } }