This is an automated email from the ASF dual-hosted git repository. ggregory pushed a commit to branch master in repository https://gitbox.apache.org/repos/asf/commons-net.git
The following commit(s) were added to refs/heads/master by this push: new f92bfee1 Use for-each loop f92bfee1 is described below commit f92bfee1115c9b9cb6831327335bbd3734c15ce8 Author: Gary Gregory <garydgreg...@gmail.com> AuthorDate: Sat Jul 9 09:04:24 2022 -0400 Use for-each loop --- .../apache/commons/net/ftp/FTPListParseEngine.java | 641 ++++--- .../java/org/apache/commons/net/nntp/Threader.java | 933 +++++----- .../org/apache/commons/net/tftp/TFTPServer.java | 1899 ++++++++++---------- 3 files changed, 1730 insertions(+), 1743 deletions(-) diff --git a/src/main/java/org/apache/commons/net/ftp/FTPListParseEngine.java b/src/main/java/org/apache/commons/net/ftp/FTPListParseEngine.java index 44abe3cb..bb1189d5 100644 --- a/src/main/java/org/apache/commons/net/ftp/FTPListParseEngine.java +++ b/src/main/java/org/apache/commons/net/ftp/FTPListParseEngine.java @@ -1,322 +1,319 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one or more - * contributor license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright ownership. - * The ASF licenses this file to You under the Apache License, Version 2.0 - * (the "License"); you may not use this file except in compliance with - * the License. You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package org.apache.commons.net.ftp; - -import java.io.BufferedReader; -import java.io.IOException; -import java.io.InputStream; -import java.io.InputStreamReader; -import java.util.ArrayList; -import java.util.Iterator; -import java.util.LinkedList; -import java.util.List; -import java.util.ListIterator; - -import org.apache.commons.net.util.Charsets; - - -/** - * This class handles the entire process of parsing a listing of - * file entries from the server. - * <p> - * This object defines a two-part parsing mechanism. - * <p> - * The first part is comprised of reading the raw input into an internal - * list of strings. Every item in this list corresponds to an actual - * file. All extraneous matter emitted by the server will have been - * removed by the end of this phase. This is accomplished in conjunction - * with the FTPFileEntryParser associated with this engine, by calling - * its methods <code>readNextEntry()</code> - which handles the issue of - * what delimits one entry from another, usually but not always a line - * feed and <code>preParse()</code> - which handles removal of - * extraneous matter such as the preliminary lines of a listing, removal - * of duplicates on versioning systems, etc. - * <p> - * The second part is composed of the actual parsing, again in conjunction - * with the particular parser used by this engine. This is controlled - * by an iterator over the internal list of strings. This may be done - * either in block mode, by calling the <code>getNext()</code> and - * <code>getPrevious()</code> methods to provide "paged" output of less - * than the whole list at one time, or by calling the - * <code>getFiles()</code> method to return the entire list. - * <p> - * Examples: - * <p> - * Paged access: - * <pre> - * FTPClient f=FTPClient(); - * f.connect(server); - * f.login(username, password); - * FTPListParseEngine engine = f.initiateListParsing(directory); - * - * while (engine.hasNext()) { - * FTPFile[] files = engine.getNext(25); // "page size" you want - * //do whatever you want with these files, display them, etc. - * //expensive FTPFile objects not created until needed. - * } - * </pre> - * <p> - * For unpaged access, simply use FTPClient.listFiles(). That method - * uses this class transparently. - */ -public class FTPListParseEngine { - /** - * An empty immutable {@code FTPFile} array. - */ - private static final FTPFile[] EMPTY_FTP_FILE_ARRAY = new FTPFile[0]; - private List<String> entries = new LinkedList<>(); - - private ListIterator<String> internalIterator = entries.listIterator(); - private final FTPFileEntryParser parser; - - // Should invalid files (parse failures) be allowed? - private final boolean saveUnparseableEntries; - - public FTPListParseEngine(final FTPFileEntryParser parser) { - this(parser, null); - } - - /** - * Intended for use by FTPClient only - * @since 3.4 - */ - FTPListParseEngine(final FTPFileEntryParser parser, final FTPClientConfig configuration) { - this.parser = parser; - if (configuration != null) { - this.saveUnparseableEntries = configuration.getUnparseableEntries(); - } else { - this.saveUnparseableEntries = false; - } - } - - /** - * Returns an array of FTPFile objects containing the whole list of - * files returned by the server as read by this object's parser. - * - * @return an array of FTPFile objects containing the whole list of - * files returned by the server as read by this object's parser. - * None of the entries will be null - * @throws IOException - not ever thrown, may be removed in a later release - */ - public FTPFile[] getFiles() - throws IOException // TODO remove; not actually thrown - { - return getFiles(FTPFileFilters.NON_NULL); - } - - /** - * Returns an array of FTPFile objects containing the whole list of - * files returned by the server as read by this object's parser. - * The files are filtered before being added to the array. - * - * @param filter FTPFileFilter, must not be <code>null</code>. - * - * @return an array of FTPFile objects containing the whole list of - * files returned by the server as read by this object's parser. - * <p><b> - * NOTE:</b> This array may contain null members if any of the - * individual file listings failed to parse. The caller should - * check each entry for null before referencing it, or use the - * a filter such as {@link FTPFileFilters#NON_NULL} which does not - * allow null entries. - * @since 2.2 - * @throws IOException - not ever thrown, may be removed in a later release - */ - public FTPFile[] getFiles(final FTPFileFilter filter) - throws IOException // TODO remove; not actually thrown - { - final List<FTPFile> tmpResults = new ArrayList<>(); - final Iterator<String> iter = this.entries.iterator(); - while (iter.hasNext()) { - final String entry = iter.next(); - FTPFile temp = this.parser.parseFTPEntry(entry); - if (temp == null && saveUnparseableEntries) { - temp = new FTPFile(entry); - } - if (filter.accept(temp)) { - tmpResults.add(temp); - } - } - return tmpResults.toArray(EMPTY_FTP_FILE_ARRAY); - - } - - /** - * Returns an array of at most <code>quantityRequested</code> FTPFile - * objects starting at this object's internal iterator's current position. - * If fewer than <code>quantityRequested</code> such - * elements are available, the returned array will have a length equal - * to the number of entries at and after after the current position. - * If no such entries are found, this array will have a length of 0. - * - * After this method is called this object's internal iterator is advanced - * by a number of positions equal to the size of the array returned. - * - * @param quantityRequested - * the maximum number of entries we want to get. - * - * @return an array of at most <code>quantityRequested</code> FTPFile - * objects starting at the current position of this iterator within its - * list and at least the number of elements which exist in the list at - * and after its current position. - * <p><b> - * NOTE:</b> This array may contain null members if any of the - * individual file listings failed to parse. The caller should - * check each entry for null before referencing it. - */ - public FTPFile[] getNext(final int quantityRequested) { - final List<FTPFile> tmpResults = new LinkedList<>(); - int count = quantityRequested; - while (count > 0 && this.internalIterator.hasNext()) { - final String entry = this.internalIterator.next(); - FTPFile temp = this.parser.parseFTPEntry(entry); - if (temp == null && saveUnparseableEntries) { - temp = new FTPFile(entry); - } - tmpResults.add(temp); - count--; - } - return tmpResults.toArray(EMPTY_FTP_FILE_ARRAY); - - } - - /** - * Returns an array of at most <code>quantityRequested</code> FTPFile - * objects starting at this object's internal iterator's current position, - * and working back toward the beginning. - * - * If fewer than <code>quantityRequested</code> such - * elements are available, the returned array will have a length equal - * to the number of entries at and after after the current position. - * If no such entries are found, this array will have a length of 0. - * - * After this method is called this object's internal iterator is moved - * back by a number of positions equal to the size of the array returned. - * - * @param quantityRequested - * the maximum number of entries we want to get. - * - * @return an array of at most <code>quantityRequested</code> FTPFile - * objects starting at the current position of this iterator within its - * list and at least the number of elements which exist in the list at - * and after its current position. This array will be in the same order - * as the underlying list (not reversed). - * <p><b> - * NOTE:</b> This array may contain null members if any of the - * individual file listings failed to parse. The caller should - * check each entry for null before referencing it. - */ - public FTPFile[] getPrevious(final int quantityRequested) { - final List<FTPFile> tmpResults = new LinkedList<>(); - int count = quantityRequested; - while (count > 0 && this.internalIterator.hasPrevious()) { - final String entry = this.internalIterator.previous(); - FTPFile temp = this.parser.parseFTPEntry(entry); - if (temp == null && saveUnparseableEntries) { - temp = new FTPFile(entry); - } - tmpResults.add(0,temp); - count--; - } - return tmpResults.toArray(EMPTY_FTP_FILE_ARRAY); - } - - /** - * convenience method to allow clients to know whether this object's - * internal iterator's current position is at the end of the list. - * - * @return true if internal iterator is not at end of list, false - * otherwise. - */ - public boolean hasNext() { - return internalIterator.hasNext(); - } - - /** - * convenience method to allow clients to know whether this object's - * internal iterator's current position is at the beginning of the list. - * - * @return true if internal iterator is not at beginning of list, false - * otherwise. - */ - public boolean hasPrevious() { - return internalIterator.hasPrevious(); - } - - /** - * Internal method for reading (and closing) the input into the <code>entries</code> list. After this method has - * completed, <code>entries</code> will contain a collection of entries (as defined by - * <code>FTPFileEntryParser.readNextEntry()</code>), but this may contain various non-entry preliminary lines from - * the server output, duplicates, and other data that will not be part of the final listing. - * - * @param inputStream The socket stream on which the input will be read. - * @param charsetName The encoding to use. - * - * @throws IOException thrown on any failure to read the stream - */ - private void read(final InputStream inputStream, final String charsetName) throws IOException { - try (final BufferedReader reader = new BufferedReader( - new InputStreamReader(inputStream, Charsets.toCharset(charsetName)))) { - - String line = this.parser.readNextEntry(reader); - - while (line != null) { - this.entries.add(line); - line = this.parser.readNextEntry(reader); - } - } - } - - /** - * Do not use. - * @param inputStream the stream from which to read - * @throws IOException on error - * @deprecated use {@link #readServerList(InputStream, String)} instead - */ - @Deprecated - public void readServerList(final InputStream inputStream) throws IOException { - readServerList(inputStream, null); - } - - /** - * Reads (and closes) the initial reading and preparsing of the list returned by the server. After this method has - * completed, this object will contain a list of unparsed entries (Strings) each referring to a unique file on the - * server. - * - * @param inputStream input stream provided by the server socket. - * @param charsetName the encoding to be used for reading the stream - * - * @throws IOException thrown on any failure to read from the sever. - */ - public void readServerList(final InputStream inputStream, final String charsetName) throws IOException { - this.entries = new LinkedList<>(); - read(inputStream, charsetName); - this.parser.preParse(this.entries); - resetIterator(); - } - - // DEPRECATED METHODS - for API compatibility only - DO NOT USE - - /** - * resets this object's internal iterator to the beginning of the list. - */ - public void resetIterator() { - this.internalIterator = this.entries.listIterator(); - } - -} +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.commons.net.ftp; + +import java.io.BufferedReader; +import java.io.IOException; +import java.io.InputStream; +import java.io.InputStreamReader; +import java.util.ArrayList; +import java.util.LinkedList; +import java.util.List; +import java.util.ListIterator; + +import org.apache.commons.net.util.Charsets; + + +/** + * This class handles the entire process of parsing a listing of + * file entries from the server. + * <p> + * This object defines a two-part parsing mechanism. + * <p> + * The first part is comprised of reading the raw input into an internal + * list of strings. Every item in this list corresponds to an actual + * file. All extraneous matter emitted by the server will have been + * removed by the end of this phase. This is accomplished in conjunction + * with the FTPFileEntryParser associated with this engine, by calling + * its methods <code>readNextEntry()</code> - which handles the issue of + * what delimits one entry from another, usually but not always a line + * feed and <code>preParse()</code> - which handles removal of + * extraneous matter such as the preliminary lines of a listing, removal + * of duplicates on versioning systems, etc. + * <p> + * The second part is composed of the actual parsing, again in conjunction + * with the particular parser used by this engine. This is controlled + * by an iterator over the internal list of strings. This may be done + * either in block mode, by calling the <code>getNext()</code> and + * <code>getPrevious()</code> methods to provide "paged" output of less + * than the whole list at one time, or by calling the + * <code>getFiles()</code> method to return the entire list. + * <p> + * Examples: + * <p> + * Paged access: + * <pre> + * FTPClient f=FTPClient(); + * f.connect(server); + * f.login(username, password); + * FTPListParseEngine engine = f.initiateListParsing(directory); + * + * while (engine.hasNext()) { + * FTPFile[] files = engine.getNext(25); // "page size" you want + * //do whatever you want with these files, display them, etc. + * //expensive FTPFile objects not created until needed. + * } + * </pre> + * <p> + * For unpaged access, simply use FTPClient.listFiles(). That method + * uses this class transparently. + */ +public class FTPListParseEngine { + /** + * An empty immutable {@code FTPFile} array. + */ + private static final FTPFile[] EMPTY_FTP_FILE_ARRAY = new FTPFile[0]; + private List<String> entries = new LinkedList<>(); + + private ListIterator<String> internalIterator = entries.listIterator(); + private final FTPFileEntryParser parser; + + // Should invalid files (parse failures) be allowed? + private final boolean saveUnparseableEntries; + + public FTPListParseEngine(final FTPFileEntryParser parser) { + this(parser, null); + } + + /** + * Intended for use by FTPClient only + * @since 3.4 + */ + FTPListParseEngine(final FTPFileEntryParser parser, final FTPClientConfig configuration) { + this.parser = parser; + if (configuration != null) { + this.saveUnparseableEntries = configuration.getUnparseableEntries(); + } else { + this.saveUnparseableEntries = false; + } + } + + /** + * Returns an array of FTPFile objects containing the whole list of + * files returned by the server as read by this object's parser. + * + * @return an array of FTPFile objects containing the whole list of + * files returned by the server as read by this object's parser. + * None of the entries will be null + * @throws IOException - not ever thrown, may be removed in a later release + */ + public FTPFile[] getFiles() + throws IOException // TODO remove; not actually thrown + { + return getFiles(FTPFileFilters.NON_NULL); + } + + /** + * Returns an array of FTPFile objects containing the whole list of + * files returned by the server as read by this object's parser. + * The files are filtered before being added to the array. + * + * @param filter FTPFileFilter, must not be <code>null</code>. + * + * @return an array of FTPFile objects containing the whole list of + * files returned by the server as read by this object's parser. + * <p><b> + * NOTE:</b> This array may contain null members if any of the + * individual file listings failed to parse. The caller should + * check each entry for null before referencing it, or use the + * a filter such as {@link FTPFileFilters#NON_NULL} which does not + * allow null entries. + * @since 2.2 + * @throws IOException - not ever thrown, may be removed in a later release + */ + public FTPFile[] getFiles(final FTPFileFilter filter) + throws IOException // TODO remove; not actually thrown + { + final List<FTPFile> tmpResults = new ArrayList<>(); + for (final String entry : entries) { + FTPFile temp = this.parser.parseFTPEntry(entry); + if (temp == null && saveUnparseableEntries) { + temp = new FTPFile(entry); + } + if (filter.accept(temp)) { + tmpResults.add(temp); + } + } + return tmpResults.toArray(EMPTY_FTP_FILE_ARRAY); + + } + + /** + * Returns an array of at most <code>quantityRequested</code> FTPFile + * objects starting at this object's internal iterator's current position. + * If fewer than <code>quantityRequested</code> such + * elements are available, the returned array will have a length equal + * to the number of entries at and after after the current position. + * If no such entries are found, this array will have a length of 0. + * + * After this method is called this object's internal iterator is advanced + * by a number of positions equal to the size of the array returned. + * + * @param quantityRequested + * the maximum number of entries we want to get. + * + * @return an array of at most <code>quantityRequested</code> FTPFile + * objects starting at the current position of this iterator within its + * list and at least the number of elements which exist in the list at + * and after its current position. + * <p><b> + * NOTE:</b> This array may contain null members if any of the + * individual file listings failed to parse. The caller should + * check each entry for null before referencing it. + */ + public FTPFile[] getNext(final int quantityRequested) { + final List<FTPFile> tmpResults = new LinkedList<>(); + int count = quantityRequested; + while (count > 0 && this.internalIterator.hasNext()) { + final String entry = this.internalIterator.next(); + FTPFile temp = this.parser.parseFTPEntry(entry); + if (temp == null && saveUnparseableEntries) { + temp = new FTPFile(entry); + } + tmpResults.add(temp); + count--; + } + return tmpResults.toArray(EMPTY_FTP_FILE_ARRAY); + + } + + /** + * Returns an array of at most <code>quantityRequested</code> FTPFile + * objects starting at this object's internal iterator's current position, + * and working back toward the beginning. + * + * If fewer than <code>quantityRequested</code> such + * elements are available, the returned array will have a length equal + * to the number of entries at and after after the current position. + * If no such entries are found, this array will have a length of 0. + * + * After this method is called this object's internal iterator is moved + * back by a number of positions equal to the size of the array returned. + * + * @param quantityRequested + * the maximum number of entries we want to get. + * + * @return an array of at most <code>quantityRequested</code> FTPFile + * objects starting at the current position of this iterator within its + * list and at least the number of elements which exist in the list at + * and after its current position. This array will be in the same order + * as the underlying list (not reversed). + * <p><b> + * NOTE:</b> This array may contain null members if any of the + * individual file listings failed to parse. The caller should + * check each entry for null before referencing it. + */ + public FTPFile[] getPrevious(final int quantityRequested) { + final List<FTPFile> tmpResults = new LinkedList<>(); + int count = quantityRequested; + while (count > 0 && this.internalIterator.hasPrevious()) { + final String entry = this.internalIterator.previous(); + FTPFile temp = this.parser.parseFTPEntry(entry); + if (temp == null && saveUnparseableEntries) { + temp = new FTPFile(entry); + } + tmpResults.add(0,temp); + count--; + } + return tmpResults.toArray(EMPTY_FTP_FILE_ARRAY); + } + + /** + * convenience method to allow clients to know whether this object's + * internal iterator's current position is at the end of the list. + * + * @return true if internal iterator is not at end of list, false + * otherwise. + */ + public boolean hasNext() { + return internalIterator.hasNext(); + } + + /** + * convenience method to allow clients to know whether this object's + * internal iterator's current position is at the beginning of the list. + * + * @return true if internal iterator is not at beginning of list, false + * otherwise. + */ + public boolean hasPrevious() { + return internalIterator.hasPrevious(); + } + + /** + * Internal method for reading (and closing) the input into the <code>entries</code> list. After this method has + * completed, <code>entries</code> will contain a collection of entries (as defined by + * <code>FTPFileEntryParser.readNextEntry()</code>), but this may contain various non-entry preliminary lines from + * the server output, duplicates, and other data that will not be part of the final listing. + * + * @param inputStream The socket stream on which the input will be read. + * @param charsetName The encoding to use. + * + * @throws IOException thrown on any failure to read the stream + */ + private void read(final InputStream inputStream, final String charsetName) throws IOException { + try (final BufferedReader reader = new BufferedReader( + new InputStreamReader(inputStream, Charsets.toCharset(charsetName)))) { + + String line = this.parser.readNextEntry(reader); + + while (line != null) { + this.entries.add(line); + line = this.parser.readNextEntry(reader); + } + } + } + + /** + * Do not use. + * @param inputStream the stream from which to read + * @throws IOException on error + * @deprecated use {@link #readServerList(InputStream, String)} instead + */ + @Deprecated + public void readServerList(final InputStream inputStream) throws IOException { + readServerList(inputStream, null); + } + + /** + * Reads (and closes) the initial reading and preparsing of the list returned by the server. After this method has + * completed, this object will contain a list of unparsed entries (Strings) each referring to a unique file on the + * server. + * + * @param inputStream input stream provided by the server socket. + * @param charsetName the encoding to be used for reading the stream + * + * @throws IOException thrown on any failure to read from the sever. + */ + public void readServerList(final InputStream inputStream, final String charsetName) throws IOException { + this.entries = new LinkedList<>(); + read(inputStream, charsetName); + this.parser.preParse(this.entries); + resetIterator(); + } + + // DEPRECATED METHODS - for API compatibility only - DO NOT USE + + /** + * resets this object's internal iterator to the beginning of the list. + */ + public void resetIterator() { + this.internalIterator = this.entries.listIterator(); + } + +} diff --git a/src/main/java/org/apache/commons/net/nntp/Threader.java b/src/main/java/org/apache/commons/net/nntp/Threader.java index bbf00370..481fcde0 100644 --- a/src/main/java/org/apache/commons/net/nntp/Threader.java +++ b/src/main/java/org/apache/commons/net/nntp/Threader.java @@ -1,469 +1,464 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one or more - * contributor license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright ownership. - * The ASF licenses this file to You under the Apache License, Version 2.0 - * (the "License"); you may not use this file except in compliance with - * the License. You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - - -package org.apache.commons.net.nntp; - -/** - * This is an implementation of a message threading algorithm, as originally devised by Zamie Zawinski. - * See <a href="http://www.jwz.org/doc/threading.html">http://www.jwz.org/doc/threading.html</a> for details. - * For his Java implementation, see - * <a href="http://lxr.mozilla.org/mozilla/source/grendel/sources/grendel/view/Threader.java"> - * http://lxr.mozilla.org/mozilla/source/grendel/sources/grendel/view/Threader.java</a> - */ - -import java.util.Arrays; -import java.util.HashMap; -import java.util.Iterator; -import java.util.List; -import java.util.Map; - -public class Threader { - - /** - * - * @param threadable - * @param idTable - */ - private void buildContainer(final Threadable threadable, final HashMap<String,ThreadContainer> idTable) { - String id = threadable.messageThreadId(); - ThreadContainer container = idTable.get(id); - int bogusIdCount = 0; - - // A ThreadContainer exists for this id already. This should be a forward reference, but may - // be a duplicate id, in which case we will need to generate a bogus placeholder id - if (container != null) { - if (container.threadable != null) { // oops! duplicate ids... - bogusIdCount++ ; // Avoid dead local store warning - id = "<Bogus-id:" + (bogusIdCount) + ">"; - container = null; - } else { - // The container just contained a forward reference to this message, so let's - // fill in the threadable field of the container with this message - container.threadable = threadable; - } - } - - // No container exists for that message Id. Create one and insert it into the hash table. - if (container == null) { - container = new ThreadContainer(); - container.threadable = threadable; - idTable.put(id, container); - } - - // Iterate through all of the references and create ThreadContainers for any references that - // don't have them. - ThreadContainer parentRef = null; - { - final String[] references = threadable.messageThreadReferences(); - for (final String refString : references) - { - ThreadContainer ref = idTable.get(refString); - - // if this id doesnt have a container, create one - if (ref == null) { - ref = new ThreadContainer(); - idTable.put(refString, ref); - } - - // Link references together in the order they appear in the References: header, - // IF they dont have a have a parent already && - // IF it will not cause a circular reference - if ((parentRef != null) - && (ref.parent == null) - && (parentRef != ref) - && !(ref.findChild(parentRef))) { - // Link ref into the parent's child list - ref.parent = parentRef; - ref.next = parentRef.child; - parentRef.child = ref; - } - parentRef = ref; - } - } - - // parentRef is now set to the container of the last element in the references field. make that - // be the parent of this container, unless doing so causes a circular reference - if (parentRef != null - && (parentRef == container || container.findChild(parentRef))) - { - parentRef = null; - } - - // if it has a parent already, its because we saw this message in a References: field, and presumed - // a parent based on the other entries in that field. Now that we have the actual message, we can - // throw away the old parent and use this new one - if (container.parent != null) { - ThreadContainer rest, prev; - - for (prev = null, rest = container.parent.child; - rest != null; - prev = rest, rest = rest.next) { - if (rest == container) { - break; - } - } - - if (rest == null) { - throw new RuntimeException( - "Didnt find " - + container - + " in parent" - + container.parent); - } - - // Unlink this container from the parent's child list - if (prev == null) { - container.parent.child = container.next; - } else { - prev.next = container.next; - } - - container.next = null; - container.parent = null; - } - - // If we have a parent, link container into the parents child list - if (parentRef != null) { - container.parent = parentRef; - container.next = parentRef.child; - parentRef.child = container; - } - } - - /** - * Find the root set of all existing ThreadContainers - * @param idTable - * @return root the ThreadContainer representing the root node - */ - private ThreadContainer findRootSet(final HashMap<String, ThreadContainer> idTable) { - final ThreadContainer root = new ThreadContainer(); - final Iterator<Map.Entry<String, ThreadContainer>> iter = idTable.entrySet().iterator(); - - while (iter.hasNext()) { - final Map.Entry<String, ThreadContainer> entry = iter.next(); - final ThreadContainer c = entry.getValue(); - if (c.parent == null) { - if (c.next != null) { - throw new RuntimeException( - "c.next is " + c.next.toString()); - } - c.next = root.child; - root.child = c; - } - } - return root; - } - - /** - * If any two members of the root set have the same subject, merge them. - * This is to attempt to accomodate messages without References: headers. - * @param root - */ - private void gatherSubjects(final ThreadContainer root) { - - int count = 0; - - for (ThreadContainer c = root.child; c != null; c = c.next) { - count++; - } - - // TODO verify this will avoid rehashing - HashMap<String, ThreadContainer> subjectTable = new HashMap<>((int) (count * 1.2), (float) 0.9); - count = 0; - - for (ThreadContainer c = root.child; c != null; c = c.next) { - Threadable threadable = c.threadable; - - // No threadable? If so, it is a dummy node in the root set. - // Only root set members may be dummies, and they alway have at least 2 kids - // Take the first kid as representative of the subject - if (threadable == null) { - threadable = c.child.threadable; - } - - final String subj = threadable.simplifiedSubject(); - - if (subj == null || subj.isEmpty()) { - continue; - } - - final ThreadContainer old = subjectTable.get(subj); - - // Add this container to the table iff: - // - There exists no container with this subject - // - or this is a dummy container and the old one is not - the dummy one is - // more interesting as a root, so put it in the table instead - // - The container in the table has a "Re:" version of this subject, and - // this container has a non-"Re:" version of this subject. The non-"Re:" version - // is the more interesting of the two. - if (old == null - || (c.threadable == null && old.threadable != null) - || (old.threadable != null - && old.threadable.subjectIsReply() - && c.threadable != null - && !c.threadable.subjectIsReply())) { - subjectTable.put(subj, c); - count++; - } - } - - // If the table is empty, we're done - if (count == 0) { - return; - } - - // subjectTable is now populated with one entry for each subject which occurs in the - // root set. Iterate over the root set, and gather together the difference. - ThreadContainer prev, c, rest; - for (prev = null, c = root.child, rest = c.next; - c != null; - prev = c, c = rest, rest = (rest == null ? null : rest.next)) { - Threadable threadable = c.threadable; - - // is it a dummy node? - if (threadable == null) { - threadable = c.child.threadable; - } - - final String subj = threadable.simplifiedSubject(); - - // Dont thread together all subjectless messages - if (subj == null || subj.isEmpty()) { - continue; - } - - final ThreadContainer old = subjectTable.get(subj); - - if (old == c) { // That's us - continue; - } - - // We have now found another container in the root set with the same subject - // Remove the "second" message from the root set - if (prev == null) { - root.child = c.next; - } else { - prev.next = c.next; - } - c.next = null; - - if (old.threadable == null && c.threadable == null) { - // both dummies - merge them - ThreadContainer tail; - for (tail = old.child; - tail != null && tail.next != null; - tail = tail.next) { - // do nothing - } - - if (tail != null) { // protect against possible NPE - tail.next = c.child; - } - - for (tail = c.child; tail != null; tail = tail.next) { - tail.parent = old; - } - - c.child = null; - } else if ( - old.threadable == null - || (c.threadable != null - && c.threadable.subjectIsReply() - && !old.threadable.subjectIsReply())) { - // Else if old is empty, or c has "Re:" and old does not ==> make this message a child of old - c.parent = old; - c.next = old.child; - old.child = c; - } else { - // else make the old and new messages be children of a new dummy container. - // We create a new container object for old.msg and empty the old container - final ThreadContainer newc = new ThreadContainer(); - newc.threadable = old.threadable; - newc.child = old.child; - - for (ThreadContainer tail = newc.child; - tail != null; - tail = tail.next) - { - tail.parent = newc; - } - - old.threadable = null; - old.child = null; - - c.parent = old; - newc.parent = old; - - // Old is now a dummy- give it 2 kids , c and newc - old.child = c; - c.next = newc; - } - // We've done a merge, so keep the same prev - c = prev; - } - - subjectTable.clear(); - subjectTable = null; - - } - - /** - * Delete any empty or dummy ThreadContainers - * @param parent - */ - private void pruneEmptyContainers(final ThreadContainer parent) { - ThreadContainer container, prev, next; - for (prev = null, container = parent.child, next = container.next; - container != null; - prev = container, - container = next, - next = (container == null ? null : container.next)) { - - // Is it empty and without any children? If so,delete it - if (container.threadable == null && container.child == null) { - if (prev == null) { - parent.child = container.next; - } else { - prev.next = container.next; - } - - // Set container to prev so that prev keeps its same value the next time through the loop - container = prev; - } - - // Else if empty, with kids, and (not at root or only one kid) - else if ( - container.threadable == null - && container.child != null - && (container.parent != null - || container.child.next == null)) { - // We have an invalid/expired message with kids. Promote the kids to this level. - ThreadContainer tail; - final ThreadContainer kids = container.child; - - // Remove this container and replace with 'kids'. - if (prev == null) { - parent.child = kids; - } else { - prev.next = kids; - } - - // Make each child's parent be this level's parent -> i.e. promote the children. - // Make the last child's next point to this container's next - // i.e. splice kids into the list in place of container - for (tail = kids; tail.next != null; tail = tail.next) { - tail.parent = container.parent; - } - - tail.parent = container.parent; - tail.next = container.next; - - // next currently points to the item after the inserted items in the chain - reset that so we process the newly - // promoted items next time round - next = kids; - - // Set container to prev so that prev keeps its same value the next time through the loop - container = prev; - } else if (container.child != null) { - // A real message , with kids - // Iterate over the children - pruneEmptyContainers(container); - } - } - } - - /** - * The client passes in a list of Iterable objects, and - * the Threader constructs a connected 'graph' of messages - * @param messages iterable of messages to thread, must not be empty - * @return null if messages == null or root.child == null or messages list is empty - * @since 3.0 - */ - public Threadable thread(final Iterable<? extends Threadable> messages) { - if (messages == null) { - return null; - } - - HashMap<String,ThreadContainer> idTable = new HashMap<>(); - - // walk through each Threadable element - for (final Threadable t : messages) { - if (!t.isDummy()) { - buildContainer(t, idTable); - } - } - - if (idTable.isEmpty()) { - return null; - } - - final ThreadContainer root = findRootSet(idTable); - idTable.clear(); - idTable = null; - - pruneEmptyContainers(root); - - root.reverseChildren(); - gatherSubjects(root); - - if (root.next != null) { - throw new RuntimeException("root node has a next:" + root); - } - - for (ThreadContainer r = root.child; r != null; r = r.next) { - if (r.threadable == null) { - r.threadable = r.child.threadable.makeDummy(); - } - } - - final Threadable result = (root.child == null ? null : root.child.threadable); - root.flush(); - - return result; - } - - /** - * The client passes in a list of Threadable objects, and - * the Threader constructs a connected 'graph' of messages - * @param messages list of messages to thread, must not be empty - * @return null if messages == null or root.child == null or messages list is empty - * @since 2.2 - */ - public Threadable thread(final List<? extends Threadable> messages) { - return thread((Iterable<? extends Threadable>)messages); - } - - - // DEPRECATED METHODS - for API compatibility only - DO NOT USE - - /** - * The client passes in an array of Threadable objects, and - * the Threader constructs a connected 'graph' of messages - * @param messages array of messages to thread, must not be empty - * @return null if messages == null or root.child == null or messages array is empty - * @deprecated (2.2) prefer {@link #thread(List)} - */ - @Deprecated - public Threadable thread(final Threadable[] messages) { - if (messages == null) { - return null; - } - return thread(Arrays.asList(messages)); - } - -} +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + + +package org.apache.commons.net.nntp; + +/** + * This is an implementation of a message threading algorithm, as originally devised by Zamie Zawinski. + * See <a href="http://www.jwz.org/doc/threading.html">http://www.jwz.org/doc/threading.html</a> for details. + * For his Java implementation, see + * <a href="http://lxr.mozilla.org/mozilla/source/grendel/sources/grendel/view/Threader.java"> + * http://lxr.mozilla.org/mozilla/source/grendel/sources/grendel/view/Threader.java</a> + */ + +import java.util.Arrays; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +public class Threader { + + /** + * + * @param threadable + * @param idTable + */ + private void buildContainer(final Threadable threadable, final HashMap<String,ThreadContainer> idTable) { + String id = threadable.messageThreadId(); + ThreadContainer container = idTable.get(id); + int bogusIdCount = 0; + + // A ThreadContainer exists for this id already. This should be a forward reference, but may + // be a duplicate id, in which case we will need to generate a bogus placeholder id + if (container != null) { + if (container.threadable != null) { // oops! duplicate ids... + bogusIdCount++ ; // Avoid dead local store warning + id = "<Bogus-id:" + (bogusIdCount) + ">"; + container = null; + } else { + // The container just contained a forward reference to this message, so let's + // fill in the threadable field of the container with this message + container.threadable = threadable; + } + } + + // No container exists for that message Id. Create one and insert it into the hash table. + if (container == null) { + container = new ThreadContainer(); + container.threadable = threadable; + idTable.put(id, container); + } + + // Iterate through all of the references and create ThreadContainers for any references that + // don't have them. + ThreadContainer parentRef = null; + { + final String[] references = threadable.messageThreadReferences(); + for (final String refString : references) + { + ThreadContainer ref = idTable.get(refString); + + // if this id doesnt have a container, create one + if (ref == null) { + ref = new ThreadContainer(); + idTable.put(refString, ref); + } + + // Link references together in the order they appear in the References: header, + // IF they dont have a have a parent already && + // IF it will not cause a circular reference + if ((parentRef != null) + && (ref.parent == null) + && (parentRef != ref) + && !(ref.findChild(parentRef))) { + // Link ref into the parent's child list + ref.parent = parentRef; + ref.next = parentRef.child; + parentRef.child = ref; + } + parentRef = ref; + } + } + + // parentRef is now set to the container of the last element in the references field. make that + // be the parent of this container, unless doing so causes a circular reference + if (parentRef != null + && (parentRef == container || container.findChild(parentRef))) + { + parentRef = null; + } + + // if it has a parent already, its because we saw this message in a References: field, and presumed + // a parent based on the other entries in that field. Now that we have the actual message, we can + // throw away the old parent and use this new one + if (container.parent != null) { + ThreadContainer rest, prev; + + for (prev = null, rest = container.parent.child; + rest != null; + prev = rest, rest = rest.next) { + if (rest == container) { + break; + } + } + + if (rest == null) { + throw new RuntimeException( + "Didnt find " + + container + + " in parent" + + container.parent); + } + + // Unlink this container from the parent's child list + if (prev == null) { + container.parent.child = container.next; + } else { + prev.next = container.next; + } + + container.next = null; + container.parent = null; + } + + // If we have a parent, link container into the parents child list + if (parentRef != null) { + container.parent = parentRef; + container.next = parentRef.child; + parentRef.child = container; + } + } + + /** + * Find the root set of all existing ThreadContainers + * @param idTable + * @return root the ThreadContainer representing the root node + */ + private ThreadContainer findRootSet(final HashMap<String, ThreadContainer> idTable) { + final ThreadContainer root = new ThreadContainer(); + for (final Map.Entry<String, ThreadContainer> entry : idTable.entrySet()) { + final ThreadContainer c = entry.getValue(); + if (c.parent == null) { + if (c.next != null) { + throw new RuntimeException("c.next is " + c.next.toString()); + } + c.next = root.child; + root.child = c; + } + } + return root; + } + + /** + * If any two members of the root set have the same subject, merge them. + * This is to attempt to accomodate messages without References: headers. + * @param root + */ + private void gatherSubjects(final ThreadContainer root) { + + int count = 0; + + for (ThreadContainer c = root.child; c != null; c = c.next) { + count++; + } + + // TODO verify this will avoid rehashing + HashMap<String, ThreadContainer> subjectTable = new HashMap<>((int) (count * 1.2), (float) 0.9); + count = 0; + + for (ThreadContainer c = root.child; c != null; c = c.next) { + Threadable threadable = c.threadable; + + // No threadable? If so, it is a dummy node in the root set. + // Only root set members may be dummies, and they alway have at least 2 kids + // Take the first kid as representative of the subject + if (threadable == null) { + threadable = c.child.threadable; + } + + final String subj = threadable.simplifiedSubject(); + + if (subj == null || subj.isEmpty()) { + continue; + } + + final ThreadContainer old = subjectTable.get(subj); + + // Add this container to the table iff: + // - There exists no container with this subject + // - or this is a dummy container and the old one is not - the dummy one is + // more interesting as a root, so put it in the table instead + // - The container in the table has a "Re:" version of this subject, and + // this container has a non-"Re:" version of this subject. The non-"Re:" version + // is the more interesting of the two. + if (old == null + || (c.threadable == null && old.threadable != null) + || (old.threadable != null + && old.threadable.subjectIsReply() + && c.threadable != null + && !c.threadable.subjectIsReply())) { + subjectTable.put(subj, c); + count++; + } + } + + // If the table is empty, we're done + if (count == 0) { + return; + } + + // subjectTable is now populated with one entry for each subject which occurs in the + // root set. Iterate over the root set, and gather together the difference. + ThreadContainer prev, c, rest; + for (prev = null, c = root.child, rest = c.next; + c != null; + prev = c, c = rest, rest = (rest == null ? null : rest.next)) { + Threadable threadable = c.threadable; + + // is it a dummy node? + if (threadable == null) { + threadable = c.child.threadable; + } + + final String subj = threadable.simplifiedSubject(); + + // Dont thread together all subjectless messages + if (subj == null || subj.isEmpty()) { + continue; + } + + final ThreadContainer old = subjectTable.get(subj); + + if (old == c) { // That's us + continue; + } + + // We have now found another container in the root set with the same subject + // Remove the "second" message from the root set + if (prev == null) { + root.child = c.next; + } else { + prev.next = c.next; + } + c.next = null; + + if (old.threadable == null && c.threadable == null) { + // both dummies - merge them + ThreadContainer tail; + for (tail = old.child; + tail != null && tail.next != null; + tail = tail.next) { + // do nothing + } + + if (tail != null) { // protect against possible NPE + tail.next = c.child; + } + + for (tail = c.child; tail != null; tail = tail.next) { + tail.parent = old; + } + + c.child = null; + } else if ( + old.threadable == null + || (c.threadable != null + && c.threadable.subjectIsReply() + && !old.threadable.subjectIsReply())) { + // Else if old is empty, or c has "Re:" and old does not ==> make this message a child of old + c.parent = old; + c.next = old.child; + old.child = c; + } else { + // else make the old and new messages be children of a new dummy container. + // We create a new container object for old.msg and empty the old container + final ThreadContainer newc = new ThreadContainer(); + newc.threadable = old.threadable; + newc.child = old.child; + + for (ThreadContainer tail = newc.child; + tail != null; + tail = tail.next) + { + tail.parent = newc; + } + + old.threadable = null; + old.child = null; + + c.parent = old; + newc.parent = old; + + // Old is now a dummy- give it 2 kids , c and newc + old.child = c; + c.next = newc; + } + // We've done a merge, so keep the same prev + c = prev; + } + + subjectTable.clear(); + subjectTable = null; + + } + + /** + * Delete any empty or dummy ThreadContainers + * @param parent + */ + private void pruneEmptyContainers(final ThreadContainer parent) { + ThreadContainer container, prev, next; + for (prev = null, container = parent.child, next = container.next; + container != null; + prev = container, + container = next, + next = (container == null ? null : container.next)) { + + // Is it empty and without any children? If so,delete it + if (container.threadable == null && container.child == null) { + if (prev == null) { + parent.child = container.next; + } else { + prev.next = container.next; + } + + // Set container to prev so that prev keeps its same value the next time through the loop + container = prev; + } + + // Else if empty, with kids, and (not at root or only one kid) + else if ( + container.threadable == null + && container.child != null + && (container.parent != null + || container.child.next == null)) { + // We have an invalid/expired message with kids. Promote the kids to this level. + ThreadContainer tail; + final ThreadContainer kids = container.child; + + // Remove this container and replace with 'kids'. + if (prev == null) { + parent.child = kids; + } else { + prev.next = kids; + } + + // Make each child's parent be this level's parent -> i.e. promote the children. + // Make the last child's next point to this container's next + // i.e. splice kids into the list in place of container + for (tail = kids; tail.next != null; tail = tail.next) { + tail.parent = container.parent; + } + + tail.parent = container.parent; + tail.next = container.next; + + // next currently points to the item after the inserted items in the chain - reset that so we process the newly + // promoted items next time round + next = kids; + + // Set container to prev so that prev keeps its same value the next time through the loop + container = prev; + } else if (container.child != null) { + // A real message , with kids + // Iterate over the children + pruneEmptyContainers(container); + } + } + } + + /** + * The client passes in a list of Iterable objects, and + * the Threader constructs a connected 'graph' of messages + * @param messages iterable of messages to thread, must not be empty + * @return null if messages == null or root.child == null or messages list is empty + * @since 3.0 + */ + public Threadable thread(final Iterable<? extends Threadable> messages) { + if (messages == null) { + return null; + } + + HashMap<String,ThreadContainer> idTable = new HashMap<>(); + + // walk through each Threadable element + for (final Threadable t : messages) { + if (!t.isDummy()) { + buildContainer(t, idTable); + } + } + + if (idTable.isEmpty()) { + return null; + } + + final ThreadContainer root = findRootSet(idTable); + idTable.clear(); + idTable = null; + + pruneEmptyContainers(root); + + root.reverseChildren(); + gatherSubjects(root); + + if (root.next != null) { + throw new RuntimeException("root node has a next:" + root); + } + + for (ThreadContainer r = root.child; r != null; r = r.next) { + if (r.threadable == null) { + r.threadable = r.child.threadable.makeDummy(); + } + } + + final Threadable result = (root.child == null ? null : root.child.threadable); + root.flush(); + + return result; + } + + /** + * The client passes in a list of Threadable objects, and + * the Threader constructs a connected 'graph' of messages + * @param messages list of messages to thread, must not be empty + * @return null if messages == null or root.child == null or messages list is empty + * @since 2.2 + */ + public Threadable thread(final List<? extends Threadable> messages) { + return thread((Iterable<? extends Threadable>)messages); + } + + + // DEPRECATED METHODS - for API compatibility only - DO NOT USE + + /** + * The client passes in an array of Threadable objects, and + * the Threader constructs a connected 'graph' of messages + * @param messages array of messages to thread, must not be empty + * @return null if messages == null or root.child == null or messages array is empty + * @deprecated (2.2) prefer {@link #thread(List)} + */ + @Deprecated + public Threadable thread(final Threadable[] messages) { + if (messages == null) { + return null; + } + return thread(Arrays.asList(messages)); + } + +} diff --git a/src/test/java/org/apache/commons/net/tftp/TFTPServer.java b/src/test/java/org/apache/commons/net/tftp/TFTPServer.java index 21f6fa84..cddedf42 100644 --- a/src/test/java/org/apache/commons/net/tftp/TFTPServer.java +++ b/src/test/java/org/apache/commons/net/tftp/TFTPServer.java @@ -1,952 +1,947 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one or more - * contributor license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright ownership. - * The ASF licenses this file to You under the Apache License, Version 2.0 - * (the "License"); you may not use this file except in compliance with - * the License. You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package org.apache.commons.net.tftp; - -import java.io.BufferedInputStream; -import java.io.BufferedOutputStream; -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.OutputStream; -import java.io.PrintStream; -import java.net.InetAddress; -import java.net.NetworkInterface; -import java.net.SocketTimeoutException; -import java.util.Enumeration; -import java.util.HashSet; -import java.util.Iterator; - -import org.apache.commons.net.io.FromNetASCIIOutputStream; -import org.apache.commons.net.io.ToNetASCIIInputStream; - -/** - * A fully multi-threaded tftp server. Can handle multiple clients at the same time. Implements RFC - * 1350 and wrapping block numbers for large file support. - * - * To launch, just create an instance of the class. An IOException will be thrown if the server - * fails to start for reasons such as port in use, port denied, etc. - * - * To stop, use the shutdown method. - * - * To check to see if the server is still running (or if it stopped because of an error), call the - * isRunning() method. - * - * By default, events are not logged to stdout/stderr. This can be changed with the - * setLog and setLogError methods. - * - * <p> - * Example usage is below: - * - * <code> - * public static void main(String[] args) throws Exception - * { - * if (args.length != 1) - * { - * System.out - * .println("You must provide 1 argument - the base path for the server to serve from."); - * System.exit(1); - * } - * - * TFTPServer ts = new TFTPServer(new File(args[0]), new File(args[0]), GET_AND_PUT); - * ts.setSocketTimeout(2000); - * - * System.out.println("TFTP Server running. Press enter to stop."); - * new InputStreamReader(System.in).read(); - * - * ts.shutdown(); - * System.out.println("Server shut down."); - * System.exit(0); - * } - * - * </code> - * - * @since 2.0 - */ - -public class TFTPServer implements Runnable -{ - public enum ServerMode { GET_ONLY, PUT_ONLY, GET_AND_PUT} - /* - * An instance of an ongoing transfer. - */ - private class TFTPTransfer implements Runnable - { - private final TFTPPacket tftpPacket_; - - private boolean shutdownTransfer; - - TFTP transferTftp_; - - public TFTPTransfer(final TFTPPacket tftpPacket) - { - tftpPacket_ = tftpPacket; - } - - /* - * Utility method to make sure that paths provided by tftp clients do not get outside of the - * serverRoot directory. - */ - private File buildSafeFile(final File serverDirectory, final String fileName, final boolean createSubDirs) - throws IOException - { - File temp = new File(serverDirectory, fileName); - temp = temp.getCanonicalFile(); - - if (!isSubdirectoryOf(serverDirectory, temp)) - { - throw new IOException("Cannot access files outside of tftp server root."); - } - - // ensure directory exists (if requested) - if (createSubDirs) - { - createDirectory(temp.getParentFile()); - } - - return temp; - } - - /* - * recursively create subdirectories - */ - private void createDirectory(final File file) throws IOException - { - final File parent = file.getParentFile(); - if (parent == null) - { - throw new IOException("Unexpected error creating requested directory"); - } - if (!parent.exists()) - { - // recurse... - createDirectory(parent); - } - - if (!parent.isDirectory()) { - throw new IOException( - "Invalid directory path - file in the way of requested folder"); - } - if (file.isDirectory()) - { - return; - } - final boolean result = file.mkdir(); - if (!result) - { - throw new IOException("Couldn't create requested directory"); - } - } - - /* - * Handle a tftp read request. - */ - private void handleRead(final TFTPReadRequestPacket trrp) throws IOException, TFTPPacketException - { - InputStream is = null; - try - { - if (mode_ == ServerMode.PUT_ONLY) - { - transferTftp_.bufferedSend(new TFTPErrorPacket(trrp.getAddress(), trrp - .getPort(), TFTPErrorPacket.ILLEGAL_OPERATION, - "Read not allowed by server.")); - return; - } - - try - { - is = new BufferedInputStream(new FileInputStream(buildSafeFile( - serverReadDirectory_, trrp.getFilename(), false))); - } - catch (final FileNotFoundException e) - { - transferTftp_.bufferedSend(new TFTPErrorPacket(trrp.getAddress(), trrp - .getPort(), TFTPErrorPacket.FILE_NOT_FOUND, e.getMessage())); - return; - } - catch (final Exception e) - { - transferTftp_.bufferedSend(new TFTPErrorPacket(trrp.getAddress(), trrp - .getPort(), TFTPErrorPacket.UNDEFINED, e.getMessage())); - return; - } - - if (trrp.getMode() == TFTP.NETASCII_MODE) - { - is = new ToNetASCIIInputStream(is); - } - - final byte[] temp = new byte[TFTPDataPacket.MAX_DATA_LENGTH]; - - TFTPPacket answer; - - int block = 1; - boolean sendNext = true; - - int readLength = TFTPDataPacket.MAX_DATA_LENGTH; - - TFTPDataPacket lastSentData = null; - - // We are reading a file, so when we read less than the - // requested bytes, we know that we are at the end of the file. - while (readLength == TFTPDataPacket.MAX_DATA_LENGTH && !shutdownTransfer) - { - if (sendNext) - { - readLength = is.read(temp); - if (readLength == -1) - { - readLength = 0; - } - - lastSentData = new TFTPDataPacket(trrp.getAddress(), trrp.getPort(), block, - temp, 0, readLength); - sendData(transferTftp_, lastSentData); // send the data - } - - answer = null; - - int timeoutCount = 0; - - while (!shutdownTransfer - && (answer == null || !answer.getAddress().equals(trrp.getAddress()) || answer - .getPort() != trrp.getPort())) - { - // listen for an answer. - if (answer != null) - { - // The answer that we got didn't come from the - // expected source, fire back an error, and continue - // listening. - log_.println("TFTP Server ignoring message from unexpected source."); - transferTftp_.bufferedSend(new TFTPErrorPacket(answer.getAddress(), - answer.getPort(), TFTPErrorPacket.UNKNOWN_TID, - "Unexpected Host or Port")); - } - try - { - answer = transferTftp_.bufferedReceive(); - } - catch (final SocketTimeoutException e) - { - if (timeoutCount >= maxTimeoutRetries_) - { - throw e; - } - // didn't get an ack for this data. need to resend - // it. - timeoutCount++; - transferTftp_.bufferedSend(lastSentData); - continue; - } - } - - if (answer == null || !(answer instanceof TFTPAckPacket)) - { - if (!shutdownTransfer) - { - logError_ - .println("Unexpected response from tftp client during transfer (" - + answer + "). Transfer aborted."); - } - break; - } - // once we get here, we know we have an answer packet - // from the correct host. - final TFTPAckPacket ack = (TFTPAckPacket) answer; - if (ack.getBlockNumber() != block) - { - /* - * The origional tftp spec would have called on us to resend the - * previous data here, however, that causes the SAS Syndrome. - * http://www.faqs.org/rfcs/rfc1123.html section 4.2.3.1 The modified - * spec says that we ignore a duplicate ack. If the packet was really - * lost, we will time out on receive, and resend the previous data at - * that point. - */ - sendNext = false; - } - else - { - // send the next block - block++; - if (block > 65535) - { - // wrap the block number - block = 0; - } - sendNext = true; - } - } - } - finally - { - try - { - if (is != null) - { - is.close(); - } - } - catch (final IOException e) - { - // noop - } - } - } - - /* - * handle a tftp write request. - */ - private void handleWrite(final TFTPWriteRequestPacket twrp) throws IOException, - TFTPPacketException - { - OutputStream bos = null; - try - { - if (mode_ == ServerMode.GET_ONLY) - { - transferTftp_.bufferedSend(new TFTPErrorPacket(twrp.getAddress(), twrp - .getPort(), TFTPErrorPacket.ILLEGAL_OPERATION, - "Write not allowed by server.")); - return; - } - - int lastBlock = 0; - final String fileName = twrp.getFilename(); - - try - { - final File temp = buildSafeFile(serverWriteDirectory_, fileName, true); - if (temp.exists()) - { - transferTftp_.bufferedSend(new TFTPErrorPacket(twrp.getAddress(), twrp - .getPort(), TFTPErrorPacket.FILE_EXISTS, "File already exists")); - return; - } - bos = new BufferedOutputStream(new FileOutputStream(temp)); - - if (twrp.getMode() == TFTP.NETASCII_MODE) - { - bos = new FromNetASCIIOutputStream(bos); - } - } - catch (final Exception e) - { - transferTftp_.bufferedSend(new TFTPErrorPacket(twrp.getAddress(), twrp - .getPort(), TFTPErrorPacket.UNDEFINED, e.getMessage())); - return; - } - - TFTPAckPacket lastSentAck = new TFTPAckPacket(twrp.getAddress(), twrp.getPort(), 0); - sendData(transferTftp_, lastSentAck); // send the data - - while (true) - { - // get the response - ensure it is from the right place. - TFTPPacket dataPacket = null; - - int timeoutCount = 0; - - while (!shutdownTransfer - && (dataPacket == null - || !dataPacket.getAddress().equals(twrp.getAddress()) || dataPacket - .getPort() != twrp.getPort())) - { - // listen for an answer. - if (dataPacket != null) - { - // The data that we got didn't come from the - // expected source, fire back an error, and continue - // listening. - log_.println("TFTP Server ignoring message from unexpected source."); - transferTftp_.bufferedSend(new TFTPErrorPacket(dataPacket.getAddress(), - dataPacket.getPort(), TFTPErrorPacket.UNKNOWN_TID, - "Unexpected Host or Port")); - } - - try - { - dataPacket = transferTftp_.bufferedReceive(); - } - catch (final SocketTimeoutException e) - { - if (timeoutCount >= maxTimeoutRetries_) - { - throw e; - } - // It didn't get our ack. Resend it. - transferTftp_.bufferedSend(lastSentAck); - timeoutCount++; - continue; - } - } - - if (dataPacket instanceof TFTPWriteRequestPacket) - { - // it must have missed our initial ack. Send another. - lastSentAck = new TFTPAckPacket(twrp.getAddress(), twrp.getPort(), 0); - transferTftp_.bufferedSend(lastSentAck); - } - else if (dataPacket == null || !(dataPacket instanceof TFTPDataPacket)) - { - if (!shutdownTransfer) - { - logError_ - .println("Unexpected response from tftp client during transfer (" - + dataPacket + "). Transfer aborted."); - } - break; - } - else - { - final int block = ((TFTPDataPacket) dataPacket).getBlockNumber(); - final byte[] data = ((TFTPDataPacket) dataPacket).getData(); - final int dataLength = ((TFTPDataPacket) dataPacket).getDataLength(); - final int dataOffset = ((TFTPDataPacket) dataPacket).getDataOffset(); - - if (block > lastBlock || lastBlock == 65535 && block == 0) - { - // it might resend a data block if it missed our ack - // - don't rewrite the block. - bos.write(data, dataOffset, dataLength); - lastBlock = block; - } - - lastSentAck = new TFTPAckPacket(twrp.getAddress(), twrp.getPort(), block); - sendData(transferTftp_, lastSentAck); // send the data - if (dataLength < TFTPDataPacket.MAX_DATA_LENGTH) - { - // end of stream signal - The tranfer is complete. - bos.close(); - - // But my ack may be lost - so listen to see if I - // need to resend the ack. - for (int i = 0; i < maxTimeoutRetries_; i++) - { - try - { - dataPacket = transferTftp_.bufferedReceive(); - } - catch (final SocketTimeoutException e) - { - // this is the expected route - the client - // shouldn't be sending any more packets. - break; - } - - if (dataPacket != null - && (!dataPacket.getAddress().equals(twrp.getAddress()) || dataPacket - .getPort() != twrp.getPort())) - { - // make sure it was from the right client... - transferTftp_ - .bufferedSend(new TFTPErrorPacket(dataPacket - .getAddress(), dataPacket.getPort(), - TFTPErrorPacket.UNKNOWN_TID, - "Unexpected Host or Port")); - } - else - { - // This means they sent us the last - // datapacket again, must have missed our - // ack. resend it. - transferTftp_.bufferedSend(lastSentAck); - } - } - - // all done. - break; - } - } - } - } - finally - { - if (bos != null) - { - bos.close(); - } - } - } - - /* - * recursively check to see if one directory is a parent of another. - */ - private boolean isSubdirectoryOf(final File parent, final File child) - { - final File childsParent = child.getParentFile(); - if (childsParent == null) - { - return false; - } - if (childsParent.equals(parent)) - { - return true; - } - return isSubdirectoryOf(parent, childsParent); - } - - @Override - public void run() - { - try - { - transferTftp_ = newTFTP(); - - transferTftp_.beginBufferedOps(); - transferTftp_.setDefaultTimeout(socketTimeout_); - - transferTftp_.open(); - - if (tftpPacket_ instanceof TFTPReadRequestPacket) - { - handleRead((TFTPReadRequestPacket) tftpPacket_); - } - else if (tftpPacket_ instanceof TFTPWriteRequestPacket) - { - handleWrite((TFTPWriteRequestPacket) tftpPacket_); - } - else - { - log_.println("Unsupported TFTP request (" + tftpPacket_ + ") - ignored."); - } - } - catch (final Exception e) - { - if (!shutdownTransfer) - { - logError_ - .println("Unexpected Error in during TFTP file transfer. Transfer aborted. " - + e); - } - } - finally - { - try - { - if (transferTftp_ != null && transferTftp_.isOpen()) - { - transferTftp_.endBufferedOps(); - transferTftp_.close(); - } - } - catch (final Exception e) - { - // noop - } - synchronized(transfers_) - { - transfers_.remove(this); - } - } - } - - public void shutdown() - { - shutdownTransfer = true; - try - { - transferTftp_.close(); - } - catch (final RuntimeException e) - { - // noop - } - } - } - - private static final int DEFAULT_TFTP_PORT = 69; - /* /dev/null output stream (default) */ - private static final PrintStream nullStream = new PrintStream( - new OutputStream() { - @Override - public void write(final byte[] b) throws IOException {} - @Override - public void write(final int b){} - } - ); - private final HashSet<TFTPTransfer> transfers_ = new HashSet<>(); - private volatile boolean shutdownServer; - private TFTP serverTftp_; - private File serverReadDirectory_; - private File serverWriteDirectory_; - private final int port_; - private final InetAddress laddr_; - - private Exception serverException; - - private final ServerMode mode_; - // don't have access to a logger api, so we will log to these streams, which - // by default are set to a no-op logger - private PrintStream log_; - - private PrintStream logError_; - private int maxTimeoutRetries_ = 3; - private int socketTimeout_; - - - private Thread serverThread; - - /** - * Start a TFTP Server on the specified port. Gets and Puts occur in the specified directory. - * - * The server will start in another thread, allowing this constructor to return immediately. - * - * If a get or a put comes in with a relative path that tries to get outside of the - * serverDirectory, then the get or put will be denied. - * - * GET_ONLY mode only allows gets, PUT_ONLY mode only allows puts, and GET_AND_PUT allows both. - * Modes are defined as int constants in this class. - * - * @param serverReadDirectory directory for GET requests - * @param serverWriteDirectory directory for PUT requests - * @param port The local port to bind to. - * @param localaddr The local address to bind to. - * @param mode A value as specified above. - * @param log Stream to write log message to. If not provided, uses System.out - * @param errorLog Stream to write error messages to. If not provided, uses System.err. - * @throws IOException if the server directory is invalid or does not exist. - */ - public TFTPServer(final File serverReadDirectory, final File serverWriteDirectory, final int port, - final InetAddress localaddr, final ServerMode mode, final PrintStream log, final PrintStream errorLog) - throws IOException - { - port_ = port; - mode_ = mode; - laddr_ = localaddr; - log_ = log == null ? nullStream: log; - logError_ = errorLog == null ? nullStream : errorLog; - launch(serverReadDirectory, serverWriteDirectory); - } - - /** - * Start a TFTP Server on the specified port. Gets and Puts occur in the specified directory. - * - * The server will start in another thread, allowing this constructor to return immediately. - * - * If a get or a put comes in with a relative path that tries to get outside of the - * serverDirectory, then the get or put will be denied. - * - * GET_ONLY mode only allows gets, PUT_ONLY mode only allows puts, and GET_AND_PUT allows both. - * Modes are defined as int constants in this class. - * - * @param serverReadDirectory directory for GET requests - * @param serverWriteDirectory directory for PUT requests - * @param port the port to use - * @param localiface The local network interface to bind to. - * The interface's first address wil be used. - * @param mode A value as specified above. - * @param log Stream to write log message to. If not provided, uses System.out - * @param errorLog Stream to write error messages to. If not provided, uses System.err. - * @throws IOException if the server directory is invalid or does not exist. - */ - public TFTPServer(final File serverReadDirectory, final File serverWriteDirectory, final int port, - final NetworkInterface localiface, final ServerMode mode, final PrintStream log, final PrintStream errorLog) - throws IOException - { - mode_ = mode; - port_= port; - InetAddress iaddr = null; - if (localiface != null) - { - final Enumeration<InetAddress> ifaddrs = localiface.getInetAddresses(); - if ((ifaddrs != null) && ifaddrs.hasMoreElements()) { - iaddr = ifaddrs.nextElement(); - } - } - log_ = log == null ? nullStream: log; - logError_ = errorLog == null ? nullStream : errorLog; - laddr_ = iaddr; - launch(serverReadDirectory, serverWriteDirectory); - } - - /** - * Start a TFTP Server on the specified port. Gets and Puts occur in the specified directory. - * - * The server will start in another thread, allowing this constructor to return immediately. - * - * If a get or a put comes in with a relative path that tries to get outside of the - * serverDirectory, then the get or put will be denied. - * - * GET_ONLY mode only allows gets, PUT_ONLY mode only allows puts, and GET_AND_PUT allows both. - * Modes are defined as int constants in this class. - * - * @param serverReadDirectory directory for GET requests - * @param serverWriteDirectory directory for PUT requests - * @param port the port to use - * @param mode A value as specified above. - * @param log Stream to write log message to. If not provided, uses System.out - * @param errorLog Stream to write error messages to. If not provided, uses System.err. - * @throws IOException if the server directory is invalid or does not exist. - */ - public TFTPServer(final File serverReadDirectory, final File serverWriteDirectory, final int port, final ServerMode mode, - final PrintStream log, final PrintStream errorLog) throws IOException - { - port_ = port; - mode_ = mode; - log_ = log == null ? nullStream: log; - logError_ = errorLog == null ? nullStream : errorLog; - laddr_ = null; - launch(serverReadDirectory, serverWriteDirectory); - } - - /** - * Start a TFTP Server on the default port (69). Gets and Puts occur in the specified - * directories. - * - * The server will start in another thread, allowing this constructor to return immediately. - * - * If a get or a put comes in with a relative path that tries to get outside of the - * serverDirectory, then the get or put will be denied. - * - * GET_ONLY mode only allows gets, PUT_ONLY mode only allows puts, and GET_AND_PUT allows both. - * Modes are defined as int constants in this class. - * - * @param serverReadDirectory directory for GET requests - * @param serverWriteDirectory directory for PUT requests - * @param mode A value as specified above. - * @throws IOException if the server directory is invalid or does not exist. - */ - public TFTPServer(final File serverReadDirectory, final File serverWriteDirectory, final ServerMode mode) - throws IOException - { - this(serverReadDirectory, serverWriteDirectory, DEFAULT_TFTP_PORT, mode, null, null); - } - - @Override - protected void finalize() throws Throwable - { - shutdown(); - } - - /** - * Get the current value for maxTimeoutRetries - * @return the max allowed number of retries - */ - public int getMaxTimeoutRetries() - { - return maxTimeoutRetries_; - } - - /** - * The current socket timeout used during transfers in milliseconds. - * @return the timeout value - */ - public int getSocketTimeout() - { - return socketTimeout_; - } - - /** - * check if the server thread is still running. - * - * @return true if running, false if stopped. - * @throws Exception throws the exception that stopped the server if the server is stopped from - * an exception. - */ - public boolean isRunning() throws Exception - { - if (shutdownServer && serverException != null) - { - throw serverException; - } - return !shutdownServer; - } - - /* - * start the server, throw an error if it can't start. - */ - private void launch(final File serverReadDirectory, final File serverWriteDirectory) throws IOException - { - log_.println("Starting TFTP Server on port " + port_ + ". Read directory: " - + serverReadDirectory + " Write directory: " + serverWriteDirectory - + " Server Mode is " + mode_); - - serverReadDirectory_ = serverReadDirectory.getCanonicalFile(); - if (!serverReadDirectory_.exists() || !serverReadDirectory.isDirectory()) - { - throw new IOException("The server read directory " + serverReadDirectory_ - + " does not exist"); - } - - serverWriteDirectory_ = serverWriteDirectory.getCanonicalFile(); - if (!serverWriteDirectory_.exists() || !serverWriteDirectory.isDirectory()) - { - throw new IOException("The server write directory " + serverWriteDirectory_ - + " does not exist"); - } - - serverTftp_ = new TFTP(); - - // This is the value used in response to each client. - socketTimeout_ = serverTftp_.getDefaultTimeout(); - - // we want the server thread to listen forever. - serverTftp_.setDefaultTimeout(0); - - if (laddr_ != null) { - serverTftp_.open(port_, laddr_); - } else { - serverTftp_.open(port_); - } - - serverThread = new Thread(this); - serverThread.setDaemon(true); - serverThread.start(); - } - - /* - * Allow test code to customise the TFTP instance - */ - TFTP newTFTP() { - return new TFTP(); - } - - @Override - public void run() - { - try - { - while (!shutdownServer) - { - final TFTPPacket tftpPacket; - - tftpPacket = serverTftp_.receive(); - - final TFTPTransfer tt = new TFTPTransfer(tftpPacket); - synchronized(transfers_) - { - transfers_.add(tt); - } - - final Thread thread = new Thread(tt); - thread.setDaemon(true); - thread.start(); - } - } - catch (final Exception e) - { - if (!shutdownServer) - { - serverException = e; - logError_.println("Unexpected Error in TFTP Server - Server shut down! + " + e); - } - } - finally - { - shutdownServer = true; // set this to true, so the launching thread can check to see if it started. - if (serverTftp_ != null && serverTftp_.isOpen()) - { - serverTftp_.close(); - } - } - } - - /* - * Also allow customisation of sending data/ack so can generate errors if needed - */ - void sendData(final TFTP tftp, final TFTPPacket data) throws IOException { - tftp.bufferedSend(data); - } - - /** - * Set the stream object to log debug / informational messages. By default, this is a no-op - * - * @param log the stream to use for logging - */ - public void setLog(final PrintStream log) - { - this.log_ = log; - } - - /** - * Set the stream object to log error messsages. By default, this is a no-op - * - * @param logError the stream to use for logging errors - */ - public void setLogError(final PrintStream logError) - { - this.logError_ = logError; - } - - /** - * Set the max number of retries in response to a timeout. Default 3. Min 0. - * - * @param retries number of retries, must be > 0 - */ - public void setMaxTimeoutRetries(final int retries) - { - if (retries < 0) - { - throw new RuntimeException("Invalid Value"); - } - maxTimeoutRetries_ = retries; - } - - /** - * Set the socket timeout in milliseconds used in transfers. Defaults to the value here: - * https://commons.apache.org/net/apidocs/org/apache/commons/net/tftp/TFTP.html#DEFAULT_TIMEOUT - * (5000 at the time I write this) Min value of 10. - * @param timeout the timeout; must be larger than 10 - */ - public void setSocketTimeout(final int timeout) - { - if (timeout < 10) - { - throw new RuntimeException("Invalid Value"); - } - socketTimeout_ = timeout; - } - - /** - * Stop the tftp server (and any currently running transfers) and release all opened network - * resources. - */ - public void shutdown() - { - shutdownServer = true; - - synchronized(transfers_) - { - final Iterator<TFTPTransfer> it = transfers_.iterator(); - while (it.hasNext()) - { - it.next().shutdown(); - } - } - - try - { - serverTftp_.close(); - } - catch (final RuntimeException e) - { - // noop - } - - try { - serverThread.join(); - } catch (final InterruptedException e) { - // we've done the best we could, return - } - } -} +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.commons.net.tftp; + +import java.io.BufferedInputStream; +import java.io.BufferedOutputStream; +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.OutputStream; +import java.io.PrintStream; +import java.net.InetAddress; +import java.net.NetworkInterface; +import java.net.SocketTimeoutException; +import java.util.Enumeration; +import java.util.HashSet; + +import org.apache.commons.net.io.FromNetASCIIOutputStream; +import org.apache.commons.net.io.ToNetASCIIInputStream; + +/** + * A fully multi-threaded tftp server. Can handle multiple clients at the same time. Implements RFC + * 1350 and wrapping block numbers for large file support. + * + * To launch, just create an instance of the class. An IOException will be thrown if the server + * fails to start for reasons such as port in use, port denied, etc. + * + * To stop, use the shutdown method. + * + * To check to see if the server is still running (or if it stopped because of an error), call the + * isRunning() method. + * + * By default, events are not logged to stdout/stderr. This can be changed with the + * setLog and setLogError methods. + * + * <p> + * Example usage is below: + * + * <code> + * public static void main(String[] args) throws Exception + * { + * if (args.length != 1) + * { + * System.out + * .println("You must provide 1 argument - the base path for the server to serve from."); + * System.exit(1); + * } + * + * TFTPServer ts = new TFTPServer(new File(args[0]), new File(args[0]), GET_AND_PUT); + * ts.setSocketTimeout(2000); + * + * System.out.println("TFTP Server running. Press enter to stop."); + * new InputStreamReader(System.in).read(); + * + * ts.shutdown(); + * System.out.println("Server shut down."); + * System.exit(0); + * } + * + * </code> + * + * @since 2.0 + */ + +public class TFTPServer implements Runnable +{ + public enum ServerMode { GET_ONLY, PUT_ONLY, GET_AND_PUT} + /* + * An instance of an ongoing transfer. + */ + private class TFTPTransfer implements Runnable + { + private final TFTPPacket tftpPacket_; + + private boolean shutdownTransfer; + + TFTP transferTftp_; + + public TFTPTransfer(final TFTPPacket tftpPacket) + { + tftpPacket_ = tftpPacket; + } + + /* + * Utility method to make sure that paths provided by tftp clients do not get outside of the + * serverRoot directory. + */ + private File buildSafeFile(final File serverDirectory, final String fileName, final boolean createSubDirs) + throws IOException + { + File temp = new File(serverDirectory, fileName); + temp = temp.getCanonicalFile(); + + if (!isSubdirectoryOf(serverDirectory, temp)) + { + throw new IOException("Cannot access files outside of tftp server root."); + } + + // ensure directory exists (if requested) + if (createSubDirs) + { + createDirectory(temp.getParentFile()); + } + + return temp; + } + + /* + * recursively create subdirectories + */ + private void createDirectory(final File file) throws IOException + { + final File parent = file.getParentFile(); + if (parent == null) + { + throw new IOException("Unexpected error creating requested directory"); + } + if (!parent.exists()) + { + // recurse... + createDirectory(parent); + } + + if (!parent.isDirectory()) { + throw new IOException( + "Invalid directory path - file in the way of requested folder"); + } + if (file.isDirectory()) + { + return; + } + final boolean result = file.mkdir(); + if (!result) + { + throw new IOException("Couldn't create requested directory"); + } + } + + /* + * Handle a tftp read request. + */ + private void handleRead(final TFTPReadRequestPacket trrp) throws IOException, TFTPPacketException + { + InputStream is = null; + try + { + if (mode_ == ServerMode.PUT_ONLY) + { + transferTftp_.bufferedSend(new TFTPErrorPacket(trrp.getAddress(), trrp + .getPort(), TFTPErrorPacket.ILLEGAL_OPERATION, + "Read not allowed by server.")); + return; + } + + try + { + is = new BufferedInputStream(new FileInputStream(buildSafeFile( + serverReadDirectory_, trrp.getFilename(), false))); + } + catch (final FileNotFoundException e) + { + transferTftp_.bufferedSend(new TFTPErrorPacket(trrp.getAddress(), trrp + .getPort(), TFTPErrorPacket.FILE_NOT_FOUND, e.getMessage())); + return; + } + catch (final Exception e) + { + transferTftp_.bufferedSend(new TFTPErrorPacket(trrp.getAddress(), trrp + .getPort(), TFTPErrorPacket.UNDEFINED, e.getMessage())); + return; + } + + if (trrp.getMode() == TFTP.NETASCII_MODE) + { + is = new ToNetASCIIInputStream(is); + } + + final byte[] temp = new byte[TFTPDataPacket.MAX_DATA_LENGTH]; + + TFTPPacket answer; + + int block = 1; + boolean sendNext = true; + + int readLength = TFTPDataPacket.MAX_DATA_LENGTH; + + TFTPDataPacket lastSentData = null; + + // We are reading a file, so when we read less than the + // requested bytes, we know that we are at the end of the file. + while (readLength == TFTPDataPacket.MAX_DATA_LENGTH && !shutdownTransfer) + { + if (sendNext) + { + readLength = is.read(temp); + if (readLength == -1) + { + readLength = 0; + } + + lastSentData = new TFTPDataPacket(trrp.getAddress(), trrp.getPort(), block, + temp, 0, readLength); + sendData(transferTftp_, lastSentData); // send the data + } + + answer = null; + + int timeoutCount = 0; + + while (!shutdownTransfer + && (answer == null || !answer.getAddress().equals(trrp.getAddress()) || answer + .getPort() != trrp.getPort())) + { + // listen for an answer. + if (answer != null) + { + // The answer that we got didn't come from the + // expected source, fire back an error, and continue + // listening. + log_.println("TFTP Server ignoring message from unexpected source."); + transferTftp_.bufferedSend(new TFTPErrorPacket(answer.getAddress(), + answer.getPort(), TFTPErrorPacket.UNKNOWN_TID, + "Unexpected Host or Port")); + } + try + { + answer = transferTftp_.bufferedReceive(); + } + catch (final SocketTimeoutException e) + { + if (timeoutCount >= maxTimeoutRetries_) + { + throw e; + } + // didn't get an ack for this data. need to resend + // it. + timeoutCount++; + transferTftp_.bufferedSend(lastSentData); + continue; + } + } + + if (answer == null || !(answer instanceof TFTPAckPacket)) + { + if (!shutdownTransfer) + { + logError_ + .println("Unexpected response from tftp client during transfer (" + + answer + "). Transfer aborted."); + } + break; + } + // once we get here, we know we have an answer packet + // from the correct host. + final TFTPAckPacket ack = (TFTPAckPacket) answer; + if (ack.getBlockNumber() != block) + { + /* + * The origional tftp spec would have called on us to resend the + * previous data here, however, that causes the SAS Syndrome. + * http://www.faqs.org/rfcs/rfc1123.html section 4.2.3.1 The modified + * spec says that we ignore a duplicate ack. If the packet was really + * lost, we will time out on receive, and resend the previous data at + * that point. + */ + sendNext = false; + } + else + { + // send the next block + block++; + if (block > 65535) + { + // wrap the block number + block = 0; + } + sendNext = true; + } + } + } + finally + { + try + { + if (is != null) + { + is.close(); + } + } + catch (final IOException e) + { + // noop + } + } + } + + /* + * handle a tftp write request. + */ + private void handleWrite(final TFTPWriteRequestPacket twrp) throws IOException, + TFTPPacketException + { + OutputStream bos = null; + try + { + if (mode_ == ServerMode.GET_ONLY) + { + transferTftp_.bufferedSend(new TFTPErrorPacket(twrp.getAddress(), twrp + .getPort(), TFTPErrorPacket.ILLEGAL_OPERATION, + "Write not allowed by server.")); + return; + } + + int lastBlock = 0; + final String fileName = twrp.getFilename(); + + try + { + final File temp = buildSafeFile(serverWriteDirectory_, fileName, true); + if (temp.exists()) + { + transferTftp_.bufferedSend(new TFTPErrorPacket(twrp.getAddress(), twrp + .getPort(), TFTPErrorPacket.FILE_EXISTS, "File already exists")); + return; + } + bos = new BufferedOutputStream(new FileOutputStream(temp)); + + if (twrp.getMode() == TFTP.NETASCII_MODE) + { + bos = new FromNetASCIIOutputStream(bos); + } + } + catch (final Exception e) + { + transferTftp_.bufferedSend(new TFTPErrorPacket(twrp.getAddress(), twrp + .getPort(), TFTPErrorPacket.UNDEFINED, e.getMessage())); + return; + } + + TFTPAckPacket lastSentAck = new TFTPAckPacket(twrp.getAddress(), twrp.getPort(), 0); + sendData(transferTftp_, lastSentAck); // send the data + + while (true) + { + // get the response - ensure it is from the right place. + TFTPPacket dataPacket = null; + + int timeoutCount = 0; + + while (!shutdownTransfer + && (dataPacket == null + || !dataPacket.getAddress().equals(twrp.getAddress()) || dataPacket + .getPort() != twrp.getPort())) + { + // listen for an answer. + if (dataPacket != null) + { + // The data that we got didn't come from the + // expected source, fire back an error, and continue + // listening. + log_.println("TFTP Server ignoring message from unexpected source."); + transferTftp_.bufferedSend(new TFTPErrorPacket(dataPacket.getAddress(), + dataPacket.getPort(), TFTPErrorPacket.UNKNOWN_TID, + "Unexpected Host or Port")); + } + + try + { + dataPacket = transferTftp_.bufferedReceive(); + } + catch (final SocketTimeoutException e) + { + if (timeoutCount >= maxTimeoutRetries_) + { + throw e; + } + // It didn't get our ack. Resend it. + transferTftp_.bufferedSend(lastSentAck); + timeoutCount++; + continue; + } + } + + if (dataPacket instanceof TFTPWriteRequestPacket) + { + // it must have missed our initial ack. Send another. + lastSentAck = new TFTPAckPacket(twrp.getAddress(), twrp.getPort(), 0); + transferTftp_.bufferedSend(lastSentAck); + } + else if (dataPacket == null || !(dataPacket instanceof TFTPDataPacket)) + { + if (!shutdownTransfer) + { + logError_ + .println("Unexpected response from tftp client during transfer (" + + dataPacket + "). Transfer aborted."); + } + break; + } + else + { + final int block = ((TFTPDataPacket) dataPacket).getBlockNumber(); + final byte[] data = ((TFTPDataPacket) dataPacket).getData(); + final int dataLength = ((TFTPDataPacket) dataPacket).getDataLength(); + final int dataOffset = ((TFTPDataPacket) dataPacket).getDataOffset(); + + if (block > lastBlock || lastBlock == 65535 && block == 0) + { + // it might resend a data block if it missed our ack + // - don't rewrite the block. + bos.write(data, dataOffset, dataLength); + lastBlock = block; + } + + lastSentAck = new TFTPAckPacket(twrp.getAddress(), twrp.getPort(), block); + sendData(transferTftp_, lastSentAck); // send the data + if (dataLength < TFTPDataPacket.MAX_DATA_LENGTH) + { + // end of stream signal - The tranfer is complete. + bos.close(); + + // But my ack may be lost - so listen to see if I + // need to resend the ack. + for (int i = 0; i < maxTimeoutRetries_; i++) + { + try + { + dataPacket = transferTftp_.bufferedReceive(); + } + catch (final SocketTimeoutException e) + { + // this is the expected route - the client + // shouldn't be sending any more packets. + break; + } + + if (dataPacket != null + && (!dataPacket.getAddress().equals(twrp.getAddress()) || dataPacket + .getPort() != twrp.getPort())) + { + // make sure it was from the right client... + transferTftp_ + .bufferedSend(new TFTPErrorPacket(dataPacket + .getAddress(), dataPacket.getPort(), + TFTPErrorPacket.UNKNOWN_TID, + "Unexpected Host or Port")); + } + else + { + // This means they sent us the last + // datapacket again, must have missed our + // ack. resend it. + transferTftp_.bufferedSend(lastSentAck); + } + } + + // all done. + break; + } + } + } + } + finally + { + if (bos != null) + { + bos.close(); + } + } + } + + /* + * recursively check to see if one directory is a parent of another. + */ + private boolean isSubdirectoryOf(final File parent, final File child) + { + final File childsParent = child.getParentFile(); + if (childsParent == null) + { + return false; + } + if (childsParent.equals(parent)) + { + return true; + } + return isSubdirectoryOf(parent, childsParent); + } + + @Override + public void run() + { + try + { + transferTftp_ = newTFTP(); + + transferTftp_.beginBufferedOps(); + transferTftp_.setDefaultTimeout(socketTimeout_); + + transferTftp_.open(); + + if (tftpPacket_ instanceof TFTPReadRequestPacket) + { + handleRead((TFTPReadRequestPacket) tftpPacket_); + } + else if (tftpPacket_ instanceof TFTPWriteRequestPacket) + { + handleWrite((TFTPWriteRequestPacket) tftpPacket_); + } + else + { + log_.println("Unsupported TFTP request (" + tftpPacket_ + ") - ignored."); + } + } + catch (final Exception e) + { + if (!shutdownTransfer) + { + logError_ + .println("Unexpected Error in during TFTP file transfer. Transfer aborted. " + + e); + } + } + finally + { + try + { + if (transferTftp_ != null && transferTftp_.isOpen()) + { + transferTftp_.endBufferedOps(); + transferTftp_.close(); + } + } + catch (final Exception e) + { + // noop + } + synchronized(transfers_) + { + transfers_.remove(this); + } + } + } + + public void shutdown() + { + shutdownTransfer = true; + try + { + transferTftp_.close(); + } + catch (final RuntimeException e) + { + // noop + } + } + } + + private static final int DEFAULT_TFTP_PORT = 69; + /* /dev/null output stream (default) */ + private static final PrintStream nullStream = new PrintStream( + new OutputStream() { + @Override + public void write(final byte[] b) throws IOException {} + @Override + public void write(final int b){} + } + ); + private final HashSet<TFTPTransfer> transfers_ = new HashSet<>(); + private volatile boolean shutdownServer; + private TFTP serverTftp_; + private File serverReadDirectory_; + private File serverWriteDirectory_; + private final int port_; + private final InetAddress laddr_; + + private Exception serverException; + + private final ServerMode mode_; + // don't have access to a logger api, so we will log to these streams, which + // by default are set to a no-op logger + private PrintStream log_; + + private PrintStream logError_; + private int maxTimeoutRetries_ = 3; + private int socketTimeout_; + + + private Thread serverThread; + + /** + * Start a TFTP Server on the specified port. Gets and Puts occur in the specified directory. + * + * The server will start in another thread, allowing this constructor to return immediately. + * + * If a get or a put comes in with a relative path that tries to get outside of the + * serverDirectory, then the get or put will be denied. + * + * GET_ONLY mode only allows gets, PUT_ONLY mode only allows puts, and GET_AND_PUT allows both. + * Modes are defined as int constants in this class. + * + * @param serverReadDirectory directory for GET requests + * @param serverWriteDirectory directory for PUT requests + * @param port The local port to bind to. + * @param localaddr The local address to bind to. + * @param mode A value as specified above. + * @param log Stream to write log message to. If not provided, uses System.out + * @param errorLog Stream to write error messages to. If not provided, uses System.err. + * @throws IOException if the server directory is invalid or does not exist. + */ + public TFTPServer(final File serverReadDirectory, final File serverWriteDirectory, final int port, + final InetAddress localaddr, final ServerMode mode, final PrintStream log, final PrintStream errorLog) + throws IOException + { + port_ = port; + mode_ = mode; + laddr_ = localaddr; + log_ = log == null ? nullStream: log; + logError_ = errorLog == null ? nullStream : errorLog; + launch(serverReadDirectory, serverWriteDirectory); + } + + /** + * Start a TFTP Server on the specified port. Gets and Puts occur in the specified directory. + * + * The server will start in another thread, allowing this constructor to return immediately. + * + * If a get or a put comes in with a relative path that tries to get outside of the + * serverDirectory, then the get or put will be denied. + * + * GET_ONLY mode only allows gets, PUT_ONLY mode only allows puts, and GET_AND_PUT allows both. + * Modes are defined as int constants in this class. + * + * @param serverReadDirectory directory for GET requests + * @param serverWriteDirectory directory for PUT requests + * @param port the port to use + * @param localiface The local network interface to bind to. + * The interface's first address wil be used. + * @param mode A value as specified above. + * @param log Stream to write log message to. If not provided, uses System.out + * @param errorLog Stream to write error messages to. If not provided, uses System.err. + * @throws IOException if the server directory is invalid or does not exist. + */ + public TFTPServer(final File serverReadDirectory, final File serverWriteDirectory, final int port, + final NetworkInterface localiface, final ServerMode mode, final PrintStream log, final PrintStream errorLog) + throws IOException + { + mode_ = mode; + port_= port; + InetAddress iaddr = null; + if (localiface != null) + { + final Enumeration<InetAddress> ifaddrs = localiface.getInetAddresses(); + if ((ifaddrs != null) && ifaddrs.hasMoreElements()) { + iaddr = ifaddrs.nextElement(); + } + } + log_ = log == null ? nullStream: log; + logError_ = errorLog == null ? nullStream : errorLog; + laddr_ = iaddr; + launch(serverReadDirectory, serverWriteDirectory); + } + + /** + * Start a TFTP Server on the specified port. Gets and Puts occur in the specified directory. + * + * The server will start in another thread, allowing this constructor to return immediately. + * + * If a get or a put comes in with a relative path that tries to get outside of the + * serverDirectory, then the get or put will be denied. + * + * GET_ONLY mode only allows gets, PUT_ONLY mode only allows puts, and GET_AND_PUT allows both. + * Modes are defined as int constants in this class. + * + * @param serverReadDirectory directory for GET requests + * @param serverWriteDirectory directory for PUT requests + * @param port the port to use + * @param mode A value as specified above. + * @param log Stream to write log message to. If not provided, uses System.out + * @param errorLog Stream to write error messages to. If not provided, uses System.err. + * @throws IOException if the server directory is invalid or does not exist. + */ + public TFTPServer(final File serverReadDirectory, final File serverWriteDirectory, final int port, final ServerMode mode, + final PrintStream log, final PrintStream errorLog) throws IOException + { + port_ = port; + mode_ = mode; + log_ = log == null ? nullStream: log; + logError_ = errorLog == null ? nullStream : errorLog; + laddr_ = null; + launch(serverReadDirectory, serverWriteDirectory); + } + + /** + * Start a TFTP Server on the default port (69). Gets and Puts occur in the specified + * directories. + * + * The server will start in another thread, allowing this constructor to return immediately. + * + * If a get or a put comes in with a relative path that tries to get outside of the + * serverDirectory, then the get or put will be denied. + * + * GET_ONLY mode only allows gets, PUT_ONLY mode only allows puts, and GET_AND_PUT allows both. + * Modes are defined as int constants in this class. + * + * @param serverReadDirectory directory for GET requests + * @param serverWriteDirectory directory for PUT requests + * @param mode A value as specified above. + * @throws IOException if the server directory is invalid or does not exist. + */ + public TFTPServer(final File serverReadDirectory, final File serverWriteDirectory, final ServerMode mode) + throws IOException + { + this(serverReadDirectory, serverWriteDirectory, DEFAULT_TFTP_PORT, mode, null, null); + } + + @Override + protected void finalize() throws Throwable + { + shutdown(); + } + + /** + * Get the current value for maxTimeoutRetries + * @return the max allowed number of retries + */ + public int getMaxTimeoutRetries() + { + return maxTimeoutRetries_; + } + + /** + * The current socket timeout used during transfers in milliseconds. + * @return the timeout value + */ + public int getSocketTimeout() + { + return socketTimeout_; + } + + /** + * check if the server thread is still running. + * + * @return true if running, false if stopped. + * @throws Exception throws the exception that stopped the server if the server is stopped from + * an exception. + */ + public boolean isRunning() throws Exception + { + if (shutdownServer && serverException != null) + { + throw serverException; + } + return !shutdownServer; + } + + /* + * start the server, throw an error if it can't start. + */ + private void launch(final File serverReadDirectory, final File serverWriteDirectory) throws IOException + { + log_.println("Starting TFTP Server on port " + port_ + ". Read directory: " + + serverReadDirectory + " Write directory: " + serverWriteDirectory + + " Server Mode is " + mode_); + + serverReadDirectory_ = serverReadDirectory.getCanonicalFile(); + if (!serverReadDirectory_.exists() || !serverReadDirectory.isDirectory()) + { + throw new IOException("The server read directory " + serverReadDirectory_ + + " does not exist"); + } + + serverWriteDirectory_ = serverWriteDirectory.getCanonicalFile(); + if (!serverWriteDirectory_.exists() || !serverWriteDirectory.isDirectory()) + { + throw new IOException("The server write directory " + serverWriteDirectory_ + + " does not exist"); + } + + serverTftp_ = new TFTP(); + + // This is the value used in response to each client. + socketTimeout_ = serverTftp_.getDefaultTimeout(); + + // we want the server thread to listen forever. + serverTftp_.setDefaultTimeout(0); + + if (laddr_ != null) { + serverTftp_.open(port_, laddr_); + } else { + serverTftp_.open(port_); + } + + serverThread = new Thread(this); + serverThread.setDaemon(true); + serverThread.start(); + } + + /* + * Allow test code to customise the TFTP instance + */ + TFTP newTFTP() { + return new TFTP(); + } + + @Override + public void run() + { + try + { + while (!shutdownServer) + { + final TFTPPacket tftpPacket; + + tftpPacket = serverTftp_.receive(); + + final TFTPTransfer tt = new TFTPTransfer(tftpPacket); + synchronized(transfers_) + { + transfers_.add(tt); + } + + final Thread thread = new Thread(tt); + thread.setDaemon(true); + thread.start(); + } + } + catch (final Exception e) + { + if (!shutdownServer) + { + serverException = e; + logError_.println("Unexpected Error in TFTP Server - Server shut down! + " + e); + } + } + finally + { + shutdownServer = true; // set this to true, so the launching thread can check to see if it started. + if (serverTftp_ != null && serverTftp_.isOpen()) + { + serverTftp_.close(); + } + } + } + + /* + * Also allow customisation of sending data/ack so can generate errors if needed + */ + void sendData(final TFTP tftp, final TFTPPacket data) throws IOException { + tftp.bufferedSend(data); + } + + /** + * Set the stream object to log debug / informational messages. By default, this is a no-op + * + * @param log the stream to use for logging + */ + public void setLog(final PrintStream log) + { + this.log_ = log; + } + + /** + * Set the stream object to log error messsages. By default, this is a no-op + * + * @param logError the stream to use for logging errors + */ + public void setLogError(final PrintStream logError) + { + this.logError_ = logError; + } + + /** + * Set the max number of retries in response to a timeout. Default 3. Min 0. + * + * @param retries number of retries, must be > 0 + */ + public void setMaxTimeoutRetries(final int retries) + { + if (retries < 0) + { + throw new RuntimeException("Invalid Value"); + } + maxTimeoutRetries_ = retries; + } + + /** + * Set the socket timeout in milliseconds used in transfers. Defaults to the value here: + * https://commons.apache.org/net/apidocs/org/apache/commons/net/tftp/TFTP.html#DEFAULT_TIMEOUT + * (5000 at the time I write this) Min value of 10. + * @param timeout the timeout; must be larger than 10 + */ + public void setSocketTimeout(final int timeout) + { + if (timeout < 10) + { + throw new RuntimeException("Invalid Value"); + } + socketTimeout_ = timeout; + } + + /** + * Stop the tftp server (and any currently running transfers) and release all opened network + * resources. + */ + public void shutdown() + { + shutdownServer = true; + + synchronized(transfers_) + { + transfers_.forEach(TFTPTransfer::shutdown); + } + + try + { + serverTftp_.close(); + } + catch (final RuntimeException e) + { + // noop + } + + try { + serverThread.join(); + } catch (final InterruptedException e) { + // we've done the best we could, return + } + } +}