blob: 21f6fa848557b5efd2dc466898bfbc1ef7750abc [file] [log] [blame]
/*
* 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 &gt; 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
}
}
}