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 }