001/*
002 * Licensed to the Apache Software Foundation (ASF) under one or more
003 * contributor license agreements.  See the NOTICE file distributed with
004 * this work for additional information regarding copyright ownership.
005 * The ASF licenses this file to You under the Apache License, Version 2.0
006 * (the "License"); you may not use this file except in compliance with
007 * the License.  You may obtain a copy of the License at
008 *
009 *      http://www.apache.org/licenses/LICENSE-2.0
010 *
011 * Unless required by applicable law or agreed to in writing, software
012 * distributed under the License is distributed on an "AS IS" BASIS,
013 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
014 * See the License for the specific language governing permissions and
015 * limitations under the License.
016 */
017
018package org.apache.commons.net.tftp;
019
020import java.io.IOException;
021import java.io.InputStream;
022import java.io.InterruptedIOException;
023import java.io.OutputStream;
024import java.net.InetAddress;
025import java.net.SocketException;
026import java.net.UnknownHostException;
027
028import org.apache.commons.net.io.FromNetASCIIOutputStream;
029import org.apache.commons.net.io.ToNetASCIIInputStream;
030
031/**
032 * The TFTPClient class encapsulates all the aspects of the TFTP protocol
033 * necessary to receive and send files through TFTP.  It is derived from
034 * the {@link org.apache.commons.net.tftp.TFTP} because
035 * it is more convenient than using aggregation, and as a result exposes
036 * the same set of methods to allow you to deal with the TFTP protocol
037 * directly.  However, almost every user should only be concerend with the
038 * the {@link org.apache.commons.net.DatagramSocketClient#open  open() },
039 * {@link org.apache.commons.net.DatagramSocketClient#close  close() },
040 * {@link #sendFile  sendFile() }, and
041 * {@link #receiveFile  receiveFile() } methods.  Additionally, the
042 * {@link #setMaxTimeouts  setMaxTimeouts() } and
043 * {@link org.apache.commons.net.DatagramSocketClient#setDefaultTimeout setDefaultTimeout() }
044 *  methods may be of importance for performance
045 * tuning.
046 * <p>
047 * Details regarding the TFTP protocol and the format of TFTP packets can
048 * be found in RFC 783.  But the point of these classes is to keep you
049 * from having to worry about the internals.
050 *
051 *
052 * @see TFTP
053 * @see TFTPPacket
054 * @see TFTPPacketException
055 */
056
057public class TFTPClient extends TFTP
058{
059    /**
060     * The default number of times a receive attempt is allowed to timeout
061     * before ending attempts to retry the receive and failing.  The default
062     * is 5 timeouts.
063     */
064    public static final int DEFAULT_MAX_TIMEOUTS = 5;
065
066    /** The maximum number of timeouts allowed before failing. */
067    private int maxTimeouts;
068
069    /** The number of bytes received in the ongoing download. */
070    private long totalBytesReceived;
071
072    /** The number of bytes sent in the ongoing upload. */
073    private long totalBytesSent;
074
075    /**
076     * Creates a TFTPClient instance with a default timeout of DEFAULT_TIMEOUT,
077     * maximum timeouts value of DEFAULT_MAX_TIMEOUTS, a null socket,
078     * and buffered operations disabled.
079     */
080    public TFTPClient()
081    {
082        maxTimeouts = DEFAULT_MAX_TIMEOUTS;
083    }
084
085    /**
086     * Sets the maximum number of times a receive attempt is allowed to
087     * timeout during a receiveFile() or sendFile() operation before ending
088     * attempts to retry the receive and failing.
089     * The default is DEFAULT_MAX_TIMEOUTS.
090     *
091     * @param numTimeouts  The maximum number of timeouts to allow.  Values
092     *        less than 1 should not be used, but if they are, they are
093     *        treated as 1.
094     */
095    public void setMaxTimeouts(final int numTimeouts)
096    {
097        if (numTimeouts < 1) {
098            maxTimeouts = 1;
099        } else {
100            maxTimeouts = numTimeouts;
101        }
102    }
103
104    /**
105     * Returns the maximum number of times a receive attempt is allowed to
106     * timeout before ending attempts to retry the receive and failing.
107     *
108     * @return The maximum number of timeouts allowed.
109     */
110    public int getMaxTimeouts()
111    {
112        return maxTimeouts;
113    }
114
115
116    /**
117     * @return The number of bytes received in the ongoing download
118     */
119    public long getTotalBytesReceived() {
120        return totalBytesReceived;
121    }
122
123    /**
124     * @return The number of bytes sent in the ongoing download
125     */
126    public long getTotalBytesSent() {
127        return totalBytesSent;
128    }
129
130    /**
131     * Requests a named file from a remote host, writes the
132     * file to an OutputStream, closes the connection, and returns the number
133     * of bytes read.  A local UDP socket must first be created by
134     * {@link org.apache.commons.net.DatagramSocketClient#open open()} before
135     * invoking this method.  This method will not close the OutputStream
136     * containing the file; you must close it after the method invocation.
137     *
138     * @param fileName  The name of the file to receive.
139     * @param mode   The TFTP mode of the transfer (one of the MODE constants).
140     * @param output The OutputStream to which the file should be written.
141     * @param host   The remote host serving the file.
142     * @param port   The port number of the remote TFTP server.
143     * @return number of bytes read
144     * @throws IOException If an I/O error occurs.  The nature of the
145     *            error will be reported in the message.
146     */
147    public int receiveFile(final String fileName, final int mode, OutputStream output,
148                           InetAddress host, final int port) throws IOException
149    {
150        int bytesRead = 0;
151        int lastBlock = 0;
152        int block = 1;
153        int hostPort = 0;
154        int dataLength = 0;
155
156        totalBytesReceived = 0;
157
158        if (mode == TFTP.ASCII_MODE) {
159            output = new FromNetASCIIOutputStream(output);
160        }
161
162        TFTPPacket sent = new TFTPReadRequestPacket(host, port, fileName, mode);
163        final TFTPAckPacket ack = new TFTPAckPacket(host, port, 0);
164
165        beginBufferedOps();
166
167        boolean justStarted = true;
168        try {
169            do { // while more data to fetch
170                bufferedSend(sent); // start the fetch/send an ack
171                boolean wantReply = true;
172                int timeouts = 0;
173                do { // until successful response
174                    try {
175                        final TFTPPacket received = bufferedReceive();
176                        // The first time we receive we get the port number and
177                        // answering host address (for hosts with multiple IPs)
178                        final int recdPort = received.getPort();
179                        final InetAddress recdAddress = received.getAddress();
180                        if (justStarted) {
181                            justStarted = false;
182                            if (recdPort == port) { // must not use the control port here
183                                final TFTPErrorPacket error = new TFTPErrorPacket(recdAddress,
184                                        recdPort, TFTPErrorPacket.UNKNOWN_TID,
185                                        "INCORRECT SOURCE PORT");
186                                bufferedSend(error);
187                                throw new IOException("Incorrect source port ("+recdPort+") in request reply.");
188                            }
189                            hostPort = recdPort;
190                            ack.setPort(hostPort);
191                            if(!host.equals(recdAddress))
192                            {
193                                host = recdAddress;
194                                ack.setAddress(host);
195                                sent.setAddress(host);
196                            }
197                        }
198                        // Comply with RFC 783 indication that an error acknowledgment
199                        // should be sent to originator if unexpected TID or host.
200                        if (host.equals(recdAddress) && recdPort == hostPort) {
201                            switch (received.getType()) {
202
203                            case TFTPPacket.ERROR:
204                                TFTPErrorPacket error = (TFTPErrorPacket)received;
205                                throw new IOException("Error code " + error.getError() +
206                                                      " received: " + error.getMessage());
207                            case TFTPPacket.DATA:
208                                final TFTPDataPacket data = (TFTPDataPacket)received;
209                                dataLength = data.getDataLength();
210                                lastBlock = data.getBlockNumber();
211
212                                if (lastBlock == block) { // is the next block number?
213                                    try {
214                                        output.write(data.getData(), data.getDataOffset(), dataLength);
215                                    } catch (final IOException e) {
216                                        error = new TFTPErrorPacket(host, hostPort,
217                                                                    TFTPErrorPacket.OUT_OF_SPACE,
218                                                                    "File write failed.");
219                                        bufferedSend(error);
220                                        throw e;
221                                    }
222                                    ++block;
223                                    if (block > 65535) {
224                                        // wrap the block number
225                                        block = 0;
226                                    }
227                                    wantReply = false; // got the next block, drop out to ack it
228                                } else { // unexpected block number
229                                    discardPackets();
230                                    if (lastBlock == (block == 0 ? 65535 : block - 1)) {
231                                        wantReply = false; // Resend last acknowledgemen
232                                    }
233                                }
234                                break;
235
236                            default:
237                                throw new IOException("Received unexpected packet type (" + received.getType() + ")");
238                            }
239                        } else { // incorrect host or TID
240                            final TFTPErrorPacket error = new TFTPErrorPacket(recdAddress, recdPort,
241                                    TFTPErrorPacket.UNKNOWN_TID,
242                                    "Unexpected host or port.");
243                            bufferedSend(error);
244                        }
245                    } catch (final SocketException | InterruptedIOException e) {
246                        if (++timeouts >= maxTimeouts) {
247                            throw new IOException("Connection timed out.");
248                        }
249                    } catch (final TFTPPacketException e) {
250                        throw new IOException("Bad packet: " + e.getMessage());
251                    }
252                } while(wantReply); // waiting for response
253
254                ack.setBlockNumber(lastBlock);
255                sent = ack;
256                bytesRead += dataLength;
257                totalBytesReceived += dataLength;
258            } while (dataLength == TFTPPacket.SEGMENT_SIZE); // not eof
259            bufferedSend(sent); // send the final ack
260        } finally {
261            endBufferedOps();
262        }
263        return bytesRead;
264    }
265
266
267    /**
268     * Requests a named file from a remote host, writes the
269     * file to an OutputStream, closes the connection, and returns the number
270     * of bytes read.  A local UDP socket must first be created by
271     * {@link org.apache.commons.net.DatagramSocketClient#open open()} before
272     * invoking this method.  This method will not close the OutputStream
273     * containing the file; you must close it after the method invocation.
274     *
275     * @param fileName The name of the file to receive.
276     * @param mode     The TFTP mode of the transfer (one of the MODE constants).
277     * @param output   The OutputStream to which the file should be written.
278     * @param hostname The name of the remote host serving the file.
279     * @param port     The port number of the remote TFTP server.
280     * @return number of bytes read
281     * @throws IOException If an I/O error occurs.  The nature of the
282     *            error will be reported in the message.
283     * @throws UnknownHostException  If the hostname cannot be resolved.
284     */
285    public int receiveFile(final String fileName, final int mode, final OutputStream output,
286                           final String hostname, final int port)
287    throws UnknownHostException, IOException
288    {
289        return receiveFile(fileName, mode, output, InetAddress.getByName(hostname),
290                           port);
291    }
292
293
294    /**
295     * Same as calling receiveFile(fileName, mode, output, host, TFTP.DEFAULT_PORT).
296     *
297     * @param fileName The name of the file to receive.
298     * @param mode     The TFTP mode of the transfer (one of the MODE constants).
299     * @param output   The OutputStream to which the file should be written.
300     * @param host     The remote host serving the file.
301     * @return number of bytes read
302     * @throws IOException If an I/O error occurs.  The nature of the
303     *            error will be reported in the message.
304     */
305    public int receiveFile(final String fileName, final int mode, final OutputStream output,
306                           final InetAddress host)
307    throws IOException
308    {
309        return receiveFile(fileName, mode, output, host, DEFAULT_PORT);
310    }
311
312    /**
313     * Same as calling receiveFile(fileName, mode, output, hostname, TFTP.DEFAULT_PORT).
314     *
315     * @param fileName The name of the file to receive.
316     * @param mode     The TFTP mode of the transfer (one of the MODE constants).
317     * @param output   The OutputStream to which the file should be written.
318     * @param hostname The name of the remote host serving the file.
319     * @return number of bytes read
320     * @throws IOException If an I/O error occurs.  The nature of the
321     *            error will be reported in the message.
322     * @throws UnknownHostException  If the hostname cannot be resolved.
323     */
324    public int receiveFile(final String fileName, final int mode, final OutputStream output,
325                           final String hostname)
326    throws UnknownHostException, IOException
327    {
328        return receiveFile(fileName, mode, output, InetAddress.getByName(hostname),
329                           DEFAULT_PORT);
330    }
331
332
333    /**
334     * Requests to send a file to a remote host, reads the file from an
335     * InputStream, sends the file to the remote host, and closes the
336     * connection.  A local UDP socket must first be created by
337     * {@link org.apache.commons.net.DatagramSocketClient#open open()} before
338     * invoking this method.  This method will not close the InputStream
339     * containing the file; you must close it after the method invocation.
340     *
341     * @param fileName The name the remote server should use when creating
342     *        the file on its file system.
343     * @param mode     The TFTP mode of the transfer (one of the MODE constants).
344     * @param input the input stream containing the data to be sent
345     * @param host     The remote host receiving the file.
346     * @param port     The port number of the remote TFTP server.
347     * @throws IOException If an I/O error occurs.  The nature of the
348     *            error will be reported in the message.
349     */
350    public void sendFile(final String fileName, final int mode, InputStream input,
351                         InetAddress host, final int port) throws IOException
352    {
353        int block = 0;
354        int hostPort = 0;
355        boolean justStarted = true;
356        boolean lastAckWait = false;
357
358        totalBytesSent = 0L;
359
360        if (mode == TFTP.ASCII_MODE) {
361            input = new ToNetASCIIInputStream(input);
362        }
363
364        TFTPPacket sent = new TFTPWriteRequestPacket(host, port, fileName, mode);
365        final TFTPDataPacket data = new TFTPDataPacket(host, port, 0, sendBuffer, 4, 0);
366
367        beginBufferedOps();
368
369        try {
370            do { // until eof
371                // first time: block is 0, lastBlock is 0, send a request packet.
372                // subsequent: block is integer starting at 1, send data packet.
373                bufferedSend(sent);
374                boolean wantReply = true;
375                int timeouts = 0;
376                do {
377                    try {
378                        final TFTPPacket received = bufferedReceive();
379                        final InetAddress recdAddress = received.getAddress();
380                        final int recdPort = received.getPort();
381                        // The first time we receive we get the port number and
382                        // answering host address (for hosts with multiple IPs)
383                        if (justStarted) {
384                            justStarted = false;
385                            if (recdPort == port) { // must not use the control port here
386                                final TFTPErrorPacket error = new TFTPErrorPacket(recdAddress,
387                                        recdPort, TFTPErrorPacket.UNKNOWN_TID,
388                                        "INCORRECT SOURCE PORT");
389                                bufferedSend(error);
390                                throw new IOException("Incorrect source port ("+recdPort+") in request reply.");
391                            }
392                            hostPort = recdPort;
393                            data.setPort(hostPort);
394                            if (!host.equals(recdAddress)) {
395                                host = recdAddress;
396                                data.setAddress(host);
397                                sent.setAddress(host);
398                            }
399                        }
400                        // Comply with RFC 783 indication that an error acknowledgment
401                        // should be sent to originator if unexpected TID or host.
402                        if (host.equals(recdAddress) && recdPort == hostPort) {
403
404                            switch (received.getType()) {
405                            case TFTPPacket.ERROR:
406                                final TFTPErrorPacket error = (TFTPErrorPacket)received;
407                                throw new IOException("Error code " + error.getError() +
408                                                      " received: " + error.getMessage());
409                            case TFTPPacket.ACKNOWLEDGEMENT:
410
411                                final int lastBlock = ((TFTPAckPacket)received).getBlockNumber();
412
413                                if (lastBlock == block) {
414                                    ++block;
415                                    if (block > 65535) {
416                                        // wrap the block number
417                                        block = 0;
418                                    }
419                                    wantReply = false; // got the ack we want
420                                } else {
421                                    discardPackets();
422                                }
423                                break;
424                            default:
425                                throw new IOException("Received unexpected packet type.");
426                            }
427                        } else { // wrong host or TID; send error
428                            final TFTPErrorPacket error = new TFTPErrorPacket(recdAddress,
429                                                        recdPort,
430                                                        TFTPErrorPacket.UNKNOWN_TID,
431                                                        "Unexpected host or port.");
432                            bufferedSend(error);
433                        }
434                    } catch (final SocketException | InterruptedIOException e) {
435                        if (++timeouts >= maxTimeouts) {
436                            throw new IOException("Connection timed out.");
437                        }
438                    } catch (final TFTPPacketException e) {
439                        throw new IOException("Bad packet: " + e.getMessage());
440                    }
441                    // retry until a good ack
442                } while(wantReply);
443
444                if (lastAckWait) {
445                    break; // we were waiting for this; now all done
446                }
447
448                int dataLength = TFTPPacket.SEGMENT_SIZE;
449                int offset = 4;
450                int totalThisPacket = 0;
451                int bytesRead = 0;
452                while (dataLength > 0 &&
453                        (bytesRead = input.read(sendBuffer, offset, dataLength)) > 0) {
454                    offset += bytesRead;
455                    dataLength -= bytesRead;
456                    totalThisPacket += bytesRead;
457                }
458                if( totalThisPacket < TFTPPacket.SEGMENT_SIZE ) {
459                    /* this will be our last packet -- send, wait for ack, stop */
460                    lastAckWait = true;
461                }
462                data.setBlockNumber(block);
463                data.setData(sendBuffer, 4, totalThisPacket);
464                sent = data;
465                totalBytesSent += totalThisPacket;
466            } while (true); // loops until after lastAckWait is set
467        } finally {
468            endBufferedOps();
469        }
470    }
471
472
473    /**
474     * Requests to send a file to a remote host, reads the file from an
475     * InputStream, sends the file to the remote host, and closes the
476     * connection.  A local UDP socket must first be created by
477     * {@link org.apache.commons.net.DatagramSocketClient#open open()} before
478     * invoking this method.  This method will not close the InputStream
479     * containing the file; you must close it after the method invocation.
480     *
481     * @param fileName The name the remote server should use when creating
482     *        the file on its file system.
483     * @param mode     The TFTP mode of the transfer (one of the MODE constants).
484     * @param input the input stream containing the data to be sent
485     * @param hostname The name of the remote host receiving the file.
486     * @param port     The port number of the remote TFTP server.
487     * @throws IOException If an I/O error occurs.  The nature of the
488     *            error will be reported in the message.
489     * @throws UnknownHostException  If the hostname cannot be resolved.
490     */
491    public void sendFile(final String fileName, final int mode, final InputStream input,
492                         final String hostname, final int port)
493    throws UnknownHostException, IOException
494    {
495        sendFile(fileName, mode, input, InetAddress.getByName(hostname), port);
496    }
497
498
499    /**
500     * Same as calling sendFile(fileName, mode, input, host, TFTP.DEFAULT_PORT).
501     *
502     * @param fileName The name the remote server should use when creating
503     *        the file on its file system.
504     * @param mode     The TFTP mode of the transfer (one of the MODE constants).
505     * @param input the input stream containing the data to be sent
506     * @param host     The name of the remote host receiving the file.
507     * @throws IOException If an I/O error occurs.  The nature of the
508     *            error will be reported in the message.
509     * @throws UnknownHostException  If the hostname cannot be resolved.
510     */
511    public void sendFile(final String fileName, final int mode, final InputStream input,
512                         final InetAddress host)
513    throws IOException
514    {
515        sendFile(fileName, mode, input, host, DEFAULT_PORT);
516    }
517
518    /**
519     * Same as calling sendFile(fileName, mode, input, hostname, TFTP.DEFAULT_PORT).
520     *
521     * @param fileName The name the remote server should use when creating
522     *        the file on its file system.
523     * @param mode     The TFTP mode of the transfer (one of the MODE constants).
524     * @param input the input stream containing the data to be sent
525     * @param hostname The name of the remote host receiving the file.
526     * @throws IOException If an I/O error occurs.  The nature of the
527     *            error will be reported in the message.
528     * @throws UnknownHostException  If the hostname cannot be resolved.
529     */
530    public void sendFile(final String fileName, final int mode, final InputStream input,
531                         final String hostname)
532    throws UnknownHostException, IOException
533    {
534        sendFile(fileName, mode, input, InetAddress.getByName(hostname),
535                 DEFAULT_PORT);
536    }
537}