    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    /**
    051     * The default number of times a {@code receive} attempt is allowed to timeout before ending attempts to retry the {@code receive} and failing.
    052     * The default is 5 timeouts.
    053     */
    054    public static final int DEFAULT_MAX_TIMEOUTS = 5;
    055
    056    /** The maximum number of timeouts allowed before failing. */
    057    private int maxTimeouts;
    058
    059    /** The number of bytes received in the ongoing download. */
    060    private long totalBytesReceived;
    061
    062    /** The number of bytes sent in the ongoing upload. */
    063    private long totalBytesSent;
    064
    065    /**
    066     * Creates a TFTPClient instance with a default timeout of DEFAULT_TIMEOUT, maximum timeouts value of DEFAULT_MAX_TIMEOUTS, a null socket, and buffered
    067     * operations disabled.
    068     */
    069    public TFTPClient() {
    070        maxTimeouts = DEFAULT_MAX_TIMEOUTS;
    071    }
    072
    073    /**
    074     * Gets the maximum number of times a {@code receive} attempt is allowed to timeout before ending attempts to retry the {@code receive} and failing.
    075     *
    076     * @return The maximum number of timeouts allowed.
    077     */
    078    public int getMaxTimeouts() {
    079        return maxTimeouts;
    080    }
    081
    082    /**
    083     * Gets the number of bytes received in the ongoing download.
    084     *
    085     * @return The number of bytes received in the ongoing download.
    086     */
    087    public long getTotalBytesReceived() {
    088        return totalBytesReceived;
    089    }
    090
    091    /**
    092     * Gets the number of bytes sent in the ongoing download.
    093     *
    094     * @return The number of bytes sent in the ongoing download.
    095     */
    096    public long getTotalBytesSent() {
    097        return totalBytesSent;
    098    }
    099
    100    /**
    101     * Same as calling receiveFile(fileName, mode, output, host, TFTP.DEFAULT_PORT).
    102     *
    103     * @param fileName The name of the file to receive.
    104     * @param mode     The TFTP mode of the transfer (one of the MODE constants).
    105     * @param output   The OutputStream to which the file should be written.
    106     * @param host     The remote host serving the file.
    107     * @return number of bytes read
    108     * @throws IOException If an I/O error occurs. The nature of the error will be reported in the message.
    109     */
    110    public int receiveFile(final String fileName, final int mode, final OutputStream output, final InetAddress host) throws IOException {
    111        return receiveFile(fileName, mode, output, host, DEFAULT_PORT);
    112    }
    113
    114    /**
    115     * 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
    116     * socket must first be created by {@link org.apache.commons.net.DatagramSocketClient#open open()} before invoking this method. This method will not close
    117     * the OutputStream containing the file; you must close it after the method invocation.
    118     *
    119     * @param fileName The name of the file to receive.
    120     * @param mode     The TFTP mode of the transfer (one of the MODE constants).
    121     * @param output   The OutputStream to which the file should be written.
    122     * @param host     The remote host serving the file.
    123     * @param port     The port number of the remote TFTP server.
    124     * @return number of bytes read
    125     * @throws IOException If an I/O error occurs. The nature of the error will be reported in the message.
    126     */
    127    public int receiveFile(final String fileName, final int mode, OutputStream output, InetAddress host, final int port) throws IOException {
    128        int bytesRead = 0;
    129        int lastBlock = 0;
    130        int block = 1;
    131        int hostPort = 0;
    132        int dataLength = 0;
    133
    134        totalBytesReceived = 0;
    135
    136        if (mode == ASCII_MODE) {
    137            output = new FromNetASCIIOutputStream(output);
    138        }
    139
    140        TFTPPacket sent = new TFTPReadRequestPacket(host, port, fileName, mode);
    141        final TFTPAckPacket ack = new TFTPAckPacket(host, port, 0);
    142
    143        beginBufferedOps();
    144
    145        boolean justStarted = true;
    146        try {
    147            do { // while more data to fetch
    148                bufferedSend(sent); // start the fetch/send an ack
    149                boolean wantReply = true;
    150                int timeouts = 0;
    151                do { // until successful response
    152                    try {
    153                        final TFTPPacket received = bufferedReceive();
    154                        // The first time we receive we get the port number and
    155                        // answering host address (for hosts with multiple IPs)
    156                        final int recdPort = received.getPort();
    157                        final InetAddress recdAddress = received.getAddress();
    158                        if (justStarted) {
    159                            justStarted = false;
    160                            if (recdPort == port) { // must not use the control port here
    161                                final TFTPErrorPacket error = new TFTPErrorPacket(recdAddress, recdPort, TFTPErrorPacket.UNKNOWN_TID, "INCORRECT SOURCE PORT");
    162                                bufferedSend(error);
    163                                throw new IOException("Incorrect source port (" + recdPort + ") in request reply.");
    164                            }
    165                            hostPort = recdPort;
    166                            ack.setPort(hostPort);
    167                            if (!host.equals(recdAddress)) {
    168                                host = recdAddress;
    169                                ack.setAddress(host);
    170                                sent.setAddress(host);
    171                            }
    172                        }
    173                        // Comply with RFC 783 indication that an error acknowledgment
    174                        // should be sent to originator if unexpected TID or host.
    175                        if (host.equals(recdAddress) && recdPort == hostPort) {
    176                            switch (received.getType()) {
    177
    178                            case TFTPPacket.ERROR:
    179                                TFTPErrorPacket error = (TFTPErrorPacket) received;
    180                                throw new IOException("Error code " + error.getError() + " received: " + error.getMessage());
    181                            case TFTPPacket.DATA:
    182                                final TFTPDataPacket data = (TFTPDataPacket) received;
    183                                dataLength = data.getDataLength();
    184                                lastBlock = data.getBlockNumber();
    185
    186                                if (lastBlock == block) { // is the next block number?
    187                                    try {
    188                                        output.write(data.getData(), data.getDataOffset(), dataLength);
    189                                    } catch (final IOException e) {
    190                                        error = new TFTPErrorPacket(host, hostPort, TFTPErrorPacket.OUT_OF_SPACE, "File write failed.");
    191                                        bufferedSend(error);
    192                                        throw e;
    193                                    }
    194                                    ++block;
    195                                    if (block > 65535) {
    196                                        // wrap the block number
    197                                        block = 0;
    198                                    }
    199                                    wantReply = false; // got the next block, drop out to ack it
    200                                } else { // unexpected block number
    201                                    discardPackets();
    202                                    if (lastBlock == (block == 0 ? 65535 : block - 1)) {
    203                                        wantReply = false; // Resend last acknowledgemen
    204                                    }
    205                                }
    206                                break;
    207
    208                            default:
    209                                throw new IOException("Received unexpected packet type (" + received.getType() + ")");
    210                            }
    211                        } else { // incorrect host or TID
    212                            final TFTPErrorPacket error = new TFTPErrorPacket(recdAddress, recdPort, TFTPErrorPacket.UNKNOWN_TID, "Unexpected host or port.");
    213                            bufferedSend(error);
    214                        }
    215                    } catch (final SocketException | InterruptedIOException e) {
    216                        if (++timeouts >= maxTimeouts) {
    217                            throw new IOException("Connection timed out.");
    218                        }
    219                    } catch (final TFTPPacketException e) {
    220                        throw new IOException("Bad packet: " + e.getMessage());
    221                    }
    222                } while (wantReply); // waiting for response
    223
    224                ack.setBlockNumber(lastBlock);
    225                sent = ack;
    226                bytesRead += dataLength;
    227                totalBytesReceived += dataLength;
    228            } while (dataLength == TFTPPacket.SEGMENT_SIZE); // not eof
    229            bufferedSend(sent); // send the final ack
    230        } finally {
    231            endBufferedOps();
    232        }
    233        return bytesRead;
    234    }
    235
    236    /**
    237     * Same as calling receiveFile(fileName, mode, output, hostname, TFTP.DEFAULT_PORT).
    238     *
    239     * @param fileName The name of the file to receive.
    240     * @param mode     The TFTP mode of the transfer (one of the MODE constants).
    241     * @param output   The OutputStream to which the file should be written.
    242     * @param hostname The name of the remote host serving the file.
    243     * @return number of bytes read
    244     * @throws IOException          If an I/O error occurs. The nature of the error will be reported in the message.
    245     * @throws UnknownHostException If the hostname cannot be resolved.
    246     */
    247    public int receiveFile(final String fileName, final int mode, final OutputStream output, final String hostname) throws UnknownHostException, IOException {
    248        return receiveFile(fileName, mode, output, InetAddress.getByName(hostname), DEFAULT_PORT);
    249    }
    250
    251    /**
    252     * 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
    253     * socket must first be created by {@link org.apache.commons.net.DatagramSocketClient#open open()} before invoking this method. This method will not close
    254     * the OutputStream containing the file; you must close it after the method invocation.
    255     *
    256     * @param fileName The name of the file to receive.
    257     * @param mode     The TFTP mode of the transfer (one of the MODE constants).
    258     * @param output   The OutputStream to which the file should be written.
    259     * @param hostname The name of the remote host serving the file.
    260     * @param port     The port number of the remote TFTP server.
    261     * @return number of bytes read
    262     * @throws IOException          If an I/O error occurs. The nature of the error will be reported in the message.
    263     * @throws UnknownHostException If the hostname cannot be resolved.
    264     */
    265    public int receiveFile(final String fileName, final int mode, final OutputStream output, final String hostname, final int port)
    266            throws UnknownHostException, IOException {
    267        return receiveFile(fileName, mode, output, InetAddress.getByName(hostname), port);
    268    }
    269
    270    /**
    271     * Same as calling sendFile(fileName, mode, input, host, TFTP.DEFAULT_PORT).
    272     *
    273     * @param fileName The name the remote server should use when creating the file on its file system.
    274     * @param mode     The TFTP mode of the transfer (one of the MODE constants).
    275     * @param input    the input stream containing the data to be sent
    276     * @param host     The name of the remote host receiving the file.
    277     * @throws IOException          If an I/O error occurs. The nature of the error will be reported in the message.
    278     * @throws UnknownHostException If the hostname cannot be resolved.
    279     */
    280    public void sendFile(final String fileName, final int mode, final InputStream input, final InetAddress host) throws IOException {
    281        sendFile(fileName, mode, input, host, DEFAULT_PORT);
    282    }
    283
    284    /**
    285     * 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
    286     * socket must first be created by {@link org.apache.commons.net.DatagramSocketClient#open open()} before invoking this method. This method will not close
    287     * the InputStream containing the file; you must close it after the method invocation.
    288     *
    289     * @param fileName The name the remote server should use when creating the file on its file system.
    290     * @param mode     The TFTP mode of the transfer (one of the MODE constants).
    291     * @param input    the input stream containing the data to be sent
    292     * @param host     The remote host receiving the file.
    293     * @param port     The port number of the remote TFTP server.
    294     * @throws IOException If an I/O error occurs. The nature of the error will be reported in the message.
    295     */
    296    public void sendFile(final String fileName, final int mode, InputStream input, InetAddress host, final int port) throws IOException {
    297        int block = 0;
    298        int hostPort = 0;
    299        boolean justStarted = true;
    300        boolean lastAckWait = false;
    301
    302        totalBytesSent = 0L;
    303
    304        if (mode == ASCII_MODE) {
    305            input = new ToNetASCIIInputStream(input);
    306        }
    307
    308        TFTPPacket sent = new TFTPWriteRequestPacket(host, port, fileName, mode);
    309        final TFTPDataPacket data = new TFTPDataPacket(host, port, 0, sendBuffer, 4, 0);
    310
    311        beginBufferedOps();
    312
    313        try {
    314            do { // until eof
    315                 // first time: block is 0, lastBlock is 0, send a request packet.
    316                 // subsequent: block is integer starting at 1, send data packet.
    317                bufferedSend(sent);
    318                boolean wantReply = true;
    319                int timeouts = 0;
    320                do {
    321                    try {
    322                        final TFTPPacket received = bufferedReceive();
    323                        final InetAddress recdAddress = received.getAddress();
    324                        final int recdPort = received.getPort();
    325                        // The first time we receive we get the port number and
    326                        // answering host address (for hosts with multiple IPs)
    327                        if (justStarted) {
    328                            justStarted = false;
    329                            if (recdPort == port) { // must not use the control port here
    330                                final TFTPErrorPacket error = new TFTPErrorPacket(recdAddress, recdPort, TFTPErrorPacket.UNKNOWN_TID, "INCORRECT SOURCE PORT");
    331                                bufferedSend(error);
    332                                throw new IOException("Incorrect source port (" + recdPort + ") in request reply.");
    333                            }
    334                            hostPort = recdPort;
    335                            data.setPort(hostPort);
    336                            if (!host.equals(recdAddress)) {
    337                                host = recdAddress;
    338                                data.setAddress(host);
    339                                sent.setAddress(host);
    340                            }
    341                        }
    342                        // Comply with RFC 783 indication that an error acknowledgment
    343                        // should be sent to originator if unexpected TID or host.
    344                        if (host.equals(recdAddress) && recdPort == hostPort) {
    345
    346                            switch (received.getType()) {
    347                            case TFTPPacket.ERROR:
    348                                final TFTPErrorPacket error = (TFTPErrorPacket) received;
    349                                throw new IOException("Error code " + error.getError() + " received: " + error.getMessage());
    350                            case TFTPPacket.ACKNOWLEDGEMENT:
    351
    352                                final int lastBlock = ((TFTPAckPacket) received).getBlockNumber();
    353
    354                                if (lastBlock == block) {
    355                                    ++block;
    356                                    if (block > 65535) {
    357                                        // wrap the block number
    358                                        block = 0;
    359                                    }
    360                                    wantReply = false; // got the ack we want
    361                                } else {
    362                                    discardPackets();
    363                                }
    364                                break;
    365                            default:
    366                                throw new IOException("Received unexpected packet type.");
    367                            }
    368                        } else { // wrong host or TID; send error
    369                            final TFTPErrorPacket error = new TFTPErrorPacket(recdAddress, recdPort, TFTPErrorPacket.UNKNOWN_TID, "Unexpected host or port.");
    370                            bufferedSend(error);
    371                        }
    372                    } catch (final SocketException | InterruptedIOException e) {
    373                        if (++timeouts >= maxTimeouts) {
    374                            throw new IOException("Connection timed out.");
    375                        }
    376                    } catch (final TFTPPacketException e) {
    377                        throw new IOException("Bad packet: " + e.getMessage());
    378                    }
    379                    // retry until a good ack
    380                } while (wantReply);
    381
    382                if (lastAckWait) {
    383                    break; // we were waiting for this; now all done
    384                }
    385
    386                int dataLength = TFTPPacket.SEGMENT_SIZE;
    387                int offset = 4;
    388                int totalThisPacket = 0;
    389                int bytesRead = 0;
    390                while (dataLength > 0 && (bytesRead = input.read(sendBuffer, offset, dataLength)) > 0) {
    391                    offset += bytesRead;
    392                    dataLength -= bytesRead;
    393                    totalThisPacket += bytesRead;
    394                }
    395                if (totalThisPacket < TFTPPacket.SEGMENT_SIZE) {
    396                    /* this will be our last packet -- send, wait for ack, stop */
    397                    lastAckWait = true;
    398                }
    399                data.setBlockNumber(block);
    400                data.setData(sendBuffer, 4, totalThisPacket);
    401                sent = data;
    402                totalBytesSent += totalThisPacket;
    403            } while (true); // loops until after lastAckWait is set
    404        } finally {
    405            endBufferedOps();
    406        }
    407    }
    408
    409    /**
    410     * Same as calling sendFile(fileName, mode, input, hostname, TFTP.DEFAULT_PORT).
    411     *
    412     * @param fileName The name the remote server should use when creating the file on its file system.
    413     * @param mode     The TFTP mode of the transfer (one of the MODE constants).
    414     * @param input    the input stream containing the data to be sent
    415     * @param hostname The name of the remote host receiving the file.
    416     * @throws IOException          If an I/O error occurs. The nature of the error will be reported in the message.
    417     * @throws UnknownHostException If the hostname cannot be resolved.
    418     */
    419    public void sendFile(final String fileName, final int mode, final InputStream input, final String hostname) throws UnknownHostException, IOException {
    420        sendFile(fileName, mode, input, InetAddress.getByName(hostname), DEFAULT_PORT);
    421    }
    422
    423    /**
    424     * 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
    425     * socket must first be created by {@link org.apache.commons.net.DatagramSocketClient#open open()} before invoking this method. This method will not close
    426     * the InputStream containing the file; you must close it after the method invocation.
    427     *
    428     * @param fileName The name the remote server should use when creating the file on its file system.
    429     * @param mode     The TFTP mode of the transfer (one of the MODE constants).
    430     * @param input    the input stream containing the data to be sent
    431     * @param hostname The name of the remote host receiving the file.
    432     * @param port     The port number of the remote TFTP server.
    433     * @throws IOException          If an I/O error occurs. The nature of the error will be reported in the message.
    434     * @throws UnknownHostException If the hostname cannot be resolved.
    435     */
    436    public void sendFile(final String fileName, final int mode, final InputStream input, final String hostname, final int port)
    437            throws UnknownHostException, IOException {
    438        sendFile(fileName, mode, input, InetAddress.getByName(hostname), port);
    439    }
    440
    441    /**
    442     * Sets the maximum number of times a {@code receive} attempt is allowed to timeout during a receiveFile() or sendFile() operation before ending
    443     * attempts to retry the {@code receive} and failing. The default is DEFAULT_MAX_TIMEOUTS.
    444     *
    445     * @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.
    446     */
    447    public void setMaxTimeouts(final int numTimeouts) {
    448        maxTimeouts = Math.max(numTimeouts, 1);
    449    }
    450}