View Javadoc
1   /*
2    * Licensed to the Apache Software Foundation (ASF) under one or more
3    * contributor license agreements.  See the NOTICE file distributed with
4    * this work for additional information regarding copyright ownership.
5    * The ASF licenses this file to You under the Apache License, Version 2.0
6    * (the "License"); you may not use this file except in compliance with
7    * the License.  You may obtain a copy of the License at
8    *
9    *      https://www.apache.org/licenses/LICENSE-2.0
10   *
11   * Unless required by applicable law or agreed to in writing, software
12   * distributed under the License is distributed on an "AS IS" BASIS,
13   * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14   * See the License for the specific language governing permissions and
15   * limitations under the License.
16   */
17  
18  package org.apache.commons.net.tftp;
19  
20  import java.io.IOException;
21  import java.io.InputStream;
22  import java.io.InterruptedIOException;
23  import java.io.OutputStream;
24  import java.net.InetAddress;
25  import java.net.SocketException;
26  import java.net.UnknownHostException;
27  
28  import org.apache.commons.net.io.FromNetASCIIOutputStream;
29  import org.apache.commons.net.io.ToNetASCIIInputStream;
30  
31  /**
32   * The TFTPClient class encapsulates all the aspects of the TFTP protocol necessary to receive and send files through TFTP. It is derived from the
33   * {@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
34   * to deal with the TFTP protocol directly. However, almost every user should only be concerend with the the
35   * {@link org.apache.commons.net.DatagramSocketClient#open open()}, {@link org.apache.commons.net.DatagramSocketClient#close close()}, {@link #sendFile
36   * sendFile()}, and {@link #receiveFile receiveFile()} methods. Additionally, the {@link #setMaxTimeouts setMaxTimeouts()} and
37   * {@link org.apache.commons.net.DatagramSocketClient#setDefaultTimeout setDefaultTimeout()} methods may be of importance for performance tuning.
38   * <p>
39   * 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
40   * worry about the internals.
41   * </p>
42   *
43   * @see TFTP
44   * @see TFTPPacket
45   * @see TFTPPacketException
46   */
47  
48  public class TFTPClient extends TFTP {
49  
50      /**
51       * The default number of times a {@code receive} attempt is allowed to timeout before ending attempts to retry the {@code receive} and failing.
52       * The default is 5 timeouts.
53       */
54      public static final int DEFAULT_MAX_TIMEOUTS = 5;
55  
56      /** The maximum number of timeouts allowed before failing. */
57      private int maxTimeouts;
58  
59      /** The number of bytes received in the ongoing download. */
60      private long totalBytesReceived;
61  
62      /** The number of bytes sent in the ongoing upload. */
63      private long totalBytesSent;
64  
65      /**
66       * Creates a TFTPClient instance with a default timeout of DEFAULT_TIMEOUT, maximum timeouts value of DEFAULT_MAX_TIMEOUTS, a null socket, and buffered
67       * operations disabled.
68       */
69      public TFTPClient() {
70          maxTimeouts = DEFAULT_MAX_TIMEOUTS;
71      }
72  
73      /**
74       * Gets the maximum number of times a {@code receive} attempt is allowed to timeout before ending attempts to retry the {@code receive} and failing.
75       *
76       * @return The maximum number of timeouts allowed.
77       */
78      public int getMaxTimeouts() {
79          return maxTimeouts;
80      }
81  
82      /**
83       * Gets the number of bytes received in the ongoing download.
84       *
85       * @return The number of bytes received in the ongoing download.
86       */
87      public long getTotalBytesReceived() {
88          return totalBytesReceived;
89      }
90  
91      /**
92       * Gets the number of bytes sent in the ongoing download.
93       *
94       * @return The number of bytes sent in the ongoing download.
95       */
96      public long getTotalBytesSent() {
97          return totalBytesSent;
98      }
99  
100     /**
101      * Same as calling receiveFile(fileName, mode, output, host, TFTP.DEFAULT_PORT).
102      *
103      * @param fileName The name of the file to receive.
104      * @param mode     The TFTP mode of the transfer (one of the MODE constants).
105      * @param output   The OutputStream to which the file should be written.
106      * @param host     The remote host serving the file.
107      * @return number of bytes read
108      * @throws IOException If an I/O error occurs. The nature of the error will be reported in the message.
109      */
110     public int receiveFile(final String fileName, final int mode, final OutputStream output, final InetAddress host) throws IOException {
111         return receiveFile(fileName, mode, output, host, DEFAULT_PORT);
112     }
113 
114     /**
115      * Requests a named file from a remote host, writes the file to an OutputStream, closes the connection, and returns the number of bytes read. A local UDP
116      * socket must first be created by {@link org.apache.commons.net.DatagramSocketClient#open open()} before invoking this method. This method will not close
117      * the OutputStream containing the file; you must close it after the method invocation.
118      *
119      * @param fileName The name of the file to receive.
120      * @param mode     The TFTP mode of the transfer (one of the MODE constants).
121      * @param output   The OutputStream to which the file should be written.
122      * @param host     The remote host serving the file.
123      * @param port     The port number of the remote TFTP server.
124      * @return number of bytes read
125      * @throws IOException If an I/O error occurs. The nature of the error will be reported in the message.
126      */
127     public int receiveFile(final String fileName, final int mode, OutputStream output, InetAddress host, final int port) throws IOException {
128         int bytesRead = 0;
129         int lastBlock = 0;
130         int block = 1;
131         int hostPort = 0;
132         int dataLength = 0;
133 
134         totalBytesReceived = 0;
135 
136         if (mode == ASCII_MODE) {
137             output = new FromNetASCIIOutputStream(output);
138         }
139 
140         TFTPPacket sent = new TFTPReadRequestPacket(host, port, fileName, mode);
141         final TFTPAckPacket ack = new TFTPAckPacket(host, port, 0);
142 
143         beginBufferedOps();
144 
145         boolean justStarted = true;
146         try {
147             do { // while more data to fetch
148                 bufferedSend(sent); // start the fetch/send an ack
149                 boolean wantReply = true;
150                 int timeouts = 0;
151                 do { // until successful response
152                     try {
153                         final TFTPPacket received = bufferedReceive();
154                         // The first time we receive we get the port number and
155                         // answering host address (for hosts with multiple IPs)
156                         final int recdPort = received.getPort();
157                         final InetAddress recdAddress = received.getAddress();
158                         if (justStarted) {
159                             justStarted = false;
160                             if (recdPort == port) { // must not use the control port here
161                                 final TFTPErrorPacket error = new TFTPErrorPacket(recdAddress, recdPort, TFTPErrorPacket.UNKNOWN_TID, "INCORRECT SOURCE PORT");
162                                 bufferedSend(error);
163                                 throw new IOException("Incorrect source port (" + recdPort + ") in request reply.");
164                             }
165                             hostPort = recdPort;
166                             ack.setPort(hostPort);
167                             if (!host.equals(recdAddress)) {
168                                 host = recdAddress;
169                                 ack.setAddress(host);
170                                 sent.setAddress(host);
171                             }
172                         }
173                         // Comply with RFC 783 indication that an error acknowledgment
174                         // should be sent to originator if unexpected TID or host.
175                         if (host.equals(recdAddress) && recdPort == hostPort) {
176                             switch (received.getType()) {
177 
178                             case TFTPPacket.ERROR:
179                                 TFTPErrorPacket error = (TFTPErrorPacket) received;
180                                 throw new IOException("Error code " + error.getError() + " received: " + error.getMessage());
181                             case TFTPPacket.DATA:
182                                 final TFTPDataPacket data = (TFTPDataPacket) received;
183                                 dataLength = data.getDataLength();
184                                 lastBlock = data.getBlockNumber();
185 
186                                 if (lastBlock == block) { // is the next block number?
187                                     try {
188                                         output.write(data.getData(), data.getDataOffset(), dataLength);
189                                     } catch (final IOException e) {
190                                         error = new TFTPErrorPacket(host, hostPort, TFTPErrorPacket.OUT_OF_SPACE, "File write failed.");
191                                         bufferedSend(error);
192                                         throw e;
193                                     }
194                                     ++block;
195                                     if (block > 65535) {
196                                         // wrap the block number
197                                         block = 0;
198                                     }
199                                     wantReply = false; // got the next block, drop out to ack it
200                                 } else { // unexpected block number
201                                     discardPackets();
202                                     if (lastBlock == (block == 0 ? 65535 : block - 1)) {
203                                         wantReply = false; // Resend last acknowledgemen
204                                     }
205                                 }
206                                 break;
207 
208                             default:
209                                 throw new IOException("Received unexpected packet type (" + received.getType() + ")");
210                             }
211                         } else { // incorrect host or TID
212                             final TFTPErrorPacket error = new TFTPErrorPacket(recdAddress, recdPort, TFTPErrorPacket.UNKNOWN_TID, "Unexpected host or port.");
213                             bufferedSend(error);
214                         }
215                     } catch (final SocketException | InterruptedIOException e) {
216                         if (++timeouts >= maxTimeouts) {
217                             throw new IOException("Connection timed out.");
218                         }
219                     } catch (final TFTPPacketException e) {
220                         throw new IOException("Bad packet: " + e.getMessage());
221                     }
222                 } while (wantReply); // waiting for response
223 
224                 ack.setBlockNumber(lastBlock);
225                 sent = ack;
226                 bytesRead += dataLength;
227                 totalBytesReceived += dataLength;
228             } while (dataLength == TFTPPacket.SEGMENT_SIZE); // not eof
229             bufferedSend(sent); // send the final ack
230         } finally {
231             endBufferedOps();
232         }
233         return bytesRead;
234     }
235 
236     /**
237      * Same as calling receiveFile(fileName, mode, output, hostname, TFTP.DEFAULT_PORT).
238      *
239      * @param fileName The name of the file to receive.
240      * @param mode     The TFTP mode of the transfer (one of the MODE constants).
241      * @param output   The OutputStream to which the file should be written.
242      * @param hostname The name of the remote host serving the file.
243      * @return number of bytes read
244      * @throws IOException          If an I/O error occurs. The nature of the error will be reported in the message.
245      * @throws UnknownHostException If the hostname cannot be resolved.
246      */
247     public int receiveFile(final String fileName, final int mode, final OutputStream output, final String hostname) throws UnknownHostException, IOException {
248         return receiveFile(fileName, mode, output, InetAddress.getByName(hostname), DEFAULT_PORT);
249     }
250 
251     /**
252      * Requests a named file from a remote host, writes the file to an OutputStream, closes the connection, and returns the number of bytes read. A local UDP
253      * socket must first be created by {@link org.apache.commons.net.DatagramSocketClient#open open()} before invoking this method. This method will not close
254      * the OutputStream containing the file; you must close it after the method invocation.
255      *
256      * @param fileName The name of the file to receive.
257      * @param mode     The TFTP mode of the transfer (one of the MODE constants).
258      * @param output   The OutputStream to which the file should be written.
259      * @param hostname The name of the remote host serving the file.
260      * @param port     The port number of the remote TFTP server.
261      * @return number of bytes read
262      * @throws IOException          If an I/O error occurs. The nature of the error will be reported in the message.
263      * @throws UnknownHostException If the hostname cannot be resolved.
264      */
265     public int receiveFile(final String fileName, final int mode, final OutputStream output, final String hostname, final int port)
266             throws UnknownHostException, IOException {
267         return receiveFile(fileName, mode, output, InetAddress.getByName(hostname), port);
268     }
269 
270     /**
271      * Same as calling sendFile(fileName, mode, input, host, TFTP.DEFAULT_PORT).
272      *
273      * @param fileName The name the remote server should use when creating the file on its file system.
274      * @param mode     The TFTP mode of the transfer (one of the MODE constants).
275      * @param input    the input stream containing the data to be sent
276      * @param host     The name of the remote host receiving the file.
277      * @throws IOException          If an I/O error occurs. The nature of the error will be reported in the message.
278      * @throws UnknownHostException If the hostname cannot be resolved.
279      */
280     public void sendFile(final String fileName, final int mode, final InputStream input, final InetAddress host) throws IOException {
281         sendFile(fileName, mode, input, host, DEFAULT_PORT);
282     }
283 
284     /**
285      * Requests to send a file to a remote host, reads the file from an InputStream, sends the file to the remote host, and closes the connection. A local UDP
286      * socket must first be created by {@link org.apache.commons.net.DatagramSocketClient#open open()} before invoking this method. This method will not close
287      * the InputStream containing the file; you must close it after the method invocation.
288      *
289      * @param fileName The name the remote server should use when creating the file on its file system.
290      * @param mode     The TFTP mode of the transfer (one of the MODE constants).
291      * @param input    the input stream containing the data to be sent
292      * @param host     The remote host receiving the file.
293      * @param port     The port number of the remote TFTP server.
294      * @throws IOException If an I/O error occurs. The nature of the error will be reported in the message.
295      */
296     public void sendFile(final String fileName, final int mode, InputStream input, InetAddress host, final int port) throws IOException {
297         int block = 0;
298         int hostPort = 0;
299         boolean justStarted = true;
300         boolean lastAckWait = false;
301 
302         totalBytesSent = 0L;
303 
304         if (mode == ASCII_MODE) {
305             input = new ToNetASCIIInputStream(input);
306         }
307 
308         TFTPPacket sent = new TFTPWriteRequestPacket(host, port, fileName, mode);
309         final TFTPDataPacket data = new TFTPDataPacket(host, port, 0, sendBuffer, 4, 0);
310 
311         beginBufferedOps();
312 
313         try {
314             do { // until eof
315                  // first time: block is 0, lastBlock is 0, send a request packet.
316                  // subsequent: block is integer starting at 1, send data packet.
317                 bufferedSend(sent);
318                 boolean wantReply = true;
319                 int timeouts = 0;
320                 do {
321                     try {
322                         final TFTPPacket received = bufferedReceive();
323                         final InetAddress recdAddress = received.getAddress();
324                         final int recdPort = received.getPort();
325                         // The first time we receive we get the port number and
326                         // answering host address (for hosts with multiple IPs)
327                         if (justStarted) {
328                             justStarted = false;
329                             if (recdPort == port) { // must not use the control port here
330                                 final TFTPErrorPacket error = new TFTPErrorPacket(recdAddress, recdPort, TFTPErrorPacket.UNKNOWN_TID, "INCORRECT SOURCE PORT");
331                                 bufferedSend(error);
332                                 throw new IOException("Incorrect source port (" + recdPort + ") in request reply.");
333                             }
334                             hostPort = recdPort;
335                             data.setPort(hostPort);
336                             if (!host.equals(recdAddress)) {
337                                 host = recdAddress;
338                                 data.setAddress(host);
339                                 sent.setAddress(host);
340                             }
341                         }
342                         // Comply with RFC 783 indication that an error acknowledgment
343                         // should be sent to originator if unexpected TID or host.
344                         if (host.equals(recdAddress) && recdPort == hostPort) {
345 
346                             switch (received.getType()) {
347                             case TFTPPacket.ERROR:
348                                 final TFTPErrorPacket error = (TFTPErrorPacket) received;
349                                 throw new IOException("Error code " + error.getError() + " received: " + error.getMessage());
350                             case TFTPPacket.ACKNOWLEDGEMENT:
351 
352                                 final int lastBlock = ((TFTPAckPacket) received).getBlockNumber();
353 
354                                 if (lastBlock == block) {
355                                     ++block;
356                                     if (block > 65535) {
357                                         // wrap the block number
358                                         block = 0;
359                                     }
360                                     wantReply = false; // got the ack we want
361                                 } else {
362                                     discardPackets();
363                                 }
364                                 break;
365                             default:
366                                 throw new IOException("Received unexpected packet type.");
367                             }
368                         } else { // wrong host or TID; send error
369                             final TFTPErrorPacket error = new TFTPErrorPacket(recdAddress, recdPort, TFTPErrorPacket.UNKNOWN_TID, "Unexpected host or port.");
370                             bufferedSend(error);
371                         }
372                     } catch (final SocketException | InterruptedIOException e) {
373                         if (++timeouts >= maxTimeouts) {
374                             throw new IOException("Connection timed out.");
375                         }
376                     } catch (final TFTPPacketException e) {
377                         throw new IOException("Bad packet: " + e.getMessage());
378                     }
379                     // retry until a good ack
380                 } while (wantReply);
381 
382                 if (lastAckWait) {
383                     break; // we were waiting for this; now all done
384                 }
385 
386                 int dataLength = TFTPPacket.SEGMENT_SIZE;
387                 int offset = 4;
388                 int totalThisPacket = 0;
389                 int bytesRead = 0;
390                 while (dataLength > 0 && (bytesRead = input.read(sendBuffer, offset, dataLength)) > 0) {
391                     offset += bytesRead;
392                     dataLength -= bytesRead;
393                     totalThisPacket += bytesRead;
394                 }
395                 if (totalThisPacket < TFTPPacket.SEGMENT_SIZE) {
396                     /* this will be our last packet -- send, wait for ack, stop */
397                     lastAckWait = true;
398                 }
399                 data.setBlockNumber(block);
400                 data.setData(sendBuffer, 4, totalThisPacket);
401                 sent = data;
402                 totalBytesSent += totalThisPacket;
403             } while (true); // loops until after lastAckWait is set
404         } finally {
405             endBufferedOps();
406         }
407     }
408 
409     /**
410      * Same as calling sendFile(fileName, mode, input, hostname, TFTP.DEFAULT_PORT).
411      *
412      * @param fileName The name the remote server should use when creating the file on its file system.
413      * @param mode     The TFTP mode of the transfer (one of the MODE constants).
414      * @param input    the input stream containing the data to be sent
415      * @param hostname The name of the remote host receiving the file.
416      * @throws IOException          If an I/O error occurs. The nature of the error will be reported in the message.
417      * @throws UnknownHostException If the hostname cannot be resolved.
418      */
419     public void sendFile(final String fileName, final int mode, final InputStream input, final String hostname) throws UnknownHostException, IOException {
420         sendFile(fileName, mode, input, InetAddress.getByName(hostname), DEFAULT_PORT);
421     }
422 
423     /**
424      * Requests to send a file to a remote host, reads the file from an InputStream, sends the file to the remote host, and closes the connection. A local UDP
425      * socket must first be created by {@link org.apache.commons.net.DatagramSocketClient#open open()} before invoking this method. This method will not close
426      * the InputStream containing the file; you must close it after the method invocation.
427      *
428      * @param fileName The name the remote server should use when creating the file on its file system.
429      * @param mode     The TFTP mode of the transfer (one of the MODE constants).
430      * @param input    the input stream containing the data to be sent
431      * @param hostname The name of the remote host receiving the file.
432      * @param port     The port number of the remote TFTP server.
433      * @throws IOException          If an I/O error occurs. The nature of the error will be reported in the message.
434      * @throws UnknownHostException If the hostname cannot be resolved.
435      */
436     public void sendFile(final String fileName, final int mode, final InputStream input, final String hostname, final int port)
437             throws UnknownHostException, IOException {
438         sendFile(fileName, mode, input, InetAddress.getByName(hostname), port);
439     }
440 
441     /**
442      * Sets the maximum number of times a {@code receive} attempt is allowed to timeout during a receiveFile() or sendFile() operation before ending
443      * attempts to retry the {@code receive} and failing. The default is DEFAULT_MAX_TIMEOUTS.
444      *
445      * @param numTimeouts The maximum number of timeouts to allow. Values less than 1 should not be used, but if they are, they are treated as 1.
446      */
447     public void setMaxTimeouts(final int numTimeouts) {
448         maxTimeouts = Math.max(numTimeouts, 1);
449     }
450 }