TFTPServer.java
/*
* 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
*
* https://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.time.Duration;
import java.util.Enumeration;
import java.util.HashSet;
import org.apache.commons.io.IOUtils;
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);
* }
*
* try (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();
* }
*
* System.out.println("Server shut down.");
* System.exit(0);
* }
* </code>
*
* @since 2.0
*/
public class TFTPServer implements Runnable, AutoCloseable {
public enum ServerMode {
GET_ONLY, PUT_ONLY, GET_AND_PUT
}
/*
* An ongoing transfer.
*/
private final class TFTPTransfer implements Runnable {
private final TFTPPacket tftpPacket;
private boolean shutdownTransfer;
TFTP transferTftp;
public TFTPTransfer(final TFTPPacket tftpPacket) {
this.tftpPacket = tftpPacket;
}
/*
* Makes 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 {
final File temp = new File(serverDirectory, fileName).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;
}
/*
* Creates subdirectories recursively.
*/
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");
}
}
/*
* Handles a tftp read request.
*/
private void handleRead(final TFTPReadRequestPacket trrp) throws IOException, TFTPPacketException {
if (mode == ServerMode.PUT_ONLY) {
transferTftp
.bufferedSend(new TFTPErrorPacket(trrp.getAddress(), trrp.getPort(), TFTPErrorPacket.ILLEGAL_OPERATION, "Read not allowed by server."));
return;
}
InputStream inputStream = null;
try {
try {
inputStream = 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) {
inputStream = new ToNetASCIIInputStream(inputStream);
}
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 = inputStream.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 {
IOUtils.closeQuietly(inputStream);
}
}
/*
* 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 {
IOUtils.close(bos);
}
}
/*
* 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 localAddress;
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 localAddress 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 localAddress, final ServerMode mode,
final PrintStream log, final PrintStream errorLog) throws IOException {
this.port = port;
this.mode = mode;
this.localAddress = localAddress;
this.log = log == null ? nullStream : log;
this.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 {
this.mode = mode;
this.port = port;
InetAddress inetAddress = null;
if (localiface != null) {
final Enumeration<InetAddress> ifaddrs = localiface.getInetAddresses();
if (ifaddrs != null && ifaddrs.hasMoreElements()) {
inetAddress = ifaddrs.nextElement();
}
}
this.log = log == null ? nullStream : log;
this.logError = errorLog == null ? nullStream : errorLog;
this.localAddress = inetAddress;
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 {
this.port = port;
this.mode = mode;
this.log = log == null ? nullStream : log;
this.logError = errorLog == null ? nullStream : errorLog;
this.localAddress = 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);
}
/**
* Closes the TFTP server (and any currently running transfers) and release all opened network resources.
*
* @since 3.10.0
*/
@Override
public void close() {
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
}
}
@Override
protected void finalize() throws Throwable {
close();
super.finalize();
}
/**
* Gets the current value for maxTimeoutRetries
*
* @return the max allowed number of retries
*/
public int getMaxTimeoutRetries() {
return maxTimeoutRetries;
}
/**
* Gets the server port number
*
* @return the server port number
*/
public int getPort() {
return port;
}
/**
* Gets 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 newServerReadDirectory, final File newServerWriteDirectory) throws IOException {
log.println("Starting TFTP Server on port " + port + ". Read directory: " + newServerReadDirectory + " Write directory: " + newServerWriteDirectory
+ " Server Mode is " + mode);
serverReadDirectory = newServerReadDirectory.getCanonicalFile();
if (!serverReadDirectory.exists() || !newServerReadDirectory.isDirectory()) {
throw new IOException("The server read directory " + serverReadDirectory + " does not exist");
}
serverWriteDirectory = newServerWriteDirectory.getCanonicalFile();
if (!serverWriteDirectory.exists() || !newServerWriteDirectory.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(Duration.ZERO);
if (localAddress != null) {
serverTftp.open(port, localAddress);
} else {
serverTftp.open(port);
}
serverThread = new Thread(this);
serverThread.setDaemon(true);
serverThread.start();
}
/*
* Allow test code to customize 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 customization of sending data/ack so can generate errors if needed
*/
void sendData(final TFTP tftp, final TFTPPacket data) throws IOException {
tftp.bufferedSend(data);
}
/**
* Sets 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;
}
/**
* Sets 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;
}
/**
* Sets the max number of retries in response to a timeout. Default 3. Min 0.
*
* @param retries number of retries, must be > 0
* @throws IllegalArgumentException if {@code retries} is less than 0.
*/
public void setMaxTimeoutRetries(final int retries) {
if (retries < 0) {
throw new IllegalArgumentException("Invalid Value");
}
maxTimeoutRetries = retries;
}
/**
* Sets the socket timeout in milliseconds used in transfers.
* <p>
* Defaults to the value {@link TFTP#DEFAULT_TIMEOUT}. Minimum value of 10.
* </p>
* @param timeout the timeout; must be equal to or larger than 10.
* @throws IllegalArgumentException if {@code timeout} is less than 10.
*/
public void setSocketTimeout(final int timeout) {
if (timeout < 10) {
throw new IllegalArgumentException("Invalid Value");
}
socketTimeout = timeout;
}
/**
* Closes the TFTP server (and any currently running transfers) and release all opened network resources.
*
* @deprecated Use {@link #close()}.
*/
@Deprecated
public void shutdown() {
close();
}
}