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