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}