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    *      http://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   *
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       * Returns 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       * @return The number of bytes received in the ongoing download
83       */
84      public long getTotalBytesReceived() {
85          return totalBytesReceived;
86      }
87  
88      /**
89       * @return The number of bytes sent in the ongoing download
90       */
91      public long getTotalBytesSent() {
92          return totalBytesSent;
93      }
94  
95      /**
96       * Same as calling receiveFile(fileName, mode, output, host, TFTP.DEFAULT_PORT).
97       *
98       * @param fileName The name of the file to receive.
99       * @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 }