/** * FTPClient * Copyright 2002, 2004, 2006, 2010 by Michael Peter Christen * first published on http://yacy.net * main implementation finished: 28.05.2002 * last major change: 06.05.2004 * added html generation for directories: 5.9.2006 * migrated to the cora package and re-licensed under lgpl: 23.08.2010 * * This file is part of YaCy Content Integration * * This library is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 2.1 of the License, or (at your option) any later version. * * This library 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 * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program in the file lgpl21.txt * If not, see <http://www.gnu.org/licenses/>. */ package net.yacy.cora.protocol.ftp; import java.io.BufferedOutputStream; import java.io.BufferedReader; import java.io.ByteArrayOutputStream; import java.io.DataInputStream; import java.io.DataOutputStream; import java.io.File; import java.io.FileInputStream; import java.io.FileNotFoundException; import java.io.FileOutputStream; import java.io.IOException; import java.io.InputStream; import java.io.InputStreamReader; import java.io.OutputStream; import java.io.PrintStream; import java.io.RandomAccessFile; import java.lang.reflect.Array; import java.lang.reflect.InvocationTargetException; import java.lang.reflect.Method; import java.net.InetAddress; import java.net.InetSocketAddress; import java.net.ServerSocket; import java.net.Socket; import java.net.SocketException; import java.nio.charset.StandardCharsets; import java.text.DateFormat; import java.text.ParseException; import java.text.SimpleDateFormat; import java.util.ArrayList; import java.util.Calendar; import java.util.Date; import java.util.HashMap; import java.util.List; import java.util.Locale; import java.util.Map; import java.util.Properties; import java.util.StringTokenizer; import java.util.concurrent.BlockingQueue; import java.util.concurrent.LinkedBlockingQueue; import java.util.regex.Matcher; import java.util.regex.Pattern; import net.yacy.cora.document.encoding.UTF8; import net.yacy.cora.protocol.Domains; import net.yacy.cora.util.ConcurrentLog; public class FTPClient { public static final String ANONYMOUS = "anonymous"; private static final ConcurrentLog log = new ConcurrentLog("FTPClient"); private static final String vDATE = "20161222"; private boolean glob = true; // glob = false -> filenames are taken // literally for mget, .. // transfer type private static final char transferType = 'i'; // transfer binary // block size [1K by default] private static final int blockSize = 1024; // client socket for commands private Socket ControlSocket = null; // socket timeout private static final int ControlSocketTimeout = 10000; // data socket timeout private int DataSocketTimeout = 0; // in seconds (default infinite) // socket for data transactions private ServerSocket DataSocketActive = null; private Socket DataSocketPassive = null; private boolean DataSocketPassiveMode = true; // output and input streams for client control connection private BufferedReader clientInput = null; private DataOutputStream clientOutput = null; // client prompt private String prompt = "ftp [local]>"; String[] cmd; // session parameters File currentLocalPath; String account, password, host, remotemessage, remotegreeting, remotesystem; int port; // entry info cache private final Map<String, entryInfo> infoCache = new HashMap<String, entryInfo>(); // date-format in LIST (english month names) private static final SimpleDateFormat lsDateFormat = new SimpleDateFormat("MMM d y H:m", new Locale("en")); // TODO: implement RFC 2640 Internationalization public FTPClient() { this.currentLocalPath = new File(System.getProperty("user.dir")); try { this.currentLocalPath = new File(this.currentLocalPath.getCanonicalPath()); } catch (final IOException e) { } this.account = null; this.password = null; this.host = null; this.port = -1; this.remotemessage = null; this.remotegreeting = null; this.remotesystem = null; } public boolean exec(String command, final boolean promptIt) { if ((command == null) || (command.isEmpty())) { return true; } int pos; String com; boolean ret = true; while (command.length() > 0) { pos = command.indexOf(';',0); if (pos < 0) { pos = command.indexOf("\n",0); } if (pos < 0) { com = command; command = ""; } else { com = command.substring(0, pos); command = command.substring(pos + 1); } if (promptIt) { log.info(this.prompt + com); } this.cmd = line2args(com); try { ret = (((Boolean) getClass().getMethod(this.cmd[0].toUpperCase(), (Class<?>[]) Array.newInstance(Class.class, 0)).invoke(this, (Object[]) Array.newInstance(Object.class, 0))) .booleanValue()); } catch (final InvocationTargetException e) { if (e.getMessage() != null) { if (notConnected()) { // the error was probably caused because there is no // connection log.warn("not connected. no effect.", e); } else { log.warn("ftp internal exception: target exception " + e); } return ret; } } catch (final IllegalAccessException e) { log.warn("ftp internal exception: wrong access " + e); return ret; } catch (final NoSuchMethodException e) { // consider first that the user attempted to execute a java // command from // the current path; either local or remote if (notConnected()) { // try a local exec try { javaexec(this.cmd); } catch (final Exception ee) { log.warn("Command '" + this.cmd[0] + "' not supported. Try 'HELP'."); } } else { // try a remote exec exec("java " + com, false); } return ret; } } return ret; } private String[] line2args(final String line) { // parse the command line if ((line == null) || (line.isEmpty())) { return null; } // pre-parse String line1 = ""; boolean quoted = false; for (int i = 0; i < line.length(); i++) { if (quoted) { if (line.charAt(i) == '"') { quoted = false; } else { line1 = line1 + line.charAt(i); } } else { if (line.charAt(i) == '"') { quoted = true; } else if (line.charAt(i) == ' ') { line1 = line1 + '|'; } else { line1 = line1 + line.charAt(i); } } } return line1.split("\\|"); } static class cl extends ClassLoader { public cl() { super(); } @Override public synchronized Class<?> loadClass(final String classname, final boolean resolve) throws ClassNotFoundException { Class<?> c = findLoadedClass(classname); if (c == null) { try { // second try: ask the system c = findSystemClass(classname); } catch (final ClassNotFoundException e) { // third try: load myself final File f = new File(System.getProperty("user.dir"), classname + ".class"); final int length = (int) f.length(); final byte[] classbytes = new byte[length]; try { final DataInputStream in = new DataInputStream(new FileInputStream(f)); in.readFully(classbytes); in.close(); c = defineClass(classname, classbytes, 0, classbytes.length); } catch (final FileNotFoundException ee) { throw new ClassNotFoundException(); } catch (final IOException ee) { throw new ClassNotFoundException(); } } } if (resolve) { resolveClass(c); } return c; } } private void javaexec(final String[] inArgs) { final String obj = inArgs[0]; final String[] args = new String[inArgs.length - 1]; // remove the object name from the array of arguments System.arraycopy(inArgs, 1, args, 0, inArgs.length - 1); // Build the argument list for invoke() method. final Object[] argList = new Object[1]; argList[0] = args; final Properties pr = System.getProperties(); final String origPath = (String) pr.get("java.class.path"); try { // set the user.dir to the actual local path pr.put("user.dir", this.currentLocalPath.toString()); // add the current path to the classpath // pr.put("java.class.path", "" + pr.get("user.dir") + // pr.get("path.separator") + origPath); // log.warning("System Properties: " + pr.toString()); System.setProperties(pr); // locate object final Class<?> c = (new cl()).loadClass(obj); // Class c = this.getClass().getClassLoader().loadClass(obj); // locate public static main(String[]) method final Class<?>[] parameterType = (Class<?>[]) Array.newInstance(Class.class, 1); parameterType[0] = Class.forName("[Ljava.lang.String;"); Method m = c.getMethod("main", parameterType); // invoke object.main() final Object result = m.invoke(null, argList); //parameterType = null; m = null; // handle result if (result != null) { log.info("returns " + result); } // set the local path to the user.dir (which may have changed) this.currentLocalPath = new File((String) pr.get("user.dir")); } catch (final ClassNotFoundException e) { // log.warning("cannot find class file " + obj + // ".class"); // class file does not exist, go silently over it to not show // everybody that the // system attempted to load a class file log.warn("Command '" + obj + "' not supported. Try 'HELP'."); } catch (final NoSuchMethodException e) { log.warn("no \"public static main(String args[])\" in " + obj); } catch (final InvocationTargetException e) { final Throwable orig = e.getTargetException(); if (orig.getMessage() != null) { log.warn("Exception from " + obj + ": " + orig.getMessage(), orig); } } catch (final IllegalAccessException e) { log.warn("Illegal access for " + obj + ": class is probably not declared as public", e); } catch (final NullPointerException e) { log.warn("main(String args[]) is not defined as static for " + obj); /* * } catch (final IOException e) { // class file does not exist, go * silently over it to not show everybody that the // system * attempted to load a class file log.warning("Command '" + obj + "' * not supported. Try 'HELP'."); */ } catch (final Exception e) { log.warn("Exception caught: ", e); } // set the classpath to its original definition pr.put("java.class.path", origPath); } // FTP CLIENT COMMANDS ------------------------------------ public boolean ASCII() { if (this.cmd.length != 1) { log.warn("Syntax: ASCII (no parameter)"); return true; } try { literal("TYPE A"); } catch (final IOException e) { log.warn("Error: ASCII transfer type not supported by server."); } return true; } public boolean BINARY() { if (this.cmd.length != 1) { log.warn("Syntax: BINARY (no parameter)"); return true; } try { literal("TYPE I"); } catch (final IOException e) { log.warn("Error: BINARY transfer type not supported by server."); } return true; } public boolean BYE() { return QUIT(); } public boolean CD() { if (this.cmd.length != 2) { log.warn("Syntax: CD <path>"); return true; } if (notConnected()) { return LCD(); } try { // send cwd command send("CWD " + this.cmd[1]); final String reply = receive(); if (isNotPositiveCompletion(reply)) { throw new IOException(reply); } } catch (final IOException e) { log.warn("Error: change of working directory to path " + this.cmd[1] + " failed."); } return true; } public boolean CLOSE() { return DISCONNECT(); } private void rmForced(final String path) throws IOException { // first try: send DELE command (to delete a file) send("DELE " + path); // read reply final String reply1 = receive(); if (isNotPositiveCompletion(reply1)) { // second try: send a RMD command (to delete a directory) send("RMD " + path); // read reply final String reply2 = receive(); if (isNotPositiveCompletion(reply2)) { // third try: test if this thing is a directory or file and send // appropriate error message if (isFolder(path)) { throw new IOException(reply2); } throw new IOException(reply1); } } } /** * @param path * @return date of entry on ftp-server or now if date can not be obtained */ public Date entryDate(final String path) { final entryInfo info = fileInfo(path); Date date = null; if (info != null) { date = info.date; } return date; } public boolean DEL() { if (this.cmd.length != 2) { log.warn("Syntax: DEL <file>"); return true; } if (notConnected()) { return LDEL(); } try { rmForced(this.cmd[1]); } catch (final IOException e) { log.warn("Error: deletion of file " + this.cmd[1] + " failed."); } return true; } public boolean RM() { return DEL(); } public boolean DIR() { if (this.cmd.length > 2) { log.warn("Syntax: DIR [<path>|<file>]"); return true; } if (notConnected()) { return LDIR(); } try { List<String> l; if (this.cmd.length == 2) { l = list(this.cmd[1], false); } else { l = list(".", false); } printElements(l); } catch (final IOException e) { log.warn("Error: remote list not available (1): " + e.getMessage()); } return true; } public boolean DISCONNECT() { try { quit(); log.info("---- Connection closed."); } catch (final IOException e) { // Connection to server lost // do not append any error to errPrintln because we can silently go over this error // otherwise the client treats this case as an error and does not accept the result of the session } try { closeConnection(); } catch (final IOException e) { this.ControlSocket = null; this.DataSocketActive = null; this.DataSocketPassive = null; this.clientInput = null; this.clientOutput = null; } this.prompt = "ftp [local]>"; return true; } private String quit() throws IOException { send("QUIT"); // read status reply final String reply = receive(); if (isNotPositiveCompletion(reply)) { throw new IOException(reply); } closeConnection(); return reply; } public boolean EXIT() { return QUIT(); } public boolean GET() { if ((this.cmd.length < 2) || (this.cmd.length > 3)) { log.warn("Syntax: GET <remote-file> [<local-file>]"); return true; } final String remote = this.cmd[1]; // (new File(cmd[1])).getName(); final boolean withoutLocalFile = this.cmd.length == 2; final String localFilename = (withoutLocalFile) ? remote : this.cmd[2]; final File local = absoluteLocalFile(localFilename); if (local.exists()) { log.warn("Error: local file " + local.toString() + " already exists.\n" + " File " + remote + " not retrieved. Local file unchanged."); } else { if (withoutLocalFile) { retrieveFilesRecursively(remote, false); } else { try { get(local.getAbsolutePath(), remote); } catch (final IOException e) { log.warn("Error: retrieving file " + remote + " failed. (" + e.getMessage() + ")"); } } } return true; } /** * @param localFilename * @return */ private File absoluteLocalFile(final String localFilename) { File local; final File l = new File(localFilename); if (l.isAbsolute()) { local = l; } else { local = new File(this.currentLocalPath, localFilename); } return local; } private void retrieveFilesRecursively(final String remote, final boolean delete) { final File local = absoluteLocalFile(remote); try { get(local.getAbsolutePath(), remote); try { if (delete) { rmForced(remote); } } catch (final IOException eee) { log.warn("Warning: remote file or path " + remote + " cannot be removed."); } } catch (final IOException e) { if (e.getMessage().startsWith("550")) { // maybe it's a "not a plain file" error message", then it can // be a folder // test if this exists (then it should be a folder) if (isFolder(remote)) { // copy the whole directory exec("cd \"" + remote + "\";lmkdir \"" + remote + "\";lcd \"" + remote + "\"", true); // exec("mget *",true); try { for (final String element : list(".", false)) { retrieveFilesRecursively(element, delete); } } catch (final IOException ee) { } exec("cd ..;lcd ..", true); try { if (delete) { rmForced(remote); } } catch (final IOException eee) { log.warn("Warning: remote file or path " + remote + " cannot be removed."); } } else { log.warn("Error: remote file or path " + remote + " does not exist."); } } else { log.warn("Error: retrieving file " + remote + " failed. (" + e.getMessage() + ")"); } } } /** * checks if path is a folder * * @param path * @return true if ftp-server changes to path */ public boolean isFolder(final String path) { try { // /// try to parse LIST output (1 command) final entryInfo info = fileInfo(path); if (info != null) { return info.type == filetype.directory; } // /// try to change to folder (4 commands) // current folder final String currentFolder = pwd(); // check if we can change to folder send("CWD " + path); final String reply = receive(); if (isNotPositiveCompletion(reply)) { throw new IOException(reply); } // check if we actually changed into the folder final String changedPath = pwd(); if (!(changedPath.equals(path) || changedPath.equals(currentFolder + (currentFolder.endsWith("/") ? "" : "/") + path))) { throw new IOException("folder is '" + changedPath + "' should be '" + path + "'"); } // return to last folder send("CWD " + currentFolder); /*reply =*/ receive(); return true; } catch (final IOException e) { return false; } } public boolean GLOB() { if (this.cmd.length != 1) { log.warn("Syntax: GLOB (no parameter)"); return true; } this.glob = !this.glob; log.info("---- globbing is now turned " + ((this.glob) ? "ON" : "OFF")); return true; } public boolean HASH() { log.warn("no games implemented"); return true; } /* * private static String[] shift(String args[]) { if ((args == null) || * (args.length == 0)) return args; else { String[] newArgs = new * String[args.length-1]; System.arraycopy(args, 1, newArgs, 0, * args.length-1); return newArgs; } } public boolean JAR() { //Sun * proprietary API may be removed in a future Java release * sun.tools.jar.Main.main(shift(cmd)); return true; } */ public boolean JJENCODE() { if (this.cmd.length != 2) { log.warn("Syntax: JJENCODE <path>"); return true; } final String path = this.cmd[1]; final File dir = new File(path); final File newPath = dir.isAbsolute() ? dir : new File(this.currentLocalPath, path); if (newPath.exists()) { if (newPath.isDirectory()) { // exec("cd \"" + remote + "\";lmkdir \"" + remote + "\";lcd \"" // + remote + "\"",true); /* * if not exist %1\nul goto :error cd %1 c:\jdk1.2.2\bin\jar * -cfM0 ..\%1.jar *.* cd .. c:\jdk1.2.2\bin\jar -cfM %1.jj * %1.jar del %1.jar */ String s = ""; final String[] l = newPath.list(); for (final String element : l) { s = s + " \"" + element + "\""; } exec("cd \"" + path + "\";jar -cfM0 ../\"" + path + ".jar\"" + s, true); exec("cd ..;jar -cfM \"" + path + ".jj\" \"" + path + ".jar\"", true); exec("rm \"" + path + ".jar\"", true); } else { log.warn("Error: local path " + newPath.toString() + " denotes not to a directory."); } } else { log.warn("Error: local path " + newPath.toString() + " does not exist."); } return true; } public boolean JJDECODE() { if (this.cmd.length != 2) { log.warn("Syntax: JJENCODE <path>"); return true; } final String path = this.cmd[1]; final File dir = new File(path); final File newPath = dir.isAbsolute() ? dir : new File(this.currentLocalPath, path); final File newFolder = new File(newPath.toString() + ".dir"); if (newPath.exists()) { if (!newPath.isDirectory()) { if (!newFolder.mkdir()) { /* * if not exist %1.jj goto :error mkdir %1.dir copy %1.jj * %1.dir\ > %1.dummy && del %1.dummy cd %1.dir * c:\jdk1.2.2\bin\jar -xf %1.jj del %1.jj * c:\jdk1.2.2\bin\jar -xf %1.jar del %1.jar cd .. */ exec("mkdir \"" + path + ".dir\"", true); } else { log.warn("Error: target dir " + newFolder.toString() + " cannot be created"); } } else { log.warn("Error: local path " + newPath.toString() + " must denote to jar/jar file"); } } else { log.warn("Error: local path " + newPath.toString() + " does not exist."); } return true; } private static String[] argList2StringArray(final String argList) { return argList.split("\\s"); } public boolean JOIN(String[] args) { // make sure the specified dest file does not exist final String dest_name = args[1]; final File dest_file = new File(dest_name); if (dest_file.exists()) { log.warn("join: destination file " + dest_name + " already exists"); return true; } // prepare or search file names of the input files to be joined String source_name; File source_file; int pc = -1; // create new string array with file names // scan first for the files pc = 0; source_name = dest_name + ".000"; String argString = ""; source_file = new File(source_name); while ((source_file.exists()) && (source_file.isFile()) && (source_file.canRead())) { argString = argString + " " + source_name; pc++; source_name = dest_name + (pc < 10 ? ".00" + pc : (pc < 100 ? ".0" + pc : "." + pc)); source_file = new File(source_name); } args = argList2StringArray(argString.substring(1)); // do the join FileOutputStream dest = null; FileInputStream source = null; byte[] buffer; int bytes_read = 0; try { // open output file dest = new FileOutputStream(dest_file); buffer = new byte[1024]; // append all source files for (pc = 0; pc < args.length; pc++) { // open the source file source_name = args[pc]; source_file = new File(source_name); source = new FileInputStream(source_file); // start with the copy of one source file while (true) { bytes_read = source.read(buffer); if (bytes_read == -1) { break; } dest.write(buffer, 0, bytes_read); } // copy finished. close source file try { source.close(); } catch (final IOException e) { } } // close the output file try { dest.close(); } catch (final IOException e) { } // if we come to this point then everything went fine // if the user wanted to delete the source it is save to do so now for (pc = 0; pc < args.length; pc++) { try { if (!(new File(args[pc])).delete()) { log.warn("join: unable to delete file " + args[pc]); } } catch (final SecurityException e) { log.warn("join: no permission to delete file " + args[pc]); } } } catch (final FileNotFoundException e) { } catch (final IOException e) { } // clean up finally { // close any opened streams if (dest != null) { try { dest.close(); } catch (final IOException e) { } } if (source != null) { try { source.close(); } catch (final IOException e) { } } // print appropriate message log.warn("join created output from " + args.length + " source files"); } return true; } public boolean COPY(final String[] args) { final File dest_file = new File(args[2]); if (dest_file.exists()) { log.warn("copy: destination file " + args[2] + " already exists"); return true; } int bytes_read = 0; FileOutputStream dest = null; FileInputStream source = null; try { // open output file dest = new FileOutputStream(dest_file); final byte[] buffer = new byte[1024]; // open the source file final File source_file = new File(args[1]); source = new FileInputStream(source_file); // start with the copy of one source file while (true) { bytes_read = source.read(buffer); if (bytes_read == -1) { break; } dest.write(buffer, 0, bytes_read); } } catch (final FileNotFoundException e) { } catch (final IOException e) { } finally { // copy finished. close source file if (source != null) { try { source.close(); } catch (final IOException e) { } } // close the output file if (dest != null) { try { dest.close(); } catch (final IOException e) { } } } return true; } public boolean JAVA() { String s = "JAVA"; for (int i = 1; i < this.cmd.length; i++) { s = s + " " + this.cmd[i]; } try { send(s); /* String reply = */receive(); } catch (final IOException e) { } return true; } public boolean LCD() { if (this.cmd.length != 2) { log.warn("Syntax: LCD <path>"); return true; } final String path = this.cmd[1]; final File dir = new File(path); File newPath = dir.isAbsolute() ? dir : new File(this.currentLocalPath, path); try { newPath = new File(newPath.getCanonicalPath()); } catch (final IOException e) { } if (newPath.exists()) { if (newPath.isDirectory()) { this.currentLocalPath = newPath; log.info("---- New local path: " + this.currentLocalPath.toString()); } else { log.warn("Error: local path " + newPath.toString() + " denotes not a directory."); } } else { log.warn("Error: local path " + newPath.toString() + " does not exist."); } return true; } public boolean LDEL() { return LRM(); } public boolean LDIR() { if (this.cmd.length != 1) { log.warn("Syntax: LDIR (no parameter)"); return true; } final String[] name = this.currentLocalPath.list(); for (String element : name) { log.info(ls(new File(this.currentLocalPath, element))); } return true; } /** * parse LIST of file * * @param path * on ftp-server * @return null if info cannot be determined or error occures */ public entryInfo fileInfo(final String path) { if (this.infoCache.containsKey(path)) { return this.infoCache.get(path); } try { /* * RFC959 page 33f: If the argument is a pathname, the command is * analogous to the "list" command except that data shall be * transferred over the control connection. */ send("STAT " + path); final String reply = receive(); if (isNotPositiveCompletion(reply)) { throw new IOException(reply); } // check if reply is correct multi-line reply final String[] lines = reply.split("\\r\\n"); if (lines.length < 3) { throw new IOException(reply); } final int startCode = getStatusCode(lines[0]); final int endCode = getStatusCode(lines[lines.length - 1]); if (startCode != endCode) { throw new IOException(reply); } // first line which gives a result is taken (should be only one) entryInfo info = null; final int endFor = lines.length - 1; for (int i = 1; i < endFor; i++) { info = parseListData(lines[i]); if (info != null) { this.infoCache.put(path, info); break; } } return info; } catch (final IOException e) { return null; } } /** * returns status of reply * * 1 Positive Preliminary reply 2 Positive Completion reply 3 Positive * Intermediate reply 4 Transient Negative Completion reply 5 Permanent * Negative Completion reply * * @param reply * @return first digit of the reply code */ private int getStatus(final String reply) { return Integer.parseInt(reply.substring(0, 1)); } /** * gives reply code * * @param reply * @return */ private int getStatusCode(final String reply) { return Integer.parseInt(reply.substring(0, 3)); } /** * checks if status code is in group 2 ("2xx message") * * @param reply * @return */ private boolean isNotPositiveCompletion(final String reply) { return getStatus(reply) != 2; } private final static Pattern lsStyle = Pattern.compile("^([-\\w]{10}).\\s*\\d+\\s+[-\\w]+\\s+[-\\w]+\\s+(\\d+)\\s+(\\w{3})\\s+(\\d+)\\s+(\\d+:?\\d*)\\s+(.*)$"); /** * parses output of LIST from ftp-server currently UNIX ls-style only, ie: * -rw-r--r-- 1 root other 531 Jan 29 03:26 README dr-xr-xr-x 2 root 512 Apr * 8 1994 etc * * @param line * @return null if not parseable */ private static entryInfo parseListData(final String line) { // groups: 1: rights, 2: size, 3: month, 4: day, 5: time or year, 6: name final Matcher tokens = lsStyle.matcher(line); if (tokens.matches() && tokens.groupCount() == 6) { filetype type = filetype.file; if (tokens.group(1).startsWith("d")) type = filetype.directory; if (tokens.group(1).startsWith("l")) type = filetype.link; long size = -1; try { size = Long.parseLong(tokens.group(2)); } catch (final NumberFormatException e) { log.warn("not a number in list-entry: ", e); return null; } String time; String year; if (tokens.group(5).contains(":")) { time = tokens.group(5); year = String.valueOf(Calendar.getInstance().get(Calendar.YEAR)); // current // year } else { time = "00:00"; year = tokens.group(5); } // construct date string // this has to be done, because the list-entry may have multiple // spaces, tabs or so Date date; final String dateString = tokens.group(3) + " " + tokens.group(4) + " " + year + " " + time; try { synchronized(lsDateFormat) { date = lsDateFormat.parse(dateString); } } catch (final ParseException e) { log.warn("---- Error: not ls date-format '" + dateString, e); date = new Date(); } final String filename = tokens.group(6); return new entryInfo(type, size, date, filename); } return null; } public static final entryInfo POISON_entryInfo = new entryInfo(); public static enum filetype { file, link, directory; } /** * parameter class * * @author danielr * @since 2008-03-13 r4558 */ public static class entryInfo { /** * file type */ public final filetype type; /** * size in bytes */ public final long size; /** * date of file */ public final Date date; /** * name of entry */ public String name; public entryInfo() { this.type = filetype.file; this.size = -1; this.date = null; this.name = null; } /** * constructor * * @param isDir * @param size * bytes * @param date * @param name */ public entryInfo(final filetype type, final long size, final Date date, final String name) { this.type = type; this.size = size; this.date = date; this.name = name; } /* * (non-Javadoc) * * @see java.lang.Object#toString() */ @Override public String toString() { final StringBuilder info = new StringBuilder(100); info.append(this.name); info.append(" (type="); info.append(this.type.name()); info.append(", size="); info.append(this.size); info.append(", "); info.append(this.date); info.append(")"); return info.toString(); } } private String ls(final File inode) { if ((inode == null) || (!inode.exists())) { return ""; } String s = ""; if (inode.isDirectory()) { s = s + "d"; } else if (inode.isFile()) { s = s + "-"; } else { s = s + "?"; } if (inode.canRead()) { s = s + "r"; } else { s = s + "-"; } if (inode.canWrite()) { s = s + "w"; } else { s = s + "-"; } s = s + " " + lenformatted(Long.toString(inode.length()), 9); final DateFormat df = DateFormat.getDateTimeInstance(); s = s + " " + df.format(new Date(inode.lastModified())); s = s + " " + inode.getName(); if (inode.isDirectory()) { s = s + "/"; } return s; } private String lenformatted(String s, int l) { l = l - s.length(); while (l > 0) { s = " " + s; l--; } return s; } public boolean LITERAL() { if (this.cmd.length == 1) { log.warn("Syntax: LITERAL <ftp-command> [<command-argument>] (see RFC959)"); return true; } String s = ""; for (int i = 1; i < this.cmd.length; i++) { s = s + " " + this.cmd[i]; } try { literal(s.substring(1)); } catch (final IOException e) { log.warn("Error: Syntax of FTP-command wrong. See RFC959 for details."); } return true; } public boolean LLS() { return LDIR(); } public boolean LMD() { return LMKDIR(); } public boolean LMKDIR() { if (this.cmd.length != 2) { log.warn("Syntax: LMKDIR <folder-name>"); return true; } final File f = new File(this.currentLocalPath, this.cmd[1]); if (f.exists()) { log.warn("Error: local file/folder " + this.cmd[1] + " already exists"); } else { if (!f.mkdir()) { log.warn("Error: creation of local folder " + this.cmd[1] + " failed"); } } return true; } public boolean LMV() { if (this.cmd.length != 3) { log.warn("Syntax: LMV <from> <to>"); return true; } final File from = new File(this.cmd[1]); final File to = new File(this.cmd[2]); if (!to.exists()) { if (from.renameTo(to)) { log.info("---- \"" + from.toString() + "\" renamed to \"" + to.toString() + "\""); } else { log.warn("rename failed"); } } else { log.warn("\"" + to.toString() + "\" already exists"); } return true; } public boolean LPWD() { if (this.cmd.length != 1) { log.warn("Syntax: LPWD (no parameter)"); return true; } log.info("---- Local path: " + this.currentLocalPath.toString()); return true; } public boolean LRD() { return LMKDIR(); } public boolean LRMDIR() { if (this.cmd.length != 2) { log.warn("Syntax: LRMDIR <folder-name>"); return true; } final File f = new File(this.currentLocalPath, this.cmd[1]); if (!f.exists()) { log.warn("Error: local folder " + this.cmd[1] + " does not exist"); } else { if (!f.delete()) { log.warn("Error: deletion of local folder " + this.cmd[1] + " failed"); } } return true; } public boolean LRM() { if (this.cmd.length != 2) { log.warn("Syntax: LRM <file-name>"); return true; } final File f = new File(this.currentLocalPath, this.cmd[1]); if (!f.exists()) { log.warn("Error: local file " + this.cmd[1] + " does not exist"); } else { if (!f.delete()) { log.warn("Error: deletion of file " + this.cmd[1] + " failed"); } } return true; } public boolean LS() { if (this.cmd.length > 2) { log.warn("Syntax: LS [<path>|<file>]"); return true; } if (notConnected()) { return LLS(); } try { List<String> l; if (this.cmd.length == 2) { l = list(this.cmd[1], true); } else { l = list(".", true); } printElements(l); } catch (final IOException e) { log.warn("Error: remote list not available (2): " + e.getMessage()); } return true; } /** * @param list */ private void printElements(final List<String> list) { log.info("---- v---v---v---v---v---v---v---v---v---v---v---v---v---v---v---v---v---v---v"); for (final String element : list) { log.info(element); } log.info("---- ^---^---^---^---^---^---^---^---^---^---^---^---^---^---^---^---^---^---^"); } public List<String> list(final String path, final boolean extended) throws IOException { createDataSocket(); send("CWD " + path); String reply = receive(); // get status code int status = getStatus(reply); if (status > 2) { throw new IOException(reply); } // send command to the control port if (extended) { send("LIST"); } else { send("NLST"); } // read status of the command from the control port reply = receive(); // get status code status = getStatus(reply); if (status != 1) { throw new IOException(reply); } // starting data transaction final Socket dataSocket = getDataSocket(); final BufferedReader dataStream = new BufferedReader(new InputStreamReader(dataSocket.getInputStream())); // read file system data String line; final ArrayList<String> files = new ArrayList<String>(); try { while ((line = dataStream.readLine()) != null) { if (!line.startsWith("total ")) { files.add(line); } } } catch (final IOException e1) { e1.printStackTrace(); } finally {try { // shutdown data connection dataStream.close(); // Closing the returned InputStream will closeDataSocket(); // close the associated socket. } catch (final IOException e) { e.printStackTrace(); }} // after stream is empty we should get control completion echo reply = receive(); //System.out.println("reply of LIST: " + reply); // boolean success = !isNotPositiveCompletion(reply); //for (String s: files) System.out.println("FILES of '" + path + "': " + s); files.trimToSize(); return files; } public boolean MDIR() { return MKDIR(); } public boolean MKDIR() { if (this.cmd.length != 2) { log.warn("Syntax: MKDIR <folder-name>"); return true; } if (notConnected()) { return LMKDIR(); } try { // send mkdir command send("MKD " + this.cmd[1]); // read reply final String reply = receive(); if (isNotPositiveCompletion(reply)) { throw new IOException(reply); } } catch (final IOException e) { log.warn("Error: creation of folder " + this.cmd[1] + " failed"); } return true; } public boolean MGET() { if (this.cmd.length != 2) { log.warn("Syntax: MGET <file-pattern>"); return true; } try { mget(this.cmd[1], false); } catch (final IOException e) { log.warn("Error: mget failed (" + e.getMessage() + ")"); } return true; } private void mget(final String pattern, final boolean remove) throws IOException { final List<String> l = list(".", false); File local; for (final String remote : l) { if (matches(remote, pattern)) { local = new File(this.currentLocalPath, remote); if (local.exists()) { log.warn("Warning: local file " + local.toString() + " overwritten."); if(!local.delete()) log.warn("Warning: local file " + local.toString() + " could not be deleted."); } retrieveFilesRecursively(remote, remove); } } } public boolean MOVEDOWN() { if (this.cmd.length != 2) { log.warn("Syntax: MOVEDOWN <file-pattern>"); return true; } try { mget(this.cmd[1], true); } catch (final IOException e) { log.warn("Error: movedown failed (" + e.getMessage() + ")"); } return true; } /** * public boolean MOVEUP() { } * * @return */ public boolean MV() { if (this.cmd.length != 3) { log.warn("Syntax: MV <from> <to>"); return true; } if (notConnected()) { return LMV(); } try { // send rename commands send("RNFR " + this.cmd[1]); // read reply String reply = receive(); if (isNotPositiveCompletion(reply)) { throw new IOException(reply); } send("RNTO " + this.cmd[2]); // read reply reply = receive(); if (isNotPositiveCompletion(reply)) { throw new IOException(reply); } } catch (final IOException e) { log.warn("Error: rename of " + this.cmd[1] + " to " + this.cmd[2] + " failed."); } return true; } public boolean NOOP() { if (this.cmd.length != 1) { log.warn("Syntax: NOOP (no parameter)"); return true; } try { literal("NOOP"); } catch (final IOException e) { log.warn("Error: server does not know how to do nothing"); } return true; } public boolean OPEN() { if ((this.cmd.length < 2) || (this.cmd.length > 3)) { log.warn("Syntax: OPEN <host> [<port>]"); return true; } int port = 21; if (this.cmd.length == 3) { try { port = java.lang.Integer.parseInt(this.cmd[2]); } catch (final NumberFormatException e) { port = 21; } } if (this.cmd[1].indexOf(':',0) > 0) { // port is given port = java.lang.Integer.parseInt(this.cmd[1].substring(this.cmd[1].indexOf(':',0) + 1)); this.cmd[1] = this.cmd[1].substring(0, this.cmd[1].indexOf(':',0)); } try { open(this.cmd[1], port); log.info("---- Connection to " + this.cmd[1] + " established."); this.prompt = "ftp [" + this.cmd[1] + "]>"; } catch (final IOException e) { log.warn("Error: connecting " + this.cmd[1] + " on port " + port + " failed: " + e.getMessage()); } return true; } public void open(final String host, final int port) throws IOException { if (this.ControlSocket != null) { exec("close", false); // close any existing connections first } try { this.ControlSocket = new Socket(); this.ControlSocket.setSoTimeout(getTimeout()); this.ControlSocket.setKeepAlive(true); this.ControlSocket.setTcpNoDelay(true); // no accumulation until buffer is full this.ControlSocket.setSoLinger(false, getTimeout()); // !wait for all data being written on close() this.ControlSocket.setSendBufferSize(1440); // read http://www.cisco.com/warp/public/105/38.shtml this.ControlSocket.setReceiveBufferSize(1440); // read http://www.cisco.com/warp/public/105/38.shtml this.ControlSocket.connect(new InetSocketAddress(host, port), 1000); this.clientInput = new BufferedReader(new InputStreamReader(this.ControlSocket.getInputStream())); this.clientOutput = new DataOutputStream(new BufferedOutputStream(this.ControlSocket.getOutputStream())); // read and return server message this.host = host; this.port = port; this.remotemessage = receive(); if ((this.remotemessage != null) && (this.remotemessage.length() > 3)) { this.remotemessage = this.remotemessage.substring(4); } } catch (final IOException e) { // if a connection was opened, it should not be used closeConnection(); throw new IOException(e.getMessage()); } } /** * @return */ public boolean notConnected() { return this.ControlSocket == null; } /** * close all sockets * * @throws IOException */ private void closeConnection() throws IOException { // cleanup if (this.clientOutput != null) this.clientOutput.close(); if (this.clientInput != null) this.clientInput.close(); if (this.ControlSocket != null) this.ControlSocket.close(); if (this.DataSocketActive != null) this.DataSocketActive.close(); if (this.DataSocketPassive != null) this.DataSocketPassive.close(); } public boolean PROMPT() { log.warn("prompt is always off"); return true; } public boolean PUT() { if ((this.cmd.length < 2) || (this.cmd.length > 3)) { log.warn("Syntax: PUT <local-file> [<remote-file>]"); return true; } final File local = new File(this.currentLocalPath, this.cmd[1]); final String remote = (this.cmd.length == 2) ? local.getName() : this.cmd[2]; if (!local.exists()) { log.warn("Error: local file " + local.toString() + " does not exist."); log.warn(" Remote file " + remote + " not overwritten."); } else { try { put(local.getAbsolutePath(), remote); } catch (final IOException e) { log.warn("Error: transmitting file " + local.toString() + " failed."); } } return true; } public boolean PWD() { if (this.cmd.length > 1) { log.warn("Syntax: PWD (no parameter)"); return true; } if (notConnected()) { return LPWD(); } try { log.info("---- Current remote path is: " + pwd()); } catch (final IOException e) { log.warn("Error: remote path not available"); } return true; } private String pwd() throws IOException { // send pwd command send("PWD"); // read current directory final String reply = receive(); if (isNotPositiveCompletion(reply)) { throw new IOException(reply); } // parse directory name out of the reply return reply.substring(5, reply.lastIndexOf('"')); } public boolean REMOTEHELP() { if (this.cmd.length != 1) { log.warn("Syntax: REMOTEHELP (no parameter)"); return true; } try { literal("HELP"); } catch (final IOException e) { log.warn("Error: remote help not supported by server."); } return true; } public boolean RMDIR() { if (this.cmd.length != 2) { log.warn("Syntax: RMDIR <folder-name>"); return true; } if (notConnected()) { return LRMDIR(); } try { rmForced(this.cmd[1]); } catch (final IOException e) { log.warn("Error: deletion of folder " + this.cmd[1] + " failed."); } return true; } public boolean QUIT() { if (!notConnected()) { exec("close", false); } return false; } public boolean RECV() { return GET(); } /** * size of file on ftp-server (maybe size of directory-entry is possible) * * @param path * @return size in bytes or -1 if size cannot be determinied */ public long fileSize(final String path) { long size = -1; try { // extended FTP size = size(path); } catch (final IOException e) { // else with LIST-data final entryInfo info = fileInfo(path); if (info != null) { size = info.size; } } return size; } public int size(final String path) throws IOException { // get the size of a file. If the given path targets to a directory, a // -1 is returned // this function is not supported by standard rfc 959. The method is // descibed in RFC 3659 Extensions to FTP // if the method is not supported by the target server, this throws an // IOException with the // server response as exception message // send command to the control port send("SIZE " + path); // read status of the command from the control port final String reply = receive(); if (getStatusCode(reply) != 213) { throw new IOException(reply); } try { return Integer.parseInt(reply.substring(4)); } catch (final NumberFormatException e) { throw new IOException(reply); } } public boolean USER() { if (this.cmd.length != 3) { log.warn("Syntax: USER <user-name> <password>"); return true; } try { login(this.cmd[1], this.cmd[2]); log.info("---- Granted access for user " + this.cmd[1] + "."); } catch (final IOException e) { log.warn("Error: authorization of user " + this.cmd[1] + " failed: " + e.getMessage()); } return true; } public boolean APPEND() { log.warn("not yet supported"); return true; } public boolean HELP() { log.info("---- ftp HELP ----"); log.info(""); log.info("This ftp client shell can act as command shell for the local host as well for the"); log.info("remote host. Commands that point to the local host are preceded by 'L'."); log.info(""); log.info("Supported Commands:"); log.info("ASCII"); log.info(" switch remote server to ASCII transfer mode"); log.info("BINARY"); log.info(" switch remote server to BINARY transfer mode"); log.info("BYE"); log.info(" quit the command shell (same as EXIT)"); log.info("CD <path>"); log.info(" change remote path"); log.info("CLOSE"); log.info(" close connection to remote host (same as DISCONNECT)"); log.info("DEL <file>"); log.info(" delete file on remote server (same as RM)"); log.info("RM <file>"); log.info(" remove file from remote server (same as DEL)"); log.info("DIR [<path>|<file>] "); log.info(" print file information for remote directory or file"); log.info("DISCONNECT"); log.info(" disconnect from remote server (same as CLOSE)"); log.info("EXIT"); log.info(" quit the command shell (same as BYE)"); log.info("GET <remote-file> [<local-file>]"); log.info(" load <remote-file> from remote server and store it locally,"); log.info(" optionally to <local-file>. if the <remote-file> is a directory,"); log.info(" then all files in that directory are retrieved,"); log.info(" including recursively all subdirectories."); log.info("GLOB"); log.info(" toggles globbing: matching with wild cards or not"); log.info("COPY"); log.info(" copies local files"); log.info("LCD <path>"); log.info(" local directory change"); log.info("LDEL <file>"); log.info(" local file delete"); log.info("LDIR"); log.info(" shows local directory content"); log.info("LITERAL <ftp-command> [<command-argument>]"); log.info(" Sends FTP commands as documented in RFC959"); log.info("LLS"); log.info(" as LDIR"); log.info("LMD"); log.info(" as LMKDIR"); log.info("LMV <local-from> <local-to>"); log.info(" copies local files"); log.info("LPWD"); log.info(" prints local path"); log.info("LRD"); log.info(" as LMKDIR"); log.info("LRMD <folder-name>"); log.info(" deletes local directory <folder-name>"); log.info("LRM <file-name>"); log.info(" deletes local file <file-name>"); log.info("LS [<path>|<file>]"); log.info(" prints list of remote directory <path> or information of file <file>"); log.info("MDIR"); log.info(" as MKDIR"); log.info("MGET <file-pattern>"); log.info(" copies files from remote server that fits into the"); log.info(" pattern <file-pattern> to the local path."); log.info("MOVEDOWN <file-pattern>"); log.info(" copies files from remote server as with MGET"); log.info(" and deletes them afterwards on the remote server"); log.info("MV <from> <to>"); log.info(" moves or renames files on the local host"); log.info("NOOP"); log.info(" sends the NOOP command to the remote server (which does nothing)"); log.info(" This command is usually used to measure the speed of the remote server."); log.info("OPEN <host[':'port]> [<port>]"); log.info(" connects the ftp shell to the remote server <host>. Optionally,"); log.info(" a port number can be given, the default port number is 21."); log.info(" Example: OPEN localhost:2121 or OPEN 192.168.0.1 2121"); log.info("PROMPT"); log.info(" compatibility command, that usually toggles beween prompting on or off."); log.info(" ftp has prompting switched off by default and cannot switched on."); log.info("PUT <local-file> [<remote-file>]"); log.info(" copies the <local-file> to the remote server to the current remote path or"); log.info(" optionally to the given <remote-file> path."); log.info("PWD"); log.info(" prints current path on the remote server."); log.info("REMOTEHELP"); log.info(" asks the remote server to print the help text of the remote server"); log.info("RMDIR <folder-name>"); log.info(" removes the directory <folder-name> on the remote server"); log.info("QUIT"); log.info(" exits the ftp application"); log.info("RECV"); log.info(" as GET"); log.info("USER <user-name> <password>"); log.info(" logs into the remote server with the user <user-name>"); log.info(" and the password <password>"); log.info(""); log.info(""); log.info("EXAMPLE:"); log.info("a standard sessions looks like this"); log.info(">open 192.168.0.1:2121"); log.info(">user anonymous bob"); log.info(">pwd"); log.info(">ls"); log.info(">....."); log.info(""); log.info(""); return true; } public boolean QUOTE() { log.warn("not yet supported"); return true; } public boolean BELL() { log.warn("not yet supported"); return true; } public boolean MDELETE() { log.warn("not yet supported"); return true; } public boolean SEND() { log.warn("not yet supported"); return true; } public boolean DEBUG() { log.warn("not yet supported"); return true; } public boolean MLS() { log.warn("not yet supported"); return true; } public boolean TRACE() { log.warn("not yet supported"); return true; } public boolean MPUT() { log.warn("not yet supported"); return true; } public boolean TYPE() { log.warn("not yet supported"); return true; } public boolean CREATE() { log.warn("not yet supported"); return true; } // helper functions private boolean matches(final String name, final String pattern) { // checks whether the string name matches with the pattern // the pattern may contain characters '*' as wildcard for several // characters (also none) and '?' to match exactly one characters // log.info("MATCH " + name + " " + pattern); if (!this.glob) { return name.equals(pattern); } if (pattern.equals("*")) { return true; } if (pattern.length() > 0 && pattern.charAt(0) == '*' && pattern.endsWith("*")) { return // avoid recursion deadlock ((matches(name, pattern.substring(1))) || (matches(name, pattern.substring(0, pattern.length() - 1)))); } try { int i = pattern.indexOf('?',0); if (i >= 0) { if (!(matches(name.substring(0, i), pattern.substring(0, i)))) { return false; } return (matches(name.substring(i + 1), pattern.substring(i + 1))); } i = pattern.indexOf('*',0); if (i >= 0) { if (!(name.substring(0, i).equals(pattern.substring(0, i)))) { return false; } if (pattern.length() == i + 1) { return true; // pattern would be '*' } return (matches(reverse(name.substring(i)), reverse(pattern.substring(i + 1)) + "*")); } return name.equals(pattern); } catch (final java.lang.StringIndexOutOfBoundsException e) { // this is normal. it's a lazy implementation return false; } } private String reverse(final String s) { if (s.length() < 2) { return s; } return reverse(s.substring(1)) + s.charAt(0); } // protocoll socket commands private void send(final String buf) throws IOException { if (this.clientOutput == null) return; byte[] b = buf.getBytes(StandardCharsets.UTF_8); this.clientOutput.write(b, 0, b.length); this.clientOutput.write('\r'); this.clientOutput.write('\n'); this.clientOutput.flush(); if (buf.startsWith("PASS")) { log.info("> PASS ********"); } else { log.info("> " + buf); } } private String receive() throws IOException { // last reply starts with 3 digit number followed by space String reply; while (true) { if (this.clientInput == null) { throw new IOException("Server has presumably shut down the connection."); } reply = this.clientInput.readLine(); // sanity check if (reply == null) { throw new IOException("Server has presumably shut down the connection."); } log.info("< " + reply); // serverResponse.addElement(reply); if (reply.length() >= 4 && Character.isDigit(reply.charAt(0)) && Character.isDigit(reply.charAt(1)) && Character.isDigit(reply.charAt(2)) && (reply.charAt(3) == ' ')) { break; // end of reply } } // return last reply line return reply; } private void sendTransferType(final char type) throws IOException { send("TYPE " + type); final String reply = receive(); if (isNotPositiveCompletion(reply)) { throw new IOException(reply); } } /** * @return * @throws IOException */ private Socket getDataSocket() throws IOException { Socket data; if (isPassive()) { if (this.DataSocketPassive == null) { createDataSocket(); } data = this.DataSocketPassive; } else { if (this.DataSocketActive == null) { createDataSocket(); } data = this.DataSocketActive.accept(); } return data; } /** * create data channel * * @throws IOException */ private void createDataSocket() throws IOException { if (isPassive()) { try { createPassiveDataPort(); } catch (final IOException e) { createActiveDataPort(); } } else { try { createActiveDataPort(); } catch (final IOException e) { createPassiveDataPort(); } } } /** * use passive ftp? * * @return */ private boolean isPassive() { return this.DataSocketPassiveMode; } private void createActiveDataPort() throws IOException { // create data socket and bind it to free port available this.DataSocketActive = new ServerSocket(0); this.DataSocketActive.setSoTimeout(getTimeout()); this.DataSocketActive.setReceiveBufferSize(1440); // read http://www.cisco.com/warp/public/105/38.shtml applyDataSocketTimeout(); // get port socket has been bound to final int DataPort = this.DataSocketActive.getLocalPort(); // client ip // InetAddress LocalIp = serverCore.publicIP(); // InetAddress LocalIp = // DataSocketActive.getInetAddress().getLocalHost(); // save ip address in high byte order // byte[] Bytes = LocalIp.getAddress(); final byte[] b = Domains.myPublicIPv4().iterator().next().getAddress(); // bytes greater than 127 should not be printed as negative final short[] s = new short[4]; for (int i = 0; i < 4; i++) { s[i] = b[i]; if (s[i] < 0) { s[i] += 256; } } // send port command via control socket: // four ip address shorts encoded and two port shorts encoded send("PORT " + // "127,0,0,1," + s[0] + "," + s[1] + "," + s[2] + "," + s[3] + "," + ((DataPort & 0xff00) >> 8) + "," + (DataPort & 0x00ff)); // read status of the command from the control port final String reply = receive(); // check status code if (isNotPositiveCompletion(reply)) { throw new IOException(reply); } this.DataSocketPassiveMode = false; } private void createPassiveDataPort() throws IOException { // send port command via control socket: // four ip address shorts encoded and two port shorts encoded send("PASV"); // read status of the command from the control port String reply = receive(); // check status code if (getStatusCode(reply) != 227) { throw new IOException(reply); } // parse the status return: address should start at the first number int pos = 4; while ((pos < reply.length()) && ((reply.charAt(pos) < '0') || (reply.charAt(pos) > '9'))) { pos++; } if (pos >= reply.length()) { throw new IOException(reply + " [could not parse return code]"); } reply = reply.substring(pos); pos = reply.length() - 1; while ((pos >= 0) && ((reply.charAt(pos) < '0') || (reply.charAt(pos) > '9'))) { pos--; } if (pos < 0) { throw new IOException("[could not parse return code: no numbers]"); } reply = reply.substring(0, pos + 1); final StringTokenizer st = new StringTokenizer(reply, ","); if (st.countTokens() != 6) { throw new IOException("[could not parse return code: wrong number of numbers]"); } // set the data host and port final int a = Integer.parseInt(st.nextToken()); final int b = Integer.parseInt(st.nextToken()); final int c = Integer.parseInt(st.nextToken()); final int d = Integer.parseInt(st.nextToken()); final InetAddress datahost = Domains.dnsResolve(a + "." + b + "." + c + "." + d); final int high = Integer.parseInt(st.nextToken()); final int low = Integer.parseInt(st.nextToken()); if (high < 0 || high > 255 || low < 0 || low > 255) { throw new IOException("[could not parse return code: syntax error]"); } final int dataport = (high << 8) + low; this.DataSocketPassive = new Socket(datahost, dataport); applyDataSocketTimeout(); this.DataSocketPassiveMode = true; } /** * closes data connection * * @throws IOException */ private void closeDataSocket() throws IOException { if (isPassive()) { if (this.DataSocketPassive != null) { this.DataSocketPassive.close(); this.DataSocketPassive = null; } } else { if (this.DataSocketActive != null) { this.DataSocketActive.close(); this.DataSocketActive = null; } } } /** * sets the timeout for the socket * * @throws SocketException */ private void applyDataSocketTimeout() throws SocketException { if (isPassive()) { if (this.DataSocketPassive != null) { this.DataSocketPassive.setSoTimeout(this.DataSocketTimeout * 1000); } } else { if (this.DataSocketActive != null) { this.DataSocketActive.setSoTimeout(this.DataSocketTimeout * 1000); } } } private void get(final String fileDest, final String fileName) throws IOException { // store time for statistics final long start = System.currentTimeMillis(); createDataSocket(); // set type of the transfer sendTransferType(transferType); // send command to the control port send("RETR " + fileName); // read status of the command from the control port final String reply = receive(); // get status code final int status = getStatus(reply); // starting data transaction if (status == 1) { Socket data = null; InputStream ClientStream = null; RandomAccessFile outFile = null; int length = 0; try { data = getDataSocket(); ClientStream = data.getInputStream(); // create local file if (fileDest == null) { outFile = new RandomAccessFile(fileName, "rw"); } else { outFile = new RandomAccessFile(fileDest, "rw"); } // write remote file to local file final byte[] block = new byte[blockSize]; int numRead; while ((numRead = ClientStream.read(block)) != -1) { outFile.write(block, 0, numRead); length = length + numRead; } } finally { // shutdown connection if(outFile != null) { outFile.close(); } if(ClientStream != null) { ClientStream.close(); } closeDataSocket(); } // after stream is empty we should get control completion echo /*reply =*/ receive(); // boolean success = !isNotPositiveCompletion(reply); // if (!success) throw new IOException(reply); // write statistics final long stop = System.currentTimeMillis(); log.info(" ---- downloaded " + ((length < 2048) ? length + " bytes" : (length / 1024) + " kbytes") + " in " + (((stop - start) < 2000) ? (stop - start) + " milliseconds" : (((int) ((stop - start) / 100)) / 10) + " seconds")); if (start == stop) { log.warn("start == stop"); } else { log.info(" (" + (length * 1000 / 1024 / (stop - start)) + " kbytes/second)"); } } else { throw new IOException(reply); } } public byte[] get(final String fileName) throws IOException { createDataSocket(); // set type of the transfer sendTransferType(transferType); // send command to the control port send("RETR " + fileName); // read status of the command from the control port final String reply = receive(); // get status code final int status = getStatus(reply); // starting data transaction if (status == 1) { Socket data = null; InputStream ClientStream = null; final ByteArrayOutputStream os = new ByteArrayOutputStream(); int length = 0; try { data = getDataSocket(); ClientStream = data.getInputStream(); // write remote file to local file final byte[] block = new byte[blockSize]; int numRead; while ((numRead = ClientStream.read(block)) != -1) { os.write(block, 0, numRead); length = length + numRead; } } finally { // shutdown connection if (ClientStream != null) { ClientStream.close(); } closeDataSocket(); } // after stream is empty we should get control completion echo /*reply =*/ receive(); // boolean success = !isNotPositiveCompletion(reply); return os.toByteArray(); } throw new IOException(reply); } private void put(final String fileName, final String fileDest) throws IOException { createDataSocket(); // set type of the transfer sendTransferType(transferType); // send command to the control port if (fileDest == null) { send("STOR " + fileName); } else { send("STOR " + fileDest); } // read status of the command from the control port String reply = receive(); // starting data transaction if (getStatus(reply) == 1) { final Socket data = getDataSocket(); final OutputStream ClientStream = data.getOutputStream(); // read from local file final RandomAccessFile inFile = new RandomAccessFile(fileName, "r"); // write remote file to local file final byte[] block = new byte[blockSize]; int numRead; while ((numRead = inFile.read(block)) >= 0) { ClientStream.write(block, 0, numRead); } // shutdown and cleanup inFile.close(); ClientStream.close(); // shutdown remote client connection data.close(); // after stream is empty we should get control completion echo reply = receive(); final boolean success = (getStatus(reply) == 2); if (!success) { throw new IOException(reply); } } else { throw new IOException(reply); } } /** * Login to server * * @param account * @param password * @throws IOException */ public void login(final String account, final String password) throws IOException { unsetLoginData(); // send user name send("USER " + account); String reply = receive(); switch (getStatus(reply)) { case 2: // User logged in, proceed. break; case 5:// 530 Not logged in. case 4: case 1:// in RFC959 an error (page 57, diagram for the Login // sequence) throw new IOException(reply); default: // send password send("PASS " + password); reply = receive(); if (isNotPositiveCompletion(reply)) { throw new IOException(reply); } } setLoginData(account, password, reply); } /** * we are authorized to use the server * * @return */ public boolean isLoggedIn() { return (this.account != null && this.password != null && this.remotegreeting != null); } /** * remember username and password which were used to login * * @param account * @param password * @param reply * remoteGreeting */ private void setLoginData(final String account, final String password, final String reply) { this.account = account; this.password = password; this.remotegreeting = reply; } private void unsetLoginData() { this.account = null; this.password = null; this.remotegreeting = null; } public void sys() throws IOException { // send system command send("SYST"); // check completion final String systemType = receive(); if (isNotPositiveCompletion(systemType)) { throw new IOException(systemType); } // exclude status code from reply this.remotesystem = systemType.substring(4); } private void literal(final String commandLine) throws IOException { // send the complete line send(commandLine); // read reply final String reply = receive(); if (getStatus(reply) == 5) { throw new IOException(reply); } } /** * control socket timeout * * @return */ public int getTimeout() { return ControlSocketTimeout; } /** * after this time the data connection is closed * * @param timeout * in seconds, 0 = infinite */ public void setDataSocketTimeout(final int timeout) { this.DataSocketTimeout = timeout; try { applyDataSocketTimeout(); } catch (final SocketException e) { log.warn("setDataSocketTimeout: " + e.getMessage()); } } public static List<String> dir(final String host, final String remotePath, final String account, final String password, final boolean extended) { try { final FTPClient c = new FTPClient(); c.cmd = new String[] { "open", host }; c.OPEN(); c.cmd = new String[] { "user", account, password }; c.USER(); c.cmd = new String[] { "ls" }; final List<String> v = c.list(remotePath, extended); c.cmd = new String[] { "close" }; c.CLOSE(); c.cmd = new String[] { "exit" }; c.EXIT(); return v; } catch (final java.security.AccessControlException e) { return null; } catch (final IOException e) { return null; } } private static void dir(final String host, final String remotePath, final String account, final String password) { try { final FTPClient c = new FTPClient(); c.exec("open " + host, false); c.exec("user " + account + " " + password, false); c.exec("cd " + remotePath, false); c.exec("ls", true); c.exec("close", false); c.exec("exit", false); } catch (final java.security.AccessControlException e) { } } /** * Asynchronously generate a list of all files on a ftp server using the anonymous account. * @param host host name or address * @param port ftp port * @param user user name * @param pw user password * @param path path on the ftp site * @param depth the maximum depth of the sub folders exploration. * @return a queue asynchronously filled with entryInfo from all files of the ftp server * @throws IOException when a error occurred */ public static BlockingQueue<entryInfo> sitelist(final String host, final int port, final String user, final String pw, final String path, final int depth) throws IOException { final FTPClient ftpClient = new FTPClient(); ftpClient.open(host, port); ftpClient.login(user, pw); final LinkedBlockingQueue<entryInfo> queue = new LinkedBlockingQueue<entryInfo>(); new Thread() { @Override public void run() { try { Thread.currentThread().setName("FTP.sitelist(" + host + ":" + port + ")"); sitelist(ftpClient, path, queue, depth); ftpClient.quit(); } catch (final Exception e) {} finally { queue.add(POISON_entryInfo); } } }.start(); return queue; } /** * Feed the queue with files under a given path on a ftp server using * the anonymous account. When path is a file path, only one entry is added * to the queue. * * @param ftpClient * fptClient initialized with a host and login information * @param path * path on the host * @param queue * the entries queue to feed * @param depth * the maximum depth of the sub folders exploration. * @throws IOException * when a error occurred */ private static void sitelist(final FTPClient ftpClient, String path, final LinkedBlockingQueue<entryInfo> queue, int depth) { List<String> list; try { list = ftpClient.list(path, true); } catch (final IOException e) { /* path might be a file path */ if (!path.endsWith("/")) { entryInfo info = ftpClient.fileInfo(path); if (info != null) { queue.add(info); } else { /* We could not get file information, but this doesn't mean the file does not exist : * we add it anyway to the queue */ info = new entryInfo(); info.name = path; queue.add(info); } } else { ConcurrentLog.logException(e); } return; } if (!path.endsWith("/")) path += "/"; entryInfo info; // first find all files and add them to the crawl list for (final String line : list) { info = parseListData(line); if (info != null && info.type == filetype.file && !info.name.endsWith(".") && !info.name.startsWith(".")) { if (!info.name.startsWith("/")) info.name = path + info.name; queue.add(info); } } // then find all directories and add them recursively if depth is over zero if(depth > 0) { for (final String line : list) { //System.out.println("LIST:" + line); info = parseListData(line); if (info != null && !info.name.endsWith(".") && !info.name.startsWith(".")) { if (info.type == filetype.directory) { sitelist(ftpClient, path + info.name, queue, depth - 1); } else if (info.type == filetype.link) { final int q = info.name.indexOf("->",0); if (q >= 0 && info.name.indexOf("..", q) < 0) { //System.out.println("*** LINK:" + line); info.name = info.name.substring(0, q).trim(); sitelist(ftpClient, path + info.name, queue, depth - 1); } } } } } } public StringBuilder dirhtml(String remotePath) throws IOException { // returns a directory listing using an existing connection if (isFolder(remotePath) && '/' != remotePath.charAt(remotePath.length()-1)) { remotePath += '/'; } final String pwd = pwd(); final List<String> list = list(remotePath, true); if (this.remotesystem == null) try {sys();} catch (final IOException e) {} final String base = "ftp://" + ((this.account.equals(ANONYMOUS)) ? "" : (this.account + ":" + this.password + "@")) + this.host + ((this.port == 21) ? "" : (":" + this.port)) + ((remotePath.length() > 0 && remotePath.charAt(0) == '/') ? "" : pwd + "/") + remotePath; return dirhtml(base, this.remotemessage, this.remotegreeting, this.remotesystem, list, true); } private static StringBuilder dirhtml( final String host, final int port, final String remotePath, final String account, final String password) throws IOException { // opens a new connection and returns a directory listing as html final FTPClient c = new FTPClient(); c.open(host, port); c.login(account, password); c.sys(); final StringBuilder page = c.dirhtml(remotePath); c.quit(); return page; } public static StringBuilder dirhtml( final String base, final String servermessage, final String greeting, final String system, final List<String> list, final boolean metaRobotNoindex) { // this creates the html output from collected strings final StringBuilder page = new StringBuilder(1024); final String title = "Index of " + base; page.append("<!DOCTYPE HTML PUBLIC \"-//W3C//DTD HTML 3.2 Final//EN\">\n"); page.append("<html><head>\n"); page.append(" <title>").append(title).append("</title>\n"); page.append(" <meta name=\"generator\" content=\"YaCy directory listing\">\n"); if (metaRobotNoindex) { page.append(" <meta name=\"robots\" content=\"noindex\">\n"); } page.append(" <base href=\"").append(base).append("\">\n"); page.append("</head><body>\n"); page.append(" <h1>").append(title).append("</h1>\n"); if (servermessage != null && greeting != null) { page.append(" <p><pre>Server \"").append(servermessage).append("\" responded:\n"); page.append(" \n"); page.append(greeting); page.append("\n"); page.append(" </pre></p>\n"); } page.append(" <hr>\n"); page.append(" <pre>\n"); int nameStart, nameEnd; entryInfo info; for (final String line : list) { info = parseListData(line); if (info != null) { // with link nameStart = line.indexOf(info.name); page.append(line.substring(0, nameStart)); page.append("<a href=\"").append(base).append(info.name).append((info.type == filetype.directory) ? "/" : "").append("\">").append(info.name).append("</a>"); nameEnd = nameStart + info.name.length(); if (line.length() > nameEnd) { page.append(line.substring(nameEnd)); } } else if (line.startsWith("http://") || line.startsWith("ftp://") || line.startsWith("smb://") || line.startsWith("file://")) { page.append("<a href=\"").append(line).append("\">").append(line).append("</a>"); } else { // raw page.append(line); } page.append('\n'); } page.append(" </pre>\n"); page.append(" <hr>\n"); if (system != null) page.append(" <pre>System info: \"").append(system).append("\"</pre>\n"); page.append("</body></html>\n"); return page; } public static String put(final String host, File localFile, String remotePath, final String remoteName, final String account, final String password) throws IOException { // returns the log try { final ByteArrayOutputStream bout = new ByteArrayOutputStream(); final PrintStream out = new PrintStream(bout); final ByteArrayOutputStream berr = new ByteArrayOutputStream(); final PrintStream err = new PrintStream(berr); final FTPClient c = new FTPClient(); c.exec("open " + host, false); c.exec("user " + account + " " + password, false); if (remotePath != null) { remotePath = remotePath.replace('\\', '/'); c.exec("cd " + remotePath, false); } c.exec("binary", false); if (localFile.isAbsolute()) { c.exec("lcd \"" + localFile.getParent() + "\"", false); localFile = new File(localFile.getName()); } c.exec("put " + localFile.toString() + ((remoteName.isEmpty()) ? "" : (" " + remoteName)), false); c.exec("close", false); c.exec("exit", false); out.close(); err.close(); final String outLog = bout.toString(); bout.close(); final String errLog = berr.toString(); berr.close(); if (errLog.length() > 0) { throw new IOException("Ftp put failed:\n" + errLog); } return outLog; } catch (final IOException e) { throw e; } } public static void get(final String host, String remoteFile, final File localPath, final String account, final String password) { try { final FTPClient c = new FTPClient(); if (remoteFile.isEmpty()) { remoteFile = "/"; } c.exec("open " + host, false); c.exec("user " + account + " " + password, false); c.exec("lcd " + localPath.getAbsolutePath(), false); c.exec("binary", false); c.exec("get " + remoteFile + " " + localPath.getAbsoluteFile().toString(), false); c.exec("close", false); c.exec("exit", false); } catch (final java.security.AccessControlException e) { } } public static void getAnonymous(final String host, final String remoteFile, final File localPath) { get(host, remoteFile, localPath, ANONYMOUS, "anomic"); } /** * class that puts a file on a ftp-server can be used as a thread */ static class pt implements Runnable { String host; File localFile; String remotePath; String remoteName; String account; String password; public pt(final String h, final File l, final String rp, final String rn, final String a, final String p) { this.host = h; this.localFile = l; this.remotePath = rp; this.remoteName = rn; this.account = a; this.password = p; } @Override public final void run() { try { Thread.currentThread().setName("FTP.pt(" + this.host + ")"); put(this.host, this.localFile, this.remotePath, this.remoteName, this.account, this.password); } catch (final IOException e) { log.warn(e.getMessage(), e); } } } public static Thread putAsync(final String host, final File localFile, final String remotePath, final String remoteName, final String account, final String password) { final Thread t = new Thread(new pt(host, localFile, remotePath, remoteName, account, password), "ftp to " + host); t.start(); return t; // return value can be used to determine status of transfer // with isAlive() or join() } private static void printHelp() { System.out.println("FTPClient help"); System.out.println("----------"); System.out.println(); System.out.println("The following commands are supported"); System.out.println("java net.yacy.cora.protocol.ftp.FTPClient -h -- prints this help"); System.out.println("java net.yacy.cora.protocol.ftp.FTPClient -dir <host>[':'<port>] <path> [<account> <password>]"); System.out.println("java net.yacy.cora.protocol.ftp.FTPClient -htmldir <host> <path>"); System.out.println("java net.yacy.cora.protocol.ftp.FTPClient -get <host>[':'<port>] <remoteFile> <localPath> [<account> <password>]"); System.out.println("java net.yacy.cora.protocol.ftp.FTPClient -put <host>[':'<port>] <localFile> <remotePath> <account> <password>"); System.out.println("java net.yacy.cora.protocol.ftp.FTPClient -sitelist <host> <port> <depth>"); System.out.println(); } public static void main(final String[] args) { try { System.out.println("WELCOME TO THE ANOMIC FTP CLIENT v" + vDATE); System.out.println("Visit http://www.anomic.de and support shareware!"); System.out.println("try -h for command line options"); System.out.println(); if (args.length == 1) { if (args[0].equals("-h")) { printHelp(); } } else if (args.length == 2) { printHelp(); } else if (args.length == 3) { if (args[0].equals("-dir")) { dir(args[1], args[2], ANONYMOUS, "anomic@"); } else if (args[0].equals("-htmldir")) { try { final StringBuilder page = dirhtml(args[1], 21, args[2], ANONYMOUS, "anomic@"); final File file = new File("dirindex.html"); FileOutputStream fos; fos = new FileOutputStream(file); fos.write(UTF8.getBytes(page.toString())); fos.close(); } catch (final FileNotFoundException e) { log.warn(e); } catch (final IOException e) { log.warn(e); } } else { printHelp(); } } else if (args.length == 4) { if (args[0].equals("-get")) { getAnonymous(args[1], args[2], new File(args[3])); } else if (args[0].equals("-sitelist")) { try { final BlockingQueue<entryInfo> q = sitelist(args[1], Integer.parseInt(args[2]), ANONYMOUS, "anomic", "/", Integer.parseInt(args[3])); entryInfo entry; while ((entry = q.take()) != FTPClient.POISON_entryInfo) { System.out.println(entry.toString()); } } catch (final FileNotFoundException e) { log.warn(e); } catch (final IOException e) { log.warn(e); } catch (final InterruptedException e) { log.warn(e); } } else { printHelp(); } } else if (args.length == 5) { if (args[0].equals("-dir")) { dir(args[1], args[2], args[3], args[4]); } else { printHelp(); } } else if (args.length == 6) { if (args[0].equals("-get")) { get(args[1], args[2], new File(args[3]), args[4], args[5]); } else if (args[0].equals("-put")) { try { put(args[1], new File(args[2]), args[3], "", args[4], args[5]); } catch (final IOException e) { log.warn(e.getMessage(), e); } } else { printHelp(); } } else { printHelp(); } } finally { ConcurrentLog.shutdown(); } } }