View Javadoc
1   /*
2    * This file is part of Waarp Project (named also Waarp or GG).
3    *
4    *  Copyright (c) 2019, Waarp SAS, and individual contributors by the @author
5    *  tags. See the COPYRIGHT.txt in the distribution for a full listing of
6    * individual contributors.
7    *
8    *  All Waarp Project is free software: you can redistribute it and/or
9    * modify it under the terms of the GNU General Public License as published by
10   * the Free Software Foundation, either version 3 of the License, or (at your
11   * option) any later version.
12   *
13   * Waarp is distributed in the hope that it will be useful, but WITHOUT ANY
14   * WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
15   * A PARTICULAR PURPOSE. See the GNU General Public License for more details.
16   *
17   *  You should have received a copy of the GNU General Public License along with
18   * Waarp . If not, see <http://www.gnu.org/licenses/>.
19   */
20  package org.waarp.ftp.client;
21  
22  import org.apache.commons.net.PrintCommandListener;
23  import org.apache.commons.net.ftp.FTP;
24  import org.apache.commons.net.ftp.FTPClient;
25  import org.apache.commons.net.ftp.FTPFile;
26  import org.apache.commons.net.ftp.FTPReply;
27  import org.apache.commons.net.ftp.FTPSClient;
28  import org.apache.commons.net.util.TrustManagerUtils;
29  import org.waarp.common.file.FileUtils;
30  import org.waarp.common.logging.SysErrLogger;
31  import org.waarp.common.logging.WaarpLogger;
32  import org.waarp.common.logging.WaarpLoggerFactory;
33  
34  import java.io.FileInputStream;
35  import java.io.FileNotFoundException;
36  import java.io.FileOutputStream;
37  import java.io.IOException;
38  import java.io.InputStream;
39  import java.io.OutputStream;
40  import java.io.PrintWriter;
41  import java.net.UnknownHostException;
42  
43  /**
44   * FTP Client using Apache Commons net FTP client (not working using FTPS or
45   * FTPSE)
46   */
47  public class WaarpFtpClient implements WaarpFtpClientInterface {
48    /**
49     * Internal Logger
50     */
51    protected static final WaarpLogger logger =
52        WaarpLoggerFactory.getLogger(WaarpFtpClient.class);
53    private static final int DEFAULT_WAIT = 2;
54  
55    final String server;
56    int port = 21;
57    final String user;
58    final String pwd;
59    final String acct;
60    int timeout;
61    boolean isPassive;
62    final int ssl; // -1 native, 1 auth
63    protected final FTPClient ftpClient;
64    protected String result;
65    protected String directory = null;
66    protected String ipAddress = null;
67  
68    /**
69     * WARNING: SSL mode (FTPS and FTPSE) are not working due to a bug in Apache
70     * Commons-Net
71     *
72     * @param server
73     * @param port
74     * @param user
75     * @param pwd
76     * @param acct
77     * @param isPassive
78     * @param ssl
79     * @param timeout
80     */
81    public WaarpFtpClient(final String server, final int port, final String user,
82                          final String pwd, final String acct,
83                          final boolean isPassive, final int ssl,
84                          final int controlTimeout, final int timeout) {
85      this(server, port, user, pwd, acct, isPassive, ssl, controlTimeout, timeout,
86           true);
87    }
88  
89    /**
90     * WARNING: SSL mode (FTPS and FTPSE) are not working due to a bug in Apache
91     * Commons-Net
92     *
93     * @param server
94     * @param port
95     * @param user
96     * @param pwd
97     * @param acct
98     * @param isPassive
99     * @param ssl
100    * @param timeout
101    */
102   public WaarpFtpClient(final String server, final int port, final String user,
103                         final String pwd, final String acct,
104                         final boolean isPassive, final int ssl,
105                         final int controlTimeout, final int timeout,
106                         final boolean trace) {
107     this.server = server;
108     this.port = port;
109     this.user = user;
110     this.pwd = pwd;
111     this.acct = acct;
112     this.isPassive = isPassive;
113     this.ssl = ssl;
114     if (this.ssl != 0) {
115       // implicit or explicit
116       ftpClient = new FTPSClient(this.ssl == -1);
117       ((FTPSClient) ftpClient).setTrustManager(
118           TrustManagerUtils.getAcceptAllTrustManager());
119     } else {
120       ftpClient = new FTPClient();
121     }
122     ftpClient.setBufferSize(1024 * 1024);
123     if (controlTimeout > 0) {
124       ftpClient.setControlKeepAliveTimeout(controlTimeout / 1000);
125       ftpClient.setControlKeepAliveReplyTimeout(controlTimeout);
126     }
127     if (timeout > 0) {
128       ftpClient.setDataTimeout(timeout);
129     }
130     ftpClient.setListHiddenFiles(true);
131     if (trace) {
132       ftpClient.addProtocolCommandListener(
133           new PrintCommandListener(new PrintWriter(System.out), true));
134     }
135   }
136 
137   @Override
138   public void setReportActiveExternalIPAddress(final String ipAddress) {
139     this.ipAddress = ipAddress;
140     if (this.ipAddress != null) {
141       try {
142         ftpClient.setReportActiveExternalIPAddress(ipAddress);
143       } catch (final UnknownHostException e) {
144         logger.error("Cannot set Ip Address since {}", e.getMessage());
145       }
146     }
147   }
148 
149   @Override
150   public void setActiveDataTransferPortRange(final int from, final int to) {
151     ftpClient.setActivePortRange(from, to);
152   }
153 
154   @Override
155   public final String getResult() {
156     return result;
157   }
158 
159   private void reconnect() {
160     logout();
161     waitAfterDataCommand();
162     connect();
163     if (directory != null) {
164       changeDir(directory);
165     }
166     if (ipAddress != null) {
167       setReportActiveExternalIPAddress(ipAddress);
168     }
169   }
170 
171   @Override
172   public final boolean connect() {
173     result = null;
174     boolean isActive = false;
175     try {
176       for (int j = 0; j < 3; j++) {
177         waitAfterDataCommand();
178         Exception lastExcemption = null;
179         for (int i = 0; i < 5; i++) {
180           try {
181             if (port > 0) {
182               ftpClient.connect(server, port);
183             } else {
184               ftpClient.connect(server);
185             }
186             lastExcemption = null;
187             break;
188           } catch (final Exception e) {
189             result = CONNECTION_IN_ERROR;
190             lastExcemption = e;
191           }
192           waitAfterDataCommand();
193         }
194         if (lastExcemption != null) {
195           logger.error(result + ": {}", lastExcemption.getMessage());
196           return false;
197         }
198         final int reply = ftpClient.getReplyCode();
199         if (FTPReply.isPositiveCompletion(reply)) {
200           break;
201         } else {
202           disconnect();
203           result = CONNECTION_IN_ERROR + ": " + reply;
204         }
205       }
206       final int reply = ftpClient.getReplyCode();
207       if (!FTPReply.isPositiveCompletion(reply)) {
208         disconnect();
209         result = CONNECTION_IN_ERROR + ": " + reply;
210         logger.error(result);
211         return false;
212       }
213       try {
214         if (acct == null) {
215           // no account
216           if (!ftpClient.login(user, pwd)) {
217             logout();
218             result = LOGIN_IN_ERROR;
219             logger.error(result);
220             return false;
221           }
222         } else if (!ftpClient.login(user, pwd, acct)) {
223           logout();
224           result = LOGIN_IN_ERROR;
225           logger.error(result);
226           return false;
227         }
228       } catch (final IOException e) {
229         result = LOGIN_IN_ERROR;
230         logger.error(result + ": {}", e.getMessage());
231         return false;
232       }
233       ftpClient.setUseEPSVwithIPv4(false);
234       if (ssl == 1) {
235         // now send request for PROT (AUTH already sent)
236         try {
237           ((FTPSClient) ftpClient).execPBSZ(0);
238           logger.debug("PBSZ 0");
239           ((FTPSClient) ftpClient).execPROT("P");
240           logger.debug("Info: {}",
241                        ((FTPSClient) ftpClient).getEnableSessionCreation());
242         } catch (final IOException e) {
243           logout();
244           result = "Explicit SSL in error";
245           logger.error(result + ": {}", e.getMessage());
246           return false;
247         }
248       }
249       try {
250         ftpClient.setFileType(FTP.BINARY_FILE_TYPE);
251       } catch (final IOException e1) {
252         result = SET_BINARY_IN_ERROR;
253         logger.error(result + ": {}", e1.getMessage());
254         return false;
255       }
256       changeMode(isPassive);
257       isActive = true;
258       return true;
259     } finally {
260       if (!isActive && ftpClient.getDataConnectionMode() ==
261                        FTPClient.ACTIVE_LOCAL_DATA_CONNECTION_MODE) {
262         disconnect();
263       }
264     }
265   }
266 
267   @Override
268   public final void logout() {
269     result = null;
270     try {
271       ftpClient.logout();
272     } catch (final IOException e) {
273       // do nothing
274     } finally {
275       try {
276         ftpClient.disconnect();
277       } catch (final IOException f) {
278         // do nothing
279       }
280     }
281   }
282 
283   @Override
284   public final void disconnect() {
285     result = null;
286     try {
287       ftpClient.disconnect();
288     } catch (final IOException e) {
289       logger.debug(DISCONNECTION_IN_ERROR, e);
290     }
291   }
292 
293   @Override
294   public final boolean makeDir(final String newDir) {
295     result = null;
296     try {
297       return ftpClient.makeDirectory(newDir);
298     } catch (final IOException e) {
299       try {
300         reconnect();
301         return ftpClient.makeDirectory(newDir);
302       } catch (final IOException e1) {
303         SysErrLogger.FAKE_LOGGER.ignoreLog(e1);
304       }
305       result = MKDIR_IN_ERROR;
306       logger.error(result + ": {}", e.getMessage());
307       waitAfterDataCommand();
308       return false;
309     }
310   }
311 
312   @Override
313   public final boolean changeDir(final String newDir) {
314     result = null;
315     try {
316       directory = newDir;
317       return ftpClient.changeWorkingDirectory(newDir);
318     } catch (final IOException e) {
319       try {
320         reconnect();
321         return ftpClient.changeWorkingDirectory(newDir);
322       } catch (final IOException e1) {
323         SysErrLogger.FAKE_LOGGER.ignoreLog(e1);
324       }
325       result = CHDIR_IN_ERROR;
326       logger.error(result + ": {}", e.getMessage());
327       return false;
328     }
329   }
330 
331   @Override
332   public final boolean changeFileType(final boolean binaryTransfer) {
333     result = null;
334     try {
335       return internalChangeFileType(binaryTransfer);
336     } catch (final IOException e) {
337       try {
338         reconnect();
339         return internalChangeFileType(binaryTransfer);
340       } catch (final IOException e1) {
341         SysErrLogger.FAKE_LOGGER.ignoreLog(e1);
342       }
343       result = FILE_TYPE_IN_ERROR;
344       logger.error(result + ": {}", e.getMessage());
345       return false;
346     }
347   }
348 
349   private boolean internalChangeFileType(final boolean binaryTransfer)
350       throws IOException {
351     if (binaryTransfer) {
352       if (!ftpClient.setFileType(FTP.BINARY_FILE_TYPE)) {
353         reconnect();
354         return ftpClient.setFileType(FTP.BINARY_FILE_TYPE);
355       }
356       return true;
357     } else {
358       if (!ftpClient.setFileType(FTP.ASCII_FILE_TYPE)) {
359         reconnect();
360         if (directory != null) {
361           changeDir(directory);
362         }
363         return ftpClient.setFileType(FTP.ASCII_FILE_TYPE);
364       }
365       return true;
366     }
367   }
368 
369   @Override
370   public final void changeMode(final boolean passive) {
371     result = null;
372     isPassive = passive;
373     if (isPassive) {
374       ftpClient.enterLocalPassiveMode();
375     } else {
376       ftpClient.enterLocalActiveMode();
377     }
378     waitAfterDataCommand();
379   }
380 
381   @Override
382   public void compressionMode(final boolean compression) {
383     logger.warn("Z Compression not supported by Apache mode");
384   }
385 
386   @Override
387   public final boolean store(final String local, final String remote) {
388     return transferFile(local, remote, 1);
389   }
390 
391   @Override
392   public final boolean store(final InputStream local, final String remote) {
393     return transferFile(local, remote, 1);
394   }
395 
396   @Override
397   public final boolean append(final String local, final String remote) {
398     return transferFile(local, remote, 2);
399   }
400 
401   @Override
402   public final boolean append(final InputStream local, final String remote) {
403     return transferFile(local, remote, 2);
404   }
405 
406   @Override
407   public final boolean retrieve(final String local, final String remote) {
408     return transferFile(local, remote, -1);
409   }
410 
411   @Override
412   public final boolean retrieve(final OutputStream local, final String remote) {
413     return transferFile(local, remote);
414   }
415 
416   private boolean internalTransferFile(final String local, final String remote,
417                                        final int getStoreOrAppend)
418       throws FileNotFoundException {
419     OutputStream output = null;
420     FileInputStream fileInputStream = null;
421     try {
422       if (getStoreOrAppend > 0) {
423         fileInputStream = new FileInputStream(local);
424         return transferFile(fileInputStream, remote, getStoreOrAppend);
425       } else {
426         if (local == null) {
427           // test
428           logger.debug("Will DLD nullStream: {}", remote);
429           output = new NullOutputStream();
430         } else {
431           logger.debug("Will DLD to local: {} into {}", remote, local);
432           output = new FileOutputStream(local);
433         }
434         return transferFile(output, remote);
435       }
436     } finally {
437       FileUtils.close(output);
438       FileUtils.close(fileInputStream);
439     }
440   }
441 
442   @Override
443   public final boolean transferFile(final String local, final String remote,
444                                     final int getStoreOrAppend) {
445     result = null;
446     try {
447       if (!internalTransferFile(local, remote, getStoreOrAppend)) {
448         reconnect();
449         return internalTransferFile(local, remote, getStoreOrAppend);
450       }
451       return true;
452     } catch (final IOException e) {
453       try {
454         reconnect();
455         return internalTransferFile(local, remote, getStoreOrAppend);
456       } catch (final IOException e1) {
457         SysErrLogger.FAKE_LOGGER.ignoreLog(e1);
458       }
459       result = CANNOT_FINALIZE_TRANSFER_OPERATION;
460       logger.error(result + ": {}", e.getMessage());
461       return false;
462     }
463   }
464 
465   private boolean internalTransferFile(final InputStream local,
466                                        final String remote,
467                                        final int getStoreOrAppend)
468       throws IOException {
469     final boolean status;
470     if (getStoreOrAppend == 1) {
471       status = ftpClient.storeFile(remote, local);
472     } else {
473       // append
474       status = ftpClient.appendFile(remote, local);
475     }
476     if (!status) {
477       result = CANNOT_FINALIZE_STORE_LIKE_OPERATION;
478       logger.error(result);
479       return false;
480     }
481     return true;
482   }
483 
484   @Override
485   public final boolean transferFile(final InputStream local,
486                                     final String remote,
487                                     final int getStoreOrAppend) {
488     result = null;
489     try {
490       if (!internalTransferFile(local, remote, getStoreOrAppend)) {
491         reconnect();
492         return internalTransferFile(local, remote, getStoreOrAppend);
493       }
494       return true;
495     } catch (final IOException e) {
496       try {
497         reconnect();
498         return internalTransferFile(local, remote, getStoreOrAppend);
499       } catch (final IOException e1) {
500         SysErrLogger.FAKE_LOGGER.ignoreLog(e1);
501       }
502       result = CANNOT_FINALIZE_STORE_LIKE_OPERATION;
503       logger.error(result + ": {}", e.getMessage());
504       return false;
505     } finally {
506       waitAfterDataCommand();
507     }
508   }
509 
510   @Override
511   public final boolean transferFile(final OutputStream local,
512                                     final String remote) {
513     result = null;
514     boolean status;
515     try {
516       status = ftpClient.retrieveFile(remote, local);
517       if (!status) {
518         reconnect();
519         status = ftpClient.retrieveFile(remote, local);
520         if (!status) {
521           result = CANNOT_FINALIZE_RETRIEVE_LIKE_OPERATION;
522           logger.error(result);
523           return false;
524         }
525       }
526       return true;
527     } catch (final IOException e) {
528       try {
529         reconnect();
530         status = ftpClient.retrieveFile(remote, local);
531         if (!status) {
532           result = CANNOT_FINALIZE_RETRIEVE_LIKE_OPERATION;
533           logger.error(result);
534           return false;
535         }
536         return true;
537       } catch (final IOException e1) {
538         SysErrLogger.FAKE_LOGGER.ignoreLog(e1);
539       }
540       result = CANNOT_FINALIZE_RETRIEVE_LIKE_OPERATION;
541       logger.error(result + ": {}", e.getMessage());
542       return false;
543     } finally {
544       waitAfterDataCommand();
545     }
546   }
547 
548   private String[] internalListFiles(final String remote, final boolean mlist)
549       throws IOException {
550     final FTPFile[] list;
551     if (mlist) {
552       list = ftpClient.listFiles(remote);
553     } else {
554       list = ftpClient.mlistDir(remote);
555     }
556     final String[] results = new String[list.length];
557     int i = 0;
558     for (final FTPFile file : list) {
559       results[i] = file.toFormattedString();
560       i++;
561     }
562     return results;
563   }
564 
565   @Override
566   public final String[] listFiles(final String remote) {
567     result = null;
568     try {
569       return internalListFiles(remote, false);
570     } catch (final IOException e) {
571       try {
572         reconnect();
573         return internalListFiles(remote, false);
574       } catch (final IOException e1) {
575         SysErrLogger.FAKE_LOGGER.ignoreLog(e1);
576       }
577       result = CANNOT_FINALIZE_TRANSFER_OPERATION;
578       logger.error(result + ": {}", e.getMessage());
579       return null;
580     } finally {
581       waitAfterDataCommand();
582     }
583   }
584 
585   @Override
586   public final String[] listFiles() {
587     return listFiles((String) null);
588   }
589 
590   @Override
591   public final String[] mlistFiles(final String remote) {
592     result = null;
593     try {
594       return internalListFiles(remote, true);
595     } catch (final IOException e) {
596       try {
597         reconnect();
598         return internalListFiles(remote, true);
599       } catch (final IOException e1) {
600         SysErrLogger.FAKE_LOGGER.ignoreLog(e1);
601       }
602       result = CANNOT_FINALIZE_TRANSFER_OPERATION;
603       logger.error(result + ": {}", e.getMessage());
604       return null;
605     } finally {
606       waitAfterDataCommand();
607     }
608   }
609 
610   @Override
611   public final String[] mlistFiles() {
612     return mlistFiles((String) null);
613   }
614 
615   @Override
616   public final String[] features() {
617     result = null;
618     try {
619       return internalFeatures();
620     } catch (final IOException e) {
621       try {
622         reconnect();
623         return internalFeatures();
624       } catch (final IOException e1) {
625         SysErrLogger.FAKE_LOGGER.ignoreLog(e1);
626       }
627       result = CANNOT_EXECUTE_OPERATION_FEATURE;
628       logger.error(result + ": {}", e.getMessage());
629       return null;
630     }
631   }
632 
633   private String[] internalFeatures() throws IOException {
634     if (ftpClient.features()) {
635       final String resultNew = ftpClient.getReplyString();
636       return resultNew.split("\\n");
637     }
638     return null;
639   }
640 
641   @Override
642   public final boolean featureEnabled(final String feature) {
643     result = null;
644     try {
645       return internalFeatureEnabled(feature);
646     } catch (final IOException e) {
647       try {
648         reconnect();
649         return internalFeatureEnabled(feature);
650       } catch (final IOException e1) {
651         SysErrLogger.FAKE_LOGGER.ignoreLog(e1);
652       }
653       result = CANNOT_EXECUTE_OPERATION_FEATURE;
654       logger.error(result + ": {}", e.getMessage());
655       return false;
656     }
657   }
658 
659   private boolean internalFeatureEnabled(final String feature)
660       throws IOException {
661     if (ftpClient.featureValue(feature) == null) {
662       final String resultNew = ftpClient.getReplyString();
663       return resultNew.contains(feature.toUpperCase());
664     }
665     return true;
666   }
667 
668   @Override
669   public boolean deleteFile(final String remote) {
670     result = null;
671     try {
672       ftpClient.deleteFile(remote);
673       return true;
674     } catch (final IOException e) {
675       try {
676         reconnect();
677         ftpClient.deleteFile(remote);
678         return true;
679       } catch (final IOException e1) {
680         SysErrLogger.FAKE_LOGGER.ignoreLog(e1);
681       }
682       result = CANNOT_EXECUTE_OPERATION_SITE;
683       logger.error(result + ": {}", e.getMessage());
684       return false;
685     }
686   }
687 
688   @Override
689   public final String[] executeCommand(final String params) {
690     result = null;
691     try {
692       return internalExecuteCommand(params);
693     } catch (final IOException e) {
694       try {
695         reconnect();
696         return internalExecuteCommand(params);
697       } catch (final IOException e1) {
698         SysErrLogger.FAKE_LOGGER.ignoreLog(e1);
699       }
700       result = CANNOT_EXECUTE_OPERATION_SITE;
701       logger.error(result + ": {}", e.getMessage());
702       return null;
703     }
704   }
705 
706   private String[] internalExecuteCommand(final String params)
707       throws IOException {
708     final int pos = params.indexOf(' ');
709     String command = params;
710     String args = null;
711     if (pos > 0) {
712       command = params.substring(0, pos);
713       args = params.substring(pos + 1);
714     }
715     String[] results = ftpClient.doCommandAsStrings(command, args);
716     if (results == null) {
717       results = new String[1];
718       results[0] = ftpClient.getReplyString();
719     }
720     return results;
721   }
722 
723   @Override
724   public final String[] executeSiteCommand(final String params) {
725     result = null;
726     try {
727       return internalExecuteSiteCommand(params);
728     } catch (final IOException e) {
729       try {
730         reconnect();
731         return internalExecuteSiteCommand(params);
732       } catch (final IOException e1) {
733         SysErrLogger.FAKE_LOGGER.ignoreLog(e1);
734       }
735       result = CANNOT_EXECUTE_OPERATION_SITE;
736       logger.error(result + ": {}", e.getMessage());
737       return null;
738     }
739   }
740 
741   private String[] internalExecuteSiteCommand(final String params)
742       throws IOException {
743     String[] results = ftpClient.doCommandAsStrings("SITE", params);
744     if (results == null) {
745       results = new String[1];
746       results[0] = ftpClient.getReplyString();
747     }
748     return results;
749   }
750 
751   @Override
752   public final void noop() {
753     try {
754       ftpClient.noop();
755     } catch (final IOException e) {
756       try {
757         reconnect();
758         ftpClient.noop();
759       } catch (final IOException e1) {
760         SysErrLogger.FAKE_LOGGER.ignoreLog(e1);
761       }
762       result = NOOP_ERROR;
763       logger.error(result + ": {}", e.getMessage());
764     }
765   }
766 
767   /**
768    * Used on Data Commands to prevent too fast command iterations
769    */
770   static void waitAfterDataCommand() {
771     if (DEFAULT_WAIT > 0) {
772       try {
773         Thread.sleep(DEFAULT_WAIT);
774       } catch (final InterruptedException e) { //NOSONAR
775         SysErrLogger.FAKE_LOGGER.ignoreLog(e);
776       }
777     } else {
778       Thread.yield();
779     }
780   }
781 }