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