You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@commons.apache.org by rw...@apache.org on 2008/02/24 22:41:05 UTC

svn commit: r630685 - in /commons/proper/net/trunk/src: java/org/apache/commons/net/tftp/TFTPServer.java test/org/apache/commons/net/tftp/ test/org/apache/commons/net/tftp/TFTPServerPathTests.java test/org/apache/commons/net/tftp/TFTPTests.java

Author: rwinston
Date: Sun Feb 24 13:41:05 2008
New Revision: 630685

URL: http://svn.apache.org/viewvc?rev=630685&view=rev
Log:
Add TFTP server (NET-187)

Added:
    commons/proper/net/trunk/src/java/org/apache/commons/net/tftp/TFTPServer.java
    commons/proper/net/trunk/src/test/org/apache/commons/net/tftp/
    commons/proper/net/trunk/src/test/org/apache/commons/net/tftp/TFTPServerPathTests.java
    commons/proper/net/trunk/src/test/org/apache/commons/net/tftp/TFTPTests.java

Added: commons/proper/net/trunk/src/java/org/apache/commons/net/tftp/TFTPServer.java
URL: http://svn.apache.org/viewvc/commons/proper/net/trunk/src/java/org/apache/commons/net/tftp/TFTPServer.java?rev=630685&view=auto
==============================================================================
--- commons/proper/net/trunk/src/java/org/apache/commons/net/tftp/TFTPServer.java (added)
+++ commons/proper/net/trunk/src/java/org/apache/commons/net/tftp/TFTPServer.java Sun Feb 24 13:41:05 2008
@@ -0,0 +1,806 @@
+package org.apache.commons.net.tftp;
+
+import java.io.*;
+import java.net.*;
+import java.util.*;
+import org.apache.commons.net.io.*;
+
+/**
+ * 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, logs debug info and errors to the console streams. This can be changed with the
+ * setLog and setLogError methods.
+ * 
+ * @author <A HREF="mailto:daniel.armbrust.list@gmail.com">Dan Armbrust</A>
+ */
+
+public class TFTPServer implements Runnable
+{
+	// Modes for the server. These should be an enum, in java 1.5
+	public static final int GET_ONLY = 0;
+	public static final int PUT_ONLY = 1;
+	public static final int GET_AND_PUT = 2;
+
+	private HashSet transfers_ = new HashSet();
+	private boolean shutdown_ = false;
+	private TFTP serverTftp_;
+	private File serverReadDirectory_;
+	private File serverWriteDirectory_;
+	private int port_;
+	private Exception serverException = null;
+	private int mode_;
+
+	// don't have access to a logger api, so we will log to these streams, which
+	// by default are set to the console.
+	private PrintStream log_;
+	private PrintStream logError_;
+
+	private int maxTimeoutRetries_ = 3;
+	private int socketTimeout_;
+
+	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);
+	}
+
+	/**
+	 * 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 serverDirectory
+	 * @param mode A value as specified above.
+	 * @throws IOException if the server directory is invalid or does not exist.
+	 */
+	public TFTPServer(File serverReadDirectory, File serverWriteDirectory, int mode)
+			throws IOException
+	{
+		this(serverReadDirectory, serverWriteDirectory, 69, mode, null, null);
+	}
+
+	/**
+	 * 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 serverDirectory
+	 * @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(File serverReadDirectory, File serverWriteDirectory, int port, int mode,
+			PrintStream log, PrintStream errorLog) throws IOException
+	{
+		port_ = port;
+		mode_ = mode;
+		log_ = (log == null ? System.out : log);
+		logError_ = (errorLog == null ? System.err : errorLog);
+		launch(serverReadDirectory, serverWriteDirectory);
+	}
+
+	/**
+	 * Set the max number of retries in response to a timeout. Default 3. Min 0.
+	 * 
+	 * @param retries
+	 */
+	public void setMaxTimeoutRetries(int retries)
+	{
+		if (retries < 0)
+		{
+			throw new RuntimeException("Invalid Value");
+		}
+		maxTimeoutRetries_ = retries;
+	}
+
+	/**
+	 * Get the current value for maxTimeoutRetries
+	 */
+	public int getMaxTimeoutRetries()
+	{
+		return maxTimeoutRetries_;
+	}
+
+	/**
+	 * Set the socket timeout in milliseconds used in transfers. Defaults to the value here:
+	 * http://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.
+	 */
+	public void setSocketTimeout(int timeout)
+	{
+		if (timeout < 10)
+		{
+			throw new RuntimeException("Invalid Value");
+		}
+		socketTimeout_ = timeout;
+	}
+
+	/**
+	 * The current socket timeout used during transfers in milliseconds.
+	 */
+	public int getSocketTimeout()
+	{
+		return socketTimeout_;
+	}
+
+	/*
+	 * start the server, throw an error if it can't start.
+	 */
+	private void launch(File serverReadDirectory, 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);
+
+		serverTftp_.open(port_);
+
+		Thread go = new Thread(this);
+		go.setDaemon(true);
+		go.start();
+	}
+
+	protected void finalize() throws Throwable
+	{
+		shutdown();
+	}
+
+	/**
+	 * 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 (shutdown_ && serverException != null)
+		{
+			throw serverException;
+		}
+		return !shutdown_;
+	}
+
+	public void run()
+	{
+		try
+		{
+			while (!shutdown_)
+			{
+				TFTPPacket tftpPacket;
+
+				tftpPacket = serverTftp_.receive();
+
+				TFTPTransfer tt = new TFTPTransfer(tftpPacket);
+				synchronized(transfers_)
+				{
+					transfers_.add(tt);
+				}
+
+				Thread thread = new Thread(tt);
+				thread.setDaemon(true);
+				thread.start();
+			}
+		}
+		catch (Exception e)
+		{
+			if (!shutdown_)
+			{
+				serverException = e;
+				logError_.println("Unexpected Error in TFTP Server - Server shut down! + " + e);
+			}
+		}
+		finally
+		{
+			shutdown_ = true; // set this to true, so the launching thread can check to see if it started.
+			if (serverTftp_ != null && serverTftp_.isOpen())
+			{
+				serverTftp_.close();
+			}
+		}
+	}
+
+	/**
+	 * Stop the tftp server (and any currently running transfers) and release all opened network
+	 * resources.
+	 */
+	public void shutdown()
+	{
+		shutdown_ = true;
+
+		synchronized(transfers_)
+		{
+			Iterator it = transfers_.iterator();
+			while (it.hasNext())
+			{
+				((TFTPTransfer) it.next()).shutdown();
+			}
+		}
+
+		try
+		{
+			serverTftp_.close();
+		}
+		catch (RuntimeException e)
+		{
+			// noop
+		}
+	}
+
+	/*
+	 * An instance of an ongoing transfer.
+	 */
+	private class TFTPTransfer implements Runnable
+	{
+		private TFTPPacket tftpPacket_;
+
+		private boolean shutdown_ = false;
+
+		TFTP transferTftp_ = null;
+
+		public TFTPTransfer(TFTPPacket tftpPacket)
+		{
+			tftpPacket_ = tftpPacket;
+		}
+
+		public void shutdown()
+		{
+			shutdown_ = true;
+			try
+			{
+				transferTftp_.close();
+			}
+			catch (RuntimeException e)
+			{
+				// noop
+			}
+		}
+
+		public void run()
+		{
+			try
+			{
+				transferTftp_ = new TFTP();
+
+				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 (Exception e)
+			{
+				if (!shutdown_)
+				{
+					logError_
+							.println("Unexpected Error in during TFTP file transfer.  Transfer aborted. "
+									+ e);
+				}
+			}
+			finally
+			{
+				try
+				{
+					if (transferTftp_ != null && transferTftp_.isOpen())
+					{
+						transferTftp_.endBufferedOps();
+						transferTftp_.close();
+					}
+				}
+				catch (Exception e)
+				{
+					// noop
+				}
+				synchronized(transfers_)
+				{
+					transfers_.remove(this);
+				}
+			}
+		}
+
+		/*
+		 * Handle a tftp read request.
+		 */
+		private void handleRead(TFTPReadRequestPacket trrp) throws IOException, TFTPPacketException
+		{
+			InputStream is = null;
+			try
+			{
+				if (mode_ == 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 (FileNotFoundException e)
+				{
+					transferTftp_.bufferedSend(new TFTPErrorPacket(trrp.getAddress(), trrp
+							.getPort(), TFTPErrorPacket.FILE_NOT_FOUND, e.getMessage()));
+					return;
+				}
+				catch (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);
+				}
+
+				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 && !shutdown_)
+				{
+					if (sendNext)
+					{
+						readLength = is.read(temp);
+						if (readLength == -1)
+						{
+							readLength = 0;
+						}
+
+						lastSentData = new TFTPDataPacket(trrp.getAddress(), trrp.getPort(), block,
+								temp, 0, readLength);
+						transferTftp_.bufferedSend(lastSentData);
+					}
+
+					answer = null;
+
+					int timeoutCount = 0;
+
+					while (!shutdown_
+							&& (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 (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 (!shutdown_)
+						{
+							logError_
+									.println("Unexpected response from tftp client during transfer ("
+											+ answer + ").  Transfer aborted.");
+						}
+						break;
+					}
+					else
+					{
+						// once we get here, we know we have an answer packet
+						// from the correct host.
+						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 (IOException e)
+				{
+					// noop
+				}
+			}
+		}
+
+		/*
+		 * handle a tftp write request.
+		 */
+		private void handleWrite(TFTPWriteRequestPacket twrp) throws IOException,
+				TFTPPacketException
+		{
+			OutputStream bos = null;
+			try
+			{
+				if (mode_ == GET_ONLY)
+				{
+					transferTftp_.bufferedSend(new TFTPErrorPacket(twrp.getAddress(), twrp
+							.getPort(), TFTPErrorPacket.ILLEGAL_OPERATION,
+							"Write not allowed by server."));
+					return;
+				}
+
+				int lastBlock = 0;
+				String fileName = twrp.getFilename();
+
+				try
+				{
+					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 (Exception e)
+				{
+					transferTftp_.bufferedSend(new TFTPErrorPacket(twrp.getAddress(), twrp
+							.getPort(), TFTPErrorPacket.UNDEFINED, e.getMessage()));
+					return;
+				}
+
+				TFTPAckPacket lastSentAck = new TFTPAckPacket(twrp.getAddress(), twrp.getPort(), 0);
+				transferTftp_.bufferedSend(lastSentAck);
+
+				while (true)
+				{
+					// get the response - ensure it is from the right place.
+					TFTPPacket dataPacket = null;
+
+					int timeoutCount = 0;
+
+					while (!shutdown_
+							&& (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 (SocketTimeoutException e)
+						{
+							if (timeoutCount >= maxTimeoutRetries_)
+							{
+								throw e;
+							}
+							// It didn't get our ack. Resend it.
+							transferTftp_.bufferedSend(lastSentAck);
+							timeoutCount++;
+							continue;
+						}
+					}
+
+					if (dataPacket != null && 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 (!shutdown_)
+						{
+							logError_
+									.println("Unexpected response from tftp client during transfer ("
+											+ dataPacket + ").  Transfer aborted.");
+						}
+						break;
+					}
+					else
+					{
+						int block = ((TFTPDataPacket) dataPacket).getBlockNumber();
+						byte[] data = ((TFTPDataPacket) dataPacket).getData();
+						int dataLength = ((TFTPDataPacket) dataPacket).getDataLength();
+						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);
+						transferTftp_.bufferedSend(lastSentAck);
+						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 (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();
+				}
+			}
+		}
+
+		/*
+		 * Utility method to make sure that paths provided by tftp clients do not get outside of the
+		 * serverRoot directory.
+		 */
+		private File buildSafeFile(File serverDirectory, String fileName, 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(File file) throws IOException
+		{
+			File parent = file.getParentFile();
+			if (parent == null)
+			{
+				throw new IOException("Unexpected error creating requested directory");
+			}
+			if (!parent.exists())
+			{
+				// recurse...
+				createDirectory(parent);
+			}
+
+			if (parent.isDirectory())
+			{
+				if (file.isDirectory())
+				{
+					return;
+				}
+				boolean result = file.mkdir();
+				if (!result)
+				{
+					throw new IOException("Couldn't create requested directory");
+				}
+			}
+			else
+			{
+				throw new IOException(
+						"Invalid directory path - file in the way of requested folder");
+			}
+		}
+
+		/*
+		 * recursively check to see if one directory is a parent of another.
+		 */
+		private boolean isSubdirectoryOf(File parent, File child)
+		{
+			File childsParent = child.getParentFile();
+			if (childsParent == null)
+			{
+				return false;
+			}
+			if (childsParent.equals(parent))
+			{
+				return true;
+			}
+			else
+			{
+				return isSubdirectoryOf(parent, childsParent);
+			}
+		}
+	}
+
+	/**
+	 * Set the stream object to log debug / informational messages. By default, this is System.out
+	 * 
+	 * @param log
+	 */
+	public void setLog(PrintStream log)
+	{
+		this.log_ = log;
+	}
+
+	/**
+	 * Set the stream object to log error messsages. By default, this is System.err
+	 * 
+	 * @param logError
+	 */
+	public void setLogError(PrintStream logError)
+	{
+		this.logError_ = logError;
+	}
+}

Added: commons/proper/net/trunk/src/test/org/apache/commons/net/tftp/TFTPServerPathTests.java
URL: http://svn.apache.org/viewvc/commons/proper/net/trunk/src/test/org/apache/commons/net/tftp/TFTPServerPathTests.java?rev=630685&view=auto
==============================================================================
--- commons/proper/net/trunk/src/test/org/apache/commons/net/tftp/TFTPServerPathTests.java (added)
+++ commons/proper/net/trunk/src/test/org/apache/commons/net/tftp/TFTPServerPathTests.java Sun Feb 24 13:41:05 2008
@@ -0,0 +1,148 @@
+package org.apache.commons.net.tftp;
+
+import java.io.*;
+import junit.framework.*;
+
+/**
+ * Some basic tests to ensure that the TFTP Server is honoring its read/write mode, and preventing
+ * files from being read or written from outside of the assigned roots.
+ * 
+ * @author <A HREF="mailto:daniel.armbrust.list@gmail.com">Dan Armbrust</A>
+ * 
+ */
+public class TFTPServerPathTests extends TestCase
+{
+	String filePrefix = "tftp-";
+	File serverDirectory = new File(System.getProperty("java.io.tmpdir"));
+
+	public void testReadOnly() throws IOException
+	{
+		// Start a read-only server
+		TFTPServer tftpS = new TFTPServer(serverDirectory, serverDirectory, 6900,
+				TFTPServer.GET_ONLY, null, null);
+
+		// Create our TFTP instance to handle the file transfer.
+		TFTPClient tftp = new TFTPClient();
+		tftp.open();
+		tftp.setSoTimeout(2000);
+
+		// make a file to work with.
+		File file = new File(serverDirectory, filePrefix + "source.txt");
+		file.createNewFile();
+
+		// Read the file from the tftp server.
+		File out = new File(serverDirectory, filePrefix + "out");
+
+		// cleanup old failed runs
+		out.delete();
+		assertTrue("Couldn't clear output location", !out.exists());
+
+		FileOutputStream output = new FileOutputStream(out);
+
+		tftp.receiveFile(file.getName(), TFTP.BINARY_MODE, output, "localhost", 6900);
+		output.close();
+
+		assertTrue("file not created", out.exists());
+
+		out.delete();
+
+		FileInputStream fis = new FileInputStream(file);
+		try
+		{
+			tftp.sendFile(out.getName(), TFTP.BINARY_MODE, fis, "localhost", 6900);
+			fail("Server allowed write");
+		}
+		catch (IOException e)
+		{
+			// expected path
+		}
+		fis.close();
+		file.delete();
+		tftpS.shutdown();
+	}
+
+	public void testWriteOnly() throws IOException
+	{
+		// Start a write-only server
+		TFTPServer tftpS = new TFTPServer(serverDirectory, serverDirectory, 6900,
+				TFTPServer.PUT_ONLY, null, null);
+
+		// Create our TFTP instance to handle the file transfer.
+		TFTPClient tftp = new TFTPClient();
+		tftp.open();
+		tftp.setSoTimeout(2000);
+
+		// make a file to work with.
+		File file = new File(serverDirectory, filePrefix + "source.txt");
+		file.createNewFile();
+
+		File out = new File(serverDirectory, filePrefix + "out");
+
+		// cleanup old failed runs
+		out.delete();
+		assertTrue("Couldn't clear output location", !out.exists());
+
+		FileOutputStream output = new FileOutputStream(out);
+
+		try
+		{
+			tftp.receiveFile(file.getName(), TFTP.BINARY_MODE, output, "localhost", 6900);
+			fail("Server allowed read");
+		}
+		catch (IOException e)
+		{
+			// expected path
+		}
+		output.close();
+		out.delete();
+
+		FileInputStream fis = new FileInputStream(file);
+		tftp.sendFile(out.getName(), TFTP.BINARY_MODE, fis, "localhost", 6900);
+
+		fis.close();
+
+		assertTrue("file not created", out.exists());
+
+		// cleanup
+		file.delete();
+		out.delete();
+		tftpS.shutdown();
+	}
+
+	public void testWriteOutsideHome() throws IOException
+	{
+		// Start a server
+		TFTPServer tftpS = new TFTPServer(serverDirectory, serverDirectory, 6900,
+				TFTPServer.GET_AND_PUT, null, null);
+
+		// Create our TFTP instance to handle the file transfer.
+		TFTPClient tftp = new TFTPClient();
+		tftp.open();
+
+		File file = new File(serverDirectory, filePrefix + "source.txt");
+		file.createNewFile();
+
+		assertFalse("test construction error", new File(serverDirectory, "../foo").exists());
+
+		FileInputStream fis = new FileInputStream(file);
+		try
+		{
+			tftp.sendFile("../foo", TFTP.BINARY_MODE, fis, "localhost", 6900);
+			fail("Server allowed write!");
+		}
+		catch (IOException e)
+		{
+			// expected path
+		}
+
+		fis.close();
+
+		assertFalse("file created when it should not have been",
+				new File(serverDirectory, "../foo").exists());
+
+		// cleanup
+		file.delete();
+
+		tftpS.shutdown();
+	}
+}

Added: commons/proper/net/trunk/src/test/org/apache/commons/net/tftp/TFTPTests.java
URL: http://svn.apache.org/viewvc/commons/proper/net/trunk/src/test/org/apache/commons/net/tftp/TFTPTests.java?rev=630685&view=auto
==============================================================================
--- commons/proper/net/trunk/src/test/org/apache/commons/net/tftp/TFTPTests.java (added)
+++ commons/proper/net/trunk/src/test/org/apache/commons/net/tftp/TFTPTests.java Sun Feb 24 13:41:05 2008
@@ -0,0 +1,219 @@
+package org.apache.commons.net.tftp;
+
+import java.io.*;
+import junit.framework.TestCase;
+import org.apache.commons.net.tftp.TFTP;
+import org.apache.commons.net.tftp.TFTPClient;
+
+/**
+ * Test the TFTP Server and TFTP Client by creating some files in the system temp folder and then
+ * uploading and downloading them.
+ * 
+ * @author <A HREF="mailto:daniel.armbrust.list@gmail.com">Dan Armbrust</A>
+ * 
+ */
+public class TFTPTests extends TestCase
+{
+	static TFTPServer tftpS;
+	static File serverDirectory = new File(System.getProperty("java.io.tmpdir"));
+	static String filePrefix = "tftp-";
+	static File[] files = new File[8];
+
+	static int testsLeftToRun = 6;
+
+	// only want to do this once...
+	static
+	{
+		try
+		{
+			files[0] = createFile(new File(serverDirectory, filePrefix + "empty.txt"), 0);
+			files[1] = createFile(new File(serverDirectory, filePrefix + "small.txt"), 1);
+			files[2] = createFile(new File(serverDirectory, filePrefix + "511.txt"), 511);
+			files[3] = createFile(new File(serverDirectory, filePrefix + "512.txt"), 512);
+			files[4] = createFile(new File(serverDirectory, filePrefix + "513.txt"), 513);
+			files[5] = createFile(new File(serverDirectory, filePrefix + "med.txt"), 1000 * 1024);
+			files[6] = createFile(new File(serverDirectory, filePrefix + "big.txt"), 5000 * 1024);
+			files[7] = createFile(new File(serverDirectory, filePrefix + "huge.txt"), 37000 * 1024);
+
+			// Start the server
+			tftpS = new TFTPServer(serverDirectory, serverDirectory, 6900, TFTPServer.GET_AND_PUT,
+					null, null);
+			tftpS.setSocketTimeout(2000);
+		}
+		catch (IOException e)
+		{
+			e.printStackTrace();
+		}
+
+	}
+
+	protected void tearDown() throws Exception
+	{
+		testsLeftToRun--;
+		if (testsLeftToRun <= 0)
+		{
+			if (tftpS != null)
+			{
+				tftpS.shutdown();
+			}
+			for (int i = 0; i < files.length; i++)
+			{
+				files[i].delete();
+			}
+		}
+		super.tearDown();
+	}
+
+	/*
+	 * Create a file, size specified in bytes
+	 */
+	private static File createFile(File file, int size) throws IOException
+	{
+		OutputStream os = new BufferedOutputStream(new FileOutputStream(file));
+		byte[] temp = "0".getBytes();
+		for (int i = 0; i < size; i++)
+		{
+			os.write(temp);
+		}
+		os.close();
+		return file;
+	}
+
+	public void testTFTPBinaryDownloads() throws Exception
+	{
+		// test with the smaller files
+		for (int i = 0; i < 6; i++)
+		{
+			testDownload(TFTP.BINARY_MODE, files[i]);
+		}
+	}
+
+	public void testASCIIDownloads() throws Exception
+	{
+		// test with the smaller files
+		for (int i = 0; i < 6; i++)
+		{
+			testDownload(TFTP.ASCII_MODE, files[i]);
+		}
+	}
+
+	public void testTFTPBinaryUploads() throws Exception
+	{
+		// test with the smaller files
+		for (int i = 0; i < 6; i++)
+		{
+			testUpload(TFTP.BINARY_MODE, files[i]);
+		}
+	}
+
+	public void testASCIIUploads() throws Exception
+	{
+		// test with the smaller files
+		for (int i = 0; i < 6; i++)
+		{
+			testUpload(TFTP.ASCII_MODE, files[i]);
+		}
+	}
+
+	public void testHugeUploads() throws Exception
+	{
+		for (int i = 5; i < files.length; i++)
+		{
+			testUpload(TFTP.BINARY_MODE, files[i]);
+		}
+	}
+
+	public void testHugeDownloads() throws Exception
+	{
+		// test with the smaller files
+		for (int i = 5; i < files.length; i++)
+		{
+			testDownload(TFTP.BINARY_MODE, files[i]);
+		}
+	}
+
+	private void testDownload(int mode, File file) throws IOException
+	{
+		// Create our TFTP instance to handle the file transfer.
+		TFTPClient tftp = new TFTPClient();
+		tftp.open();
+		tftp.setSoTimeout(2000);
+
+		File out = new File(serverDirectory, filePrefix + "download");
+
+		// cleanup old failed runs
+		out.delete();
+		assertTrue("Couldn't clear output location", !out.exists());
+
+		FileOutputStream output = new FileOutputStream(out);
+
+		tftp.receiveFile(file.getName(), mode, output, "localhost", 6900);
+		output.close();
+
+		assertTrue("file not created", out.exists());
+		assertTrue("files not identical on file " + file, filesIdentical(out, file));
+
+		// delete the downloaded file
+		out.delete();
+	}
+
+	private void testUpload(int mode, File file) throws Exception
+	{
+		// Create our TFTP instance to handle the file transfer.
+		TFTPClient tftp = new TFTPClient();
+		tftp.open();
+		tftp.setSoTimeout(2000);
+
+		File in = new File(serverDirectory, filePrefix + "upload");
+		// cleanup old failed runs
+		in.delete();
+		assertTrue("Couldn't clear output location", !in.exists());
+
+		FileInputStream fis = new FileInputStream(file);
+		tftp.sendFile(in.getName(), mode, fis, "localhost", 6900);
+		fis.close();
+
+		// need to give the server a bit of time to receive our last packet, and
+		// close out its file buffers, etc.
+		Thread.sleep(100);
+		assertTrue("file not created", in.exists());
+		assertTrue("files not identical on file " + file, filesIdentical(file, in));
+
+		in.delete();
+	}
+
+	private boolean filesIdentical(File a, File b) throws IOException
+	{
+		if (!a.exists() || !b.exists())
+		{
+			return false;
+		}
+
+		if (a.length() != b.length())
+		{
+			return false;
+		}
+
+		InputStream fisA = new BufferedInputStream(new FileInputStream(a));
+		InputStream fisB = new BufferedInputStream(new FileInputStream(b));
+
+		int aBit = fisA.read();
+		int bBit = fisB.read();
+
+		while (aBit != -1)
+		{
+			if (aBit != bBit)
+			{
+				fisA.close();
+				fisB.close();
+				return false;
+			}
+			aBit = fisA.read();
+			bBit = fisB.read();
+		}
+
+		fisA.close();
+		fisB.close();
+		return true;
+	}
+}