    001/*
    002 * Licensed to the Apache Software Foundation (ASF) under one or more
    003 * contributor license agreements.  See the NOTICE file distributed with
    004 * this work for additional information regarding copyright ownership.
    005 * The ASF licenses this file to You under the Apache License, Version 2.0
    006 * (the "License"); you may not use this file except in compliance with
    007 * the License.  You may obtain a copy of the License at
    008 *
    009 *      https://www.apache.org/licenses/LICENSE-2.0
    010 *
    011 * Unless required by applicable law or agreed to in writing, software
    012 * distributed under the License is distributed on an "AS IS" BASIS,
    013 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    014 * See the License for the specific language governing permissions and
    015 * limitations under the License.
    016 */
    017
    018package org.apache.commons.net.tftp;
    019
    020import java.io.IOException;
    021import java.io.InputStream;
    022import java.io.InterruptedIOException;
    023import java.io.OutputStream;
    024import java.net.InetAddress;
    025import java.net.SocketException;
    026import java.net.UnknownHostException;
    027
    028import org.apache.commons.net.io.FromNetASCIIOutputStream;
    029import org.apache.commons.net.io.ToNetASCIIInputStream;
    030
    031/**
    032 * The TFTPClient class encapsulates all the aspects of the TFTP protocol necessary to receive and send files through TFTP. It is derived from the
    033 * {@link org.apache.commons.net.tftp.TFTP} because it is more convenient than using aggregation, and as a result exposes the same set of methods to allow you
    034 * to deal with the TFTP protocol directly. However, almost every user should only be concerend with the the
    035 * {@link org.apache.commons.net.DatagramSocketClient#open open()}, {@link org.apache.commons.net.DatagramSocketClient#close close()}, {@link #sendFile
    036 * sendFile()}, and {@link #receiveFile receiveFile()} methods. Additionally, the {@link #setMaxTimeouts setMaxTimeouts()} and
    037 * {@link org.apache.commons.net.DatagramSocketClient#setDefaultTimeout setDefaultTimeout()} methods may be of importance for performance tuning.
    038 * <p>
    039 * Details regarding the TFTP protocol and the format of TFTP packets can be found in RFC 783. But the point of these classes is to keep you from having to
    040 * worry about the internals.
    041 * </p>
    042 *
    043 * @see TFTP
    044 * @see TFTPPacket
    045 * @see TFTPPacketException
    046 */
    047
    048public class TFTPClient extends TFTP {
    049    /**
    050     * The default number of times a {@code receive} attempt is allowed to timeout before ending attempts to retry the {@code receive} and failing.
    051     * The default is 5 timeouts.
    052     */
    053    public static final int DEFAULT_MAX_TIMEOUTS = 5;
    054
    055    /** The maximum number of timeouts allowed before failing. */
    056    private int maxTimeouts;
    057
    058    /** The number of bytes received in the ongoing download. */
    059    private long totalBytesReceived;
    060
    061    /** The number of bytes sent in the ongoing upload. */
    062    private long totalBytesSent;
    063
    064    /**
    065     * Creates a TFTPClient instance with a default timeout of DEFAULT_TIMEOUT, maximum timeouts value of DEFAULT_MAX_TIMEOUTS, a null socket, and buffered
    066     * operations disabled.
    067     */
    068    public TFTPClient() {
    069        maxTimeouts = DEFAULT_MAX_TIMEOUTS;
    070    }
    071
    072    /**
    073     * Gets the maximum number of times a {@code receive} attempt is allowed to timeout before ending attempts to retry the {@code receive} and failing.
    074     *
    075     * @return The maximum number of timeouts allowed.
    076     */
    077    public int getMaxTimeouts() {
    078        return maxTimeouts;
    079    }
    080
    081    /**
    082     * Gets the number of bytes received in the ongoing download.
    083     *
    084     * @return The number of bytes received in the ongoing download.
    085     */
    086    public long getTotalBytesReceived() {
    087        return totalBytesReceived;
    088    }
    089
    090    /**
    091     * Gets the number of bytes sent in the ongoing download.
    092     *
    093     * @return The number of bytes sent in the ongoing download.
    094     */
    095    public long getTotalBytesSent() {
    096        return totalBytesSent;
    097    }
    098
    099    /**
    100     * Same as calling receiveFile(fileName, mode, output, host, TFTP.DEFAULT_PORT).
    101     *
    102     * @param fileName The name of the file to receive.
    103     * @param mode     The TFTP mode of the transfer (one of the MODE constants).
    104     * @param output   The OutputStream to which the file should be written.
    105     * @param host     The remote host serving the file.
    106     * @return number of bytes read
    107     * @throws IOException If an I/O error occurs. The nature of the error will be reported in the message.
    108     */
    109    public int receiveFile(final String fileName, final int mode, final OutputStream output, final InetAddress host) throws IOException {
    110        return receiveFile(fileName, mode, output, host, DEFAULT_PORT);
    111    }
    112
    113    /**
    114     * Requests a named file from a remote host, writes the file to an OutputStream, closes the connection, and returns the number of bytes read. A local UDP
    115     * socket must first be created by {@link org.apache.commons.net.DatagramSocketClient#open open()} before invoking this method. This method will not close
    116     * the OutputStream containing the file; you must close it after the method invocation.
    117     *
    118     * @param fileName The name of the file to receive.
    119     * @param mode     The TFTP mode of the transfer (one of the MODE constants).
    120     * @param output   The OutputStream to which the file should be written.
    121     * @param host     The remote host serving the file.
    122     * @param port     The port number of the remote TFTP server.
    123     * @return number of bytes read
    124     * @throws IOException If an I/O error occurs. The nature of the error will be reported in the message.
    125     */
    126    public int receiveFile(final String fileName, final int mode, OutputStream output, InetAddress host, final int port) throws IOException {
    127        int bytesRead = 0;
    128        int lastBlock = 0;
    129        int block = 1;
    130        int hostPort = 0;
    131        int dataLength = 0;
    132
    133        totalBytesReceived = 0;
    134
    135        if (mode == ASCII_MODE) {
    136            output = new FromNetASCIIOutputStream(output);
    137        }
    138
    139        TFTPPacket sent = new TFTPReadRequestPacket(host, port, fileName, mode);
    140        final TFTPAckPacket ack = new TFTPAckPacket(host, port, 0);
    141
    142        beginBufferedOps();
    143
    144        boolean justStarted = true;
    145        try {
    146            do { // while more data to fetch
    147                bufferedSend(sent); // start the fetch/send an ack
    148                boolean wantReply = true;
    149                int timeouts = 0;
    150                do { // until successful response
    151                    try {
    152                        final TFTPPacket received = bufferedReceive();
    153                        // The first time we receive we get the port number and
    154                        // answering host address (for hosts with multiple IPs)
    155                        final int recdPort = received.getPort();
    156                        final InetAddress recdAddress = received.getAddress();
    157                        if (justStarted) {
    158                            justStarted = false;
    159                            if (recdPort == port) { // must not use the control port here
    160                                final TFTPErrorPacket error = new TFTPErrorPacket(recdAddress, recdPort, TFTPErrorPacket.UNKNOWN_TID, "INCORRECT SOURCE PORT");
    161                                bufferedSend(error);
    162                                throw new IOException("Incorrect source port (" + recdPort + ") in request reply.");
    163                            }
    164                            hostPort = recdPort;
    165                            ack.setPort(hostPort);
    166                            if (!host.equals(recdAddress)) {
    167                                host = recdAddress;
    168                                ack.setAddress(host);
    169                                sent.setAddress(host);
    170                            }
    171                        }
    172                        // Comply with RFC 783 indication that an error acknowledgment
    173                        // should be sent to originator if unexpected TID or host.
    174                        if (host.equals(recdAddress) && recdPort == hostPort) {
    175                            switch (received.getType()) {
    176
    177                            case TFTPPacket.ERROR:
    178                                TFTPErrorPacket error = (TFTPErrorPacket) received;
    179                                throw new IOException("Error code " + error.getError() + " received: " + error.getMessage());
    180                            case TFTPPacket.DATA:
    181                                final TFTPDataPacket data = (TFTPDataPacket) received;
    182                                dataLength = data.getDataLength();
    183                                lastBlock = data.getBlockNumber();
    184
    185                                if (lastBlock == block) { // is the next block number?
    186                                    try {
    187                                        output.write(data.getData(), data.getDataOffset(), dataLength);
    188                                    } catch (final IOException e) {
    189                                        error = new TFTPErrorPacket(host, hostPort, TFTPErrorPacket.OUT_OF_SPACE, "File write failed.");
    190                                        bufferedSend(error);
    191                                        throw e;
    192                                    }
    193                                    ++block;
    194                                    if (block > 65535) {
    195                                        // wrap the block number
    196                                        block = 0;
    197                                    }
    198                                    wantReply = false; // got the next block, drop out to ack it
    199                                } else { // unexpected block number
    200                                    discardPackets();
    201                                    if (lastBlock == (block == 0 ? 65535 : block - 1)) {
    202                                        wantReply = false; // Resend last acknowledgemen
    203                                    }
    204                                }
    205                                break;
    206
    207                            default:
    208                                throw new IOException("Received unexpected packet type (" + received.getType() + ")");
    209                            }
    210                        } else { // incorrect host or TID
    211                            final TFTPErrorPacket error = new TFTPErrorPacket(recdAddress, recdPort, TFTPErrorPacket.UNKNOWN_TID, "Unexpected host or port.");
    212                            bufferedSend(error);
    213                        }
    214                    } catch (final SocketException | InterruptedIOException e) {
    215                        if (++timeouts >= maxTimeouts) {
    216                            throw new IOException("Connection timed out.");
    217                        }
    218                    } catch (final TFTPPacketException e) {
    219                        throw new IOException("Bad packet: " + e.getMessage());
    220                    }
    221                } while (wantReply); // waiting for response
    222
    223                ack.setBlockNumber(lastBlock);
    224                sent = ack;
    225                bytesRead += dataLength;
    226                totalBytesReceived += dataLength;
    227            } while (dataLength == TFTPPacket.SEGMENT_SIZE); // not eof
    228            bufferedSend(sent); // send the final ack
    229        } finally {
    230            endBufferedOps();
    231        }
    232        return bytesRead;
    233    }
    234
    235    /**
    236     * Same as calling receiveFile(fileName, mode, output, hostname, TFTP.DEFAULT_PORT).
    237     *
    238     * @param fileName The name of the file to receive.
    239     * @param mode     The TFTP mode of the transfer (one of the MODE constants).
    240     * @param output   The OutputStream to which the file should be written.
    241     * @param hostname The name of the remote host serving the file.
    242     * @return number of bytes read
    243     * @throws IOException          If an I/O error occurs. The nature of the error will be reported in the message.
    244     * @throws UnknownHostException If the hostname cannot be resolved.
    245     */
    246    public int receiveFile(final String fileName, final int mode, final OutputStream output, final String hostname) throws UnknownHostException, IOException {
    247        return receiveFile(fileName, mode, output, InetAddress.getByName(hostname), DEFAULT_PORT);
    248    }
    249
    250    /**
    251     * Requests a named file from a remote host, writes the file to an OutputStream, closes the connection, and returns the number of bytes read. A local UDP
    252     * socket must first be created by {@link org.apache.commons.net.DatagramSocketClient#open open()} before invoking this method. This method will not close
    253     * the OutputStream containing the file; you must close it after the method invocation.
    254     *
    255     * @param fileName The name of the file to receive.
    256     * @param mode     The TFTP mode of the transfer (one of the MODE constants).
    257     * @param output   The OutputStream to which the file should be written.
    258     * @param hostname The name of the remote host serving the file.
    259     * @param port     The port number of the remote TFTP server.
    260     * @return number of bytes read
    261     * @throws IOException          If an I/O error occurs. The nature of the error will be reported in the message.
    262     * @throws UnknownHostException If the hostname cannot be resolved.
    263     */
    264    public int receiveFile(final String fileName, final int mode, final OutputStream output, final String hostname, final int port)
    265            throws UnknownHostException, IOException {
    266        return receiveFile(fileName, mode, output, InetAddress.getByName(hostname), port);
    267    }
    268
    269    /**
    270     * Same as calling sendFile(fileName, mode, input, host, TFTP.DEFAULT_PORT).
    271     *
    272     * @param fileName The name the remote server should use when creating the file on its file system.
    273     * @param mode     The TFTP mode of the transfer (one of the MODE constants).
    274     * @param input    the input stream containing the data to be sent
    275     * @param host     The name of the remote host receiving the file.
    276     * @throws IOException          If an I/O error occurs. The nature of the error will be reported in the message.
    277     * @throws UnknownHostException If the hostname cannot be resolved.
    278     */
    279    public void sendFile(final String fileName, final int mode, final InputStream input, final InetAddress host) throws IOException {
    280        sendFile(fileName, mode, input, host, DEFAULT_PORT);
    281    }
    282
    283    /**
    284     * Requests to send a file to a remote host, reads the file from an InputStream, sends the file to the remote host, and closes the connection. A local UDP
    285     * socket must first be created by {@link org.apache.commons.net.DatagramSocketClient#open open()} before invoking this method. This method will not close
    286     * the InputStream containing the file; you must close it after the method invocation.
    287     *
    288     * @param fileName The name the remote server should use when creating the file on its file system.
    289     * @param mode     The TFTP mode of the transfer (one of the MODE constants).
    290     * @param input    the input stream containing the data to be sent
    291     * @param host     The remote host receiving the file.
    292     * @param port     The port number of the remote TFTP server.
    293     * @throws IOException If an I/O error occurs. The nature of the error will be reported in the message.
    294     */
    295    public void sendFile(final String fileName, final int mode, InputStream input, InetAddress host, final int port) throws IOException {
    296        int block = 0;
    297        int hostPort = 0;
    298        boolean justStarted = true;
    299        boolean lastAckWait = false;
    300
    301        totalBytesSent = 0L;
    302
    303        if (mode == ASCII_MODE) {
    304            input = new ToNetASCIIInputStream(input);
    305        }
    306
    307        TFTPPacket sent = new TFTPWriteRequestPacket(host, port, fileName, mode);
    308        final TFTPDataPacket data = new TFTPDataPacket(host, port, 0, sendBuffer, 4, 0);
    309
    310        beginBufferedOps();
    311
    312        try {
    313            do { // until eof
    314                 // first time: block is 0, lastBlock is 0, send a request packet.
    315                 // subsequent: block is integer starting at 1, send data packet.
    316                bufferedSend(sent);
    317                boolean wantReply = true;
    318                int timeouts = 0;
    319                do {
    320                    try {
    321                        final TFTPPacket received = bufferedReceive();
    322                        final InetAddress recdAddress = received.getAddress();
    323                        final int recdPort = received.getPort();
    324                        // The first time we receive we get the port number and
    325                        // answering host address (for hosts with multiple IPs)
    326                        if (justStarted) {
    327                            justStarted = false;
    328                            if (recdPort == port) { // must not use the control port here
    329                                final TFTPErrorPacket error = new TFTPErrorPacket(recdAddress, recdPort, TFTPErrorPacket.UNKNOWN_TID, "INCORRECT SOURCE PORT");
    330                                bufferedSend(error);
    331                                throw new IOException("Incorrect source port (" + recdPort + ") in request reply.");
    332                            }
    333                            hostPort = recdPort;
    334                            data.setPort(hostPort);
    335                            if (!host.equals(recdAddress)) {
    336                                host = recdAddress;
    337                                data.setAddress(host);
    338                                sent.setAddress(host);
    339                            }
    340                        }
    341                        // Comply with RFC 783 indication that an error acknowledgment
    342                        // should be sent to originator if unexpected TID or host.
    343                        if (host.equals(recdAddress) && recdPort == hostPort) {
    344
    345                            switch (received.getType()) {
    346                            case TFTPPacket.ERROR:
    347                                final TFTPErrorPacket error = (TFTPErrorPacket) received;
    348                                throw new IOException("Error code " + error.getError() + " received: " + error.getMessage());
    349                            case TFTPPacket.ACKNOWLEDGEMENT:
    350
    351                                final int lastBlock = ((TFTPAckPacket) received).getBlockNumber();
    352
    353                                if (lastBlock == block) {
    354                                    ++block;
    355                                    if (block > 65535) {
    356                                        // wrap the block number
    357                                        block = 0;
    358                                    }
    359                                    wantReply = false; // got the ack we want
    360                                } else {
    361                                    discardPackets();
    362                                }
    363                                break;
    364                            default:
    365                                throw new IOException("Received unexpected packet type.");
    366                            }
    367                        } else { // wrong host or TID; send error
    368                            final TFTPErrorPacket error = new TFTPErrorPacket(recdAddress, recdPort, TFTPErrorPacket.UNKNOWN_TID, "Unexpected host or port.");
    369                            bufferedSend(error);
    370                        }
    371                    } catch (final SocketException | InterruptedIOException e) {
    372                        if (++timeouts >= maxTimeouts) {
    373                            throw new IOException("Connection timed out.");
    374                        }
    375                    } catch (final TFTPPacketException e) {
    376                        throw new IOException("Bad packet: " + e.getMessage());
    377                    }
    378                    // retry until a good ack
    379                } while (wantReply);
    380
    381                if (lastAckWait) {
    382                    break; // we were waiting for this; now all done
    383                }
    384
    385                int dataLength = TFTPPacket.SEGMENT_SIZE;
    386                int offset = 4;
    387                int totalThisPacket = 0;
    388                int bytesRead = 0;
    389                while (dataLength > 0 && (bytesRead = input.read(sendBuffer, offset, dataLength)) > 0) {
    390                    offset += bytesRead;
    391                    dataLength -= bytesRead;
    392                    totalThisPacket += bytesRead;
    393                }
    394                if (totalThisPacket < TFTPPacket.SEGMENT_SIZE) {
    395                    /* this will be our last packet -- send, wait for ack, stop */
    396                    lastAckWait = true;
    397                }
    398                data.setBlockNumber(block);
    399                data.setData(sendBuffer, 4, totalThisPacket);
    400                sent = data;
    401                totalBytesSent += totalThisPacket;
    402            } while (true); // loops until after lastAckWait is set
    403        } finally {
    404            endBufferedOps();
    405        }
    406    }
    407
    408    /**
    409     * Same as calling sendFile(fileName, mode, input, hostname, TFTP.DEFAULT_PORT).
    410     *
    411     * @param fileName The name the remote server should use when creating the file on its file system.
    412     * @param mode     The TFTP mode of the transfer (one of the MODE constants).
    413     * @param input    the input stream containing the data to be sent
    414     * @param hostname The name of the remote host receiving the file.
    415     * @throws IOException          If an I/O error occurs. The nature of the error will be reported in the message.
    416     * @throws UnknownHostException If the hostname cannot be resolved.
    417     */
    418    public void sendFile(final String fileName, final int mode, final InputStream input, final String hostname) throws UnknownHostException, IOException {
    419        sendFile(fileName, mode, input, InetAddress.getByName(hostname), DEFAULT_PORT);
    420    }
    421
    422    /**
    423     * Requests to send a file to a remote host, reads the file from an InputStream, sends the file to the remote host, and closes the connection. A local UDP
    424     * socket must first be created by {@link org.apache.commons.net.DatagramSocketClient#open open()} before invoking this method. This method will not close
    425     * the InputStream containing the file; you must close it after the method invocation.
    426     *
    427     * @param fileName The name the remote server should use when creating the file on its file system.
    428     * @param mode     The TFTP mode of the transfer (one of the MODE constants).
    429     * @param input    the input stream containing the data to be sent
    430     * @param hostname The name of the remote host receiving the file.
    431     * @param port     The port number of the remote TFTP server.
    432     * @throws IOException          If an I/O error occurs. The nature of the error will be reported in the message.
    433     * @throws UnknownHostException If the hostname cannot be resolved.
    434     */
    435    public void sendFile(final String fileName, final int mode, final InputStream input, final String hostname, final int port)
    436            throws UnknownHostException, IOException {
    437        sendFile(fileName, mode, input, InetAddress.getByName(hostname), port);
    438    }
    439
    440    /**
    441     * Sets the maximum number of times a {@code receive} attempt is allowed to timeout during a receiveFile() or sendFile() operation before ending
    442     * attempts to retry the {@code receive} and failing. The default is DEFAULT_MAX_TIMEOUTS.
    443     *
    444     * @param numTimeouts The maximum number of timeouts to allow. Values less than 1 should not be used, but if they are, they are treated as 1.
    445     */
    446    public void setMaxTimeouts(final int numTimeouts) {
    447        maxTimeouts = Math.max(numTimeouts, 1);
    448    }
    449}