| /* |
| * Copyright (c) 2006, Oracle and/or its affiliates. All rights reserved. |
| * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. |
| * |
| * This code is free software; you can redistribute it and/or modify it |
| * under the terms of the GNU General Public License version 2 only, as |
| * published by the Free Software Foundation. |
| * |
| * This code is distributed in the hope that it will be useful, but WITHOUT |
| * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or |
| * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License |
| * version 2 for more details (a copy is included in the LICENSE file that |
| * accompanied this code). |
| * |
| * You should have received a copy of the GNU General Public License version |
| * 2 along with this work; if not, write to the Free Software Foundation, |
| * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. |
| * |
| * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA |
| * or visit www.oracle.com if you need additional information or have any |
| * questions. |
| */ |
| |
| import java.net.*; |
| import java.io.*; |
| import java.util.regex.*; |
| import java.security.*; |
| import javax.net.ssl.*; |
| |
| /* |
| * This class handles one client connection. It will interpret and act on the |
| * commands (like USER, GET, PUT etc...) sent through the socket passed to |
| * the constructor. |
| * |
| * To function it needs to be provided 2 handlers, one for the filesystem |
| * and one for authentication. |
| * @see FileSystemHandler |
| * @see AuthHandler |
| * @see #setHandlers(FtpFileSystemHandler,FtpAuthHandler) |
| */ |
| |
| public class FtpCommandHandler extends Thread { |
| private FtpServer parent = null; |
| private Socket cmd = null; |
| private Socket oldCmd = null; |
| private InetAddress clientAddr = null; |
| private ServerSocket pasv = null; |
| |
| private BufferedReader in = null; |
| |
| private PrintStream out = null; |
| |
| private FtpFileSystemHandler fsh = null; |
| private FtpAuthHandler auth = null; |
| |
| private boolean done = false; |
| |
| private String username = null; |
| private String password = null; |
| private String account = null; |
| private boolean logged = false; |
| private boolean epsvAll = false; |
| private int dataPort = 0; |
| private InetAddress dataAddress = null; |
| private boolean pasvEnabled = true; |
| private boolean portEnabled = true; |
| private boolean extendedEnabled = true; |
| private boolean binary = true; |
| private String renameFrom = null; |
| private long restart = 0; |
| private boolean useCrypto = false; |
| private boolean useDataCrypto = false; |
| private SSLSocketFactory sslFact = null; |
| |
| private final int ERROR = -1; |
| private final int QUIT = 0; |
| private final int USER = 1; |
| private final int PASS = 2; |
| private final int CWD = 3; |
| private final int CDUP = 4; |
| private final int PWD = 5; |
| private final int TYPE = 6; |
| private final int NOOP = 7; |
| private final int RETR = 8; |
| private final int PORT = 9; |
| private final int PASV = 10; |
| private final int EPSV = 11; |
| private final int EPRT = 12; |
| private final int SYST = 13; |
| private final int STOR = 14; |
| private final int STOU = 15; |
| private final int LIST = 16; |
| private final int NLST = 17; |
| private final int RNFR = 18; |
| private final int RNTO = 19; |
| private final int DELE = 20; |
| private final int REST = 21; |
| private final int AUTH = 22; |
| private final int FEAT = 23; |
| private final int CCC = 24; |
| private final int PROT = 25; |
| private final int PBSZ = 26; |
| |
| private String[] commands = |
| { "QUIT", "USER", "PASS", "CWD", "CDUP", "PWD", "TYPE", "NOOP", "RETR", |
| "PORT", "PASV", "EPSV", "EPRT", "SYST", "STOR", "STOU", "LIST", "NLST", |
| "RNFR", "RNTO", "DELE", "REST", "AUTH", "FEAT", "CCC", "PROT", "PBSZ" |
| }; |
| |
| private boolean isPasvSet() { |
| if (pasv != null && !pasvEnabled) { |
| try { |
| pasv.close(); |
| } catch ( IOException e) { |
| |
| } |
| pasv = null; |
| } |
| if (pasvEnabled && pasv != null) |
| return true; |
| return false; |
| } |
| |
| private OutputStream getOutDataStream() throws IOException { |
| if (isPasvSet()) { |
| Socket s = pasv.accept(); |
| if (useCrypto && useDataCrypto) { |
| SSLSocket ssl = (SSLSocket) sslFact.createSocket(s, clientAddr.getHostName(), s.getPort(), true); |
| ssl.setUseClientMode(false); |
| s = ssl; |
| } |
| return s.getOutputStream(); |
| } |
| if (dataAddress != null) { |
| Socket s; |
| if (useCrypto) { |
| s = sslFact.createSocket(dataAddress, dataPort); |
| } else |
| s = new Socket(dataAddress, dataPort); |
| dataAddress = null; |
| dataPort = 0; |
| return s.getOutputStream(); |
| } |
| return null; |
| } |
| |
| private InputStream getInDataStream() throws IOException { |
| if (isPasvSet()) { |
| Socket s = pasv.accept(); |
| if (useCrypto && useDataCrypto) { |
| SSLSocket ssl = (SSLSocket) sslFact.createSocket(s, clientAddr.getHostName(), s.getPort(), true); |
| ssl.setUseClientMode(false); |
| s = ssl; |
| } |
| return s.getInputStream(); |
| } |
| if (dataAddress != null) { |
| Socket s; |
| if (useCrypto) { |
| s = sslFact.createSocket(dataAddress, dataPort); |
| } else |
| s = new Socket(dataAddress, dataPort); |
| dataAddress = null; |
| dataPort = 0; |
| return s.getInputStream(); |
| } |
| return null; |
| } |
| |
| private void parsePort(String port_arg) throws IOException { |
| if (epsvAll) { |
| out.println("501 PORT not allowed after EPSV ALL."); |
| return; |
| } |
| if (!portEnabled) { |
| out.println("500 PORT command is disabled, please use PASV."); |
| return; |
| } |
| StringBuffer host; |
| int i = 0, j = 4; |
| while (j > 0) { |
| i = port_arg.indexOf(',', i + 1); |
| if (i < 0) |
| break; |
| j--; |
| } |
| if (j != 0) { |
| out.println("500 '" + port_arg + "': command not understood."); |
| return; |
| } |
| try { |
| host = new StringBuffer(port_arg.substring(0, i)); |
| for (j = 0; j < host.length(); j++) |
| if (host.charAt(j) == ',') |
| host.setCharAt(j, '.'); |
| String ports = port_arg.substring(i + 1); |
| i = ports.indexOf(','); |
| dataPort = Integer.parseInt(ports.substring(0, i)) << 8; |
| dataPort += (Integer.parseInt(ports.substring(i + 1))); |
| dataAddress = InetAddress.getByName(host.toString()); |
| out.println("200 Command okay."); |
| } catch (Exception ex3) { |
| dataPort = 0; |
| dataAddress = null; |
| out.println("500 '" + port_arg + "': command not understood."); |
| } |
| } |
| |
| private void parseEprt(String arg) { |
| if (epsvAll) { |
| out.println("501 PORT not allowed after EPSV ALL"); |
| return; |
| } |
| if (!extendedEnabled || !portEnabled) { |
| out.println("500 EPRT is disabled, use PASV instead"); |
| return; |
| } |
| Pattern p = Pattern.compile("\\|(\\d)\\|(.*)\\|(\\d+)\\|"); |
| Matcher m = p.matcher(arg); |
| if (!m.find()) { |
| out.println("500 '" + arg + "': command not understood."); |
| return; |
| } |
| try { |
| dataAddress = InetAddress.getByName(m.group(2)); |
| } catch (UnknownHostException e) { |
| out.println("500 " + arg + ": invalid address."); |
| dataAddress = null; |
| return; |
| } |
| dataPort = Integer.parseInt(m.group(3)); |
| out.println("200 Command okay."); |
| } |
| |
| private void doPasv() { |
| if (!pasvEnabled) { |
| out.println("500 PASV is disabled, use PORT."); |
| return; |
| } |
| try { |
| if (pasv == null) |
| pasv = new ServerSocket(0); |
| int port = pasv.getLocalPort(); |
| InetAddress rAddress = cmd.getLocalAddress(); |
| if (rAddress instanceof Inet6Address) { |
| out.println("500 PASV illegal over IPv6 addresses, use EPSV."); |
| return; |
| } |
| byte[] a = rAddress.getAddress(); |
| out.println("227 Entering Passive Mode " + a[0] + "," + a[1] + "," + a[2] + "," + a[3] + "," + |
| (port >> 8) + "," + (port & 0xff) ); |
| } catch (IOException e) { |
| out.println("425 can't build data connection: Connection refused."); |
| } |
| } |
| |
| private void doEpsv(String arg) { |
| if (!extendedEnabled || !pasvEnabled) { |
| out.println("500 EPSV disabled, use PORT or PASV."); |
| return; |
| } |
| if ("all".equalsIgnoreCase(arg)) { |
| out.println("200 EPSV ALL Command successful."); |
| epsvAll = true; |
| return; |
| } |
| try { |
| if (pasv == null) |
| pasv = new ServerSocket(0); |
| int port = pasv.getLocalPort(); |
| out.println("229 Entering Extended Passive Mode (|||" + port + "|)"); |
| } catch (IOException e) { |
| out.println("500 Can't create data connection."); |
| } |
| } |
| |
| private void doRetr(String arg) { |
| try { |
| OutputStream dOut = getOutDataStream(); |
| if (dOut != null) { |
| InputStream dIn = fsh.getFile(arg); |
| if (dIn == null) { |
| out.println("550 File not found."); |
| dOut.close(); |
| return; |
| } |
| out.println("150 Opening " + (binary ? "BINARY " : "ASCII ") + " data connection for file " + arg + |
| "(" + fsh.getFileSize(arg) + " bytes)."); |
| if (binary) { |
| byte[] buf = new byte[2048]; |
| dOut = new BufferedOutputStream(dOut); |
| int count; |
| if (restart > 0) { |
| dIn.skip(restart); |
| restart = 0; |
| } |
| do { |
| count = dIn.read(buf); |
| if (count > 0) |
| dOut.write(buf, 0, count); |
| } while (count >= 0); |
| dOut.close(); |
| dIn.close(); |
| out.println("226 Transfer complete."); |
| } |
| } |
| } catch (IOException e) { |
| |
| } |
| } |
| |
| private void doStor(String arg, boolean unique) { |
| try { |
| InputStream dIn = getInDataStream(); |
| if (dIn != null) { |
| OutputStream dOut = fsh.putFile(arg); |
| if (dOut == null) { |
| out.println("500 Can't create file " + arg); |
| dIn.close(); |
| return; |
| } |
| out.println("150 Opening " + (binary ? "BINARY " : "ASCII ") + " data connection for file " + arg); |
| if (binary) { |
| byte[] buf = new byte[2048]; |
| dOut = new BufferedOutputStream(dOut); |
| int count; |
| do { |
| count = dIn.read(buf); |
| if (count > 0) |
| dOut.write(buf, 0, count); |
| } while (count >= 0); |
| dOut.close(); |
| dIn.close(); |
| out.println("226 Transfer complete."); |
| } |
| } |
| } catch (IOException e) { |
| |
| } |
| } |
| |
| private void doList() { |
| try { |
| OutputStream dOut = getOutDataStream(); |
| if (dOut != null) { |
| InputStream dIn = fsh.listCurrentDir(); |
| if (dIn == null) { |
| out.println("550 File not found."); |
| dOut.close(); |
| return; |
| } |
| out.println("150 Opening ASCII data connection for file list"); |
| byte[] buf = new byte[2048]; |
| dOut = new BufferedOutputStream(dOut); |
| int count; |
| do { |
| count = dIn.read(buf); |
| if (count > 0) |
| dOut.write(buf, 0, count); |
| } while (count >= 0); |
| dOut.close(); |
| dIn.close(); |
| out.println("226 Transfer complete."); |
| } |
| } catch (IOException e) { |
| |
| } |
| } |
| |
| private boolean useTLS() { |
| if (sslFact == null) { |
| sslFact = (SSLSocketFactory) SSLSocketFactory.getDefault(); |
| } |
| if (sslFact == null) |
| return false; |
| return true; |
| } |
| |
| private void stopTLS() { |
| if (useCrypto) { |
| SSLSocket ssl = (SSLSocket) cmd; |
| try { |
| ssl.close(); |
| } catch (IOException e) { |
| // nada |
| } |
| cmd = oldCmd; |
| oldCmd = null; |
| try { |
| in = new BufferedReader(new InputStreamReader(cmd.getInputStream())); |
| out = new PrintStream(cmd.getOutputStream(), true, "ISO8859_1"); |
| } catch (Exception ex) { |
| |
| } |
| } |
| } |
| |
| public void setHandlers(FtpFileSystemHandler f, FtpAuthHandler a) { |
| fsh = f; |
| auth = a; |
| } |
| |
| public FtpCommandHandler(Socket cl, FtpServer p) { |
| parent = p; |
| cmd = cl; |
| clientAddr = cl.getInetAddress(); |
| } |
| |
| public void terminate() { |
| done = true; |
| } |
| |
| private int parseCmd(StringBuffer cmd) { |
| |
| if (cmd == null || cmd.length() < 3) // Shortest command is 3 char long |
| return ERROR; |
| int blank = cmd.indexOf(" "); |
| if (blank < 0) |
| blank = cmd.length(); |
| if (blank < 3) |
| return ERROR; |
| String s = cmd.substring(0,blank); |
| cmd.delete(0, blank + 1); |
| System.out.println("parse: cmd = " + s + " arg = " +cmd.toString()); |
| for (int i = 0; i < commands.length; i++) |
| if (s.equalsIgnoreCase(commands[i])) |
| return i; |
| // Unknown command |
| return ERROR; |
| } |
| |
| private boolean checkLogged() { |
| if (!logged) { |
| out.println("530 Not logged in."); |
| return false; |
| } |
| return true; |
| } |
| |
| public void run() { |
| try { |
| // cmd.setSoTimeout(2000); |
| in = new BufferedReader(new InputStreamReader(cmd.getInputStream())); |
| out = new PrintStream(cmd.getOutputStream(), true, "ISO8859_1"); |
| out.println("---------------------------------\n220 Java FTP test server" |
| + " (j2se 6.0) ready.\n \n Please send commands\n" |
| + "-----------------------------\n\n\n"); |
| out.flush(); |
| if (auth.authType() == 0) // No auth needed |
| logged = true; |
| } catch (IOException e) { |
| e.printStackTrace(); |
| return; |
| } |
| |
| String str; |
| StringBuffer buf; |
| int res; |
| while (!done) { |
| try { |
| str = in.readLine(); |
| System.out.println("line: " + str); |
| buf = new StringBuffer(str); |
| res = parseCmd(buf); |
| switch (res) { |
| case ERROR: |
| out.println("500 '" + str +"': command not understood."); |
| break; |
| case QUIT: |
| out.println("221 Goodbye."); |
| done = true; |
| break; |
| case USER: |
| logged = false; |
| username = buf.toString(); |
| if (auth.authType() > 1) |
| out.println("331 User name okay, need password."); |
| else { |
| if (auth.authenticate(username, null)) { |
| out.println("230 User logged in, proceed."); |
| logged = true; |
| } else { |
| out.println("331 User name okay, need password."); |
| } |
| } |
| break; |
| case PASS: |
| if (logged || (username == null)) { |
| out.println("503 Login with USER first."); |
| break; |
| } |
| password = buf.toString(); |
| if (auth.authType() == 3) { |
| out.println("332 Need account for login."); |
| break; |
| } |
| if (auth.authenticate(username, password)) { |
| logged = true; |
| out.println("230 User " + username + " logged in."); |
| break; |
| } |
| out.println("530 Login incorrect."); |
| username = null; |
| break; |
| case CWD: |
| if (checkLogged()) { |
| String path = buf.toString(); |
| if (fsh.cd(path)) { |
| out.println("250 CWD command successful."); |
| } else { |
| out.println("550 " + path + ": no such file or directory."); |
| } |
| } |
| break; |
| case CDUP: |
| if (checkLogged()) { |
| if (fsh.cdUp()) |
| out.println("250 CWD command successful."); |
| else |
| out.println("550 invalid path."); |
| } |
| break; |
| case PWD: |
| if (checkLogged()) { |
| String s = fsh.pwd(); |
| out.println("257 \"" + s + "\" is current directory"); |
| } |
| break; |
| case NOOP: |
| if (checkLogged()) { |
| out.println("200 NOOP command successful."); |
| } |
| break; |
| case PORT: |
| if (checkLogged()) { |
| parsePort(buf.toString()); |
| } |
| break; |
| case EPRT: |
| if (checkLogged()) { |
| parseEprt(buf.toString()); |
| } |
| break; |
| case PASV: |
| if (checkLogged()) |
| doPasv(); |
| break; |
| case EPSV: |
| if (checkLogged()) |
| doEpsv(buf.toString()); |
| break; |
| case RETR: |
| if (checkLogged()) { |
| doRetr(buf.toString()); |
| } |
| break; |
| case SYST: |
| if (checkLogged()) { |
| out.println("215 UNIX Type: L8 Version: Java 6.0"); |
| } |
| break; |
| case TYPE: |
| if (checkLogged()) { |
| String arg = buf.toString(); |
| if (arg.length() != 1 || "AIE".indexOf(arg.charAt(0)) < 0) { |
| out.println("500 'TYPE " + arg + "' command not understood."); |
| continue; |
| } |
| out.println("200 Type set to " + buf.toString() + "."); |
| if (arg.charAt(0) == 'I') |
| binary = true; |
| else |
| binary = false; |
| } |
| break; |
| case STOR: |
| case STOU: |
| // TODO: separate STOR and STOU (Store Unique) |
| if (checkLogged()) { |
| doStor(buf.toString(), false); |
| } |
| break; |
| case LIST: |
| if (checkLogged()) { |
| doList(); |
| } |
| break; |
| case NLST: |
| // TODO: implememt |
| break; |
| case DELE: |
| if (checkLogged()) { |
| String arg = buf.toString(); |
| if (fsh.removeFile(arg)) { |
| out.println("250 file " + arg + " deleted."); |
| break; |
| } |
| out.println("550 " + arg + ": no such file or directory."); |
| } |
| break; |
| case RNFR: |
| if (checkLogged()) { |
| if (renameFrom != null) { |
| out.println("503 Bad sequence of commands."); |
| break; |
| } |
| renameFrom = buf.toString(); |
| if (fsh.fileExists(renameFrom)) { |
| out.println("350 File or directory exists, ready for destination name."); |
| } else { |
| out.println("550 " + renameFrom + ": no such file or directory"); |
| renameFrom = null; |
| } |
| } |
| break; |
| case RNTO: |
| if (checkLogged()) { |
| if (renameFrom == null) { |
| out.println("503 Bad sequence of commands."); |
| break; |
| } |
| if (fsh.rename(renameFrom, buf.toString())) { |
| out.println("250 Rename successful"); |
| } else { |
| out.println("550 Rename "); |
| } |
| renameFrom = null; |
| } |
| break; |
| case REST: |
| if (checkLogged()) { |
| String arg = buf.toString(); |
| restart = Long.parseLong(arg); |
| if (restart > 0) |
| out.println("350 Restarting at " + restart + ". Send STORE or RETRIEVE to initiate transfer"); |
| else |
| out.println("501 Syntax error in command of arguments."); |
| } |
| break; |
| case FEAT: |
| out.println("211-Features:"); |
| out.println(" REST STREAM"); |
| out.println(" PBSZ"); |
| out.println(" AUTH TLS"); |
| out.println(" PROT P"); |
| out.println(" CCC"); |
| out.println("211 End"); |
| break; |
| case AUTH: |
| if ("TLS".equalsIgnoreCase(buf.toString()) && useTLS()) { |
| out.println("234 TLS Authentication OK."); |
| out.flush(); |
| SSLSocket ssl; |
| String[] suites = sslFact.getSupportedCipherSuites(); |
| try { |
| ssl = (SSLSocket) sslFact.createSocket(cmd, cmd.getInetAddress().getHostName(), cmd.getPort(), false); |
| ssl.setUseClientMode(false); |
| ssl.setEnabledCipherSuites(suites); |
| ssl.startHandshake(); |
| } catch (IOException ioe) { |
| ioe.printStackTrace(); |
| out.println("550 Unable to create secure channel."); |
| break; |
| } |
| oldCmd = cmd; |
| cmd = ssl; |
| out = new PrintStream(cmd.getOutputStream(), true, "ISO8859_1"); |
| in = new BufferedReader(new InputStreamReader(cmd.getInputStream())); |
| System.out.println("Secure socket created!"); |
| useCrypto = true; |
| break; |
| } |
| out.println("501 Unknown or unsupported AUTH type"); |
| break; |
| case CCC: |
| out.println("200 Command OK."); |
| stopTLS(); |
| break; |
| case PROT: |
| String arg = buf.toString(); |
| if ("C".equalsIgnoreCase(arg)) { |
| // PROT C : Clear protection level |
| // No protection on data channel; |
| useDataCrypto = false; |
| out.println("200 Command OK."); |
| break; |
| } |
| if ("P".equalsIgnoreCase(arg)) { |
| // PROT P : Private protection level |
| // Data channel is integrity and confidentiality protected |
| useDataCrypto = true; |
| out.println("200 Command OK."); |
| break; |
| } |
| out.println("537 Requested PROT level not supported by security mechanism."); |
| break; |
| case PBSZ: |
| // TODO: finish |
| out.println("200 Command OK."); |
| break; |
| |
| } |
| |
| } catch (InterruptedIOException ie) { |
| // loop |
| } catch (IOException e) { |
| e.printStackTrace(); |
| return; |
| } |
| } |
| try { |
| in.close(); |
| out.close(); |
| cmd.close(); |
| } catch (IOException e) { |
| } |
| parent.removeClient(this); |
| } |
| } |