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