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