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 it.sauronsoftware.ftp4j.FTPAbortedException;
23  import it.sauronsoftware.ftp4j.FTPClient;
24  import it.sauronsoftware.ftp4j.FTPCommunicationListener;
25  import it.sauronsoftware.ftp4j.FTPConnector;
26  import it.sauronsoftware.ftp4j.FTPDataTransferException;
27  import it.sauronsoftware.ftp4j.FTPException;
28  import it.sauronsoftware.ftp4j.FTPFile;
29  import it.sauronsoftware.ftp4j.FTPIllegalReplyException;
30  import it.sauronsoftware.ftp4j.FTPListParseException;
31  import it.sauronsoftware.ftp4j.FTPReply;
32  import org.waarp.common.file.FileUtils;
33  import org.waarp.common.logging.SysErrLogger;
34  import org.waarp.common.logging.WaarpLogger;
35  import org.waarp.common.logging.WaarpLoggerFactory;
36  import org.waarp.common.utility.SystemPropertyUtil;
37  
38  import javax.net.ssl.SSLContext;
39  import javax.net.ssl.SSLSocketFactory;
40  import javax.net.ssl.TrustManager;
41  import javax.net.ssl.X509TrustManager;
42  import java.io.File;
43  import java.io.FileInputStream;
44  import java.io.FileNotFoundException;
45  import java.io.FileOutputStream;
46  import java.io.IOException;
47  import java.io.InputStream;
48  import java.io.OutputStream;
49  import java.net.SocketException;
50  import java.security.KeyManagementException;
51  import java.security.NoSuchAlgorithmException;
52  import java.security.cert.X509Certificate;
53  
54  import static org.waarp.common.digest.WaarpBC.*;
55  
56  /**
57   * FTP client using FTP4J model (working in all modes)
58   */
59  public class WaarpFtp4jClient implements WaarpFtpClientInterface {
60    /**
61     * Internal Logger
62     */
63    private static final WaarpLogger logger =
64        WaarpLoggerFactory.getLogger(WaarpFtp4jClient.class);
65    private static final int DEFAULT_WAIT = 1;
66  
67    static {
68      initializedTlsContext();
69    }
70  
71    final String server;
72    int port = 21;
73    final String user;
74    final String pwd;
75    final String acct;
76    final int timeout;
77    final int keepalive;
78    boolean isPassive;
79    final int ssl; // -1 native, 1 auth
80    protected FTPClient ftpClient;
81    protected String result;
82    protected String directory = null;
83  
84    /**
85     * @param server
86     * @param port
87     * @param user
88     * @param pwd
89     * @param acct
90     * @param isPassive
91     * @param ssl -1 native, 1 auth
92     * @param timeout
93     */
94    public WaarpFtp4jClient(final String server, final int port,
95                            final String user, final String pwd,
96                            final String acct, final boolean isPassive,
97                            final int ssl, final int keepalive,
98                            final int timeout) {
99      this.server = server;
100     this.port = port;
101     this.user = user;
102     this.pwd = pwd;
103     this.acct = acct;
104     this.isPassive = isPassive;
105     this.ssl = ssl;
106     this.keepalive = keepalive;
107     this.timeout = timeout;
108     ftpClient = new FTPClient();
109     if (this.ssl != 0) {
110       // implicit or explicit
111       final TrustManager[] trustManager = {
112           new X509TrustManager() {
113             @Override
114             public X509Certificate[] getAcceptedIssuers() {
115               return null;
116             }
117 
118             @Override
119             public final void checkClientTrusted(final X509Certificate[] certs,
120                                                  //NOSONAR
121                                                  final String authType) {
122               // nothing
123             }
124 
125             @Override
126             public final void checkServerTrusted(final X509Certificate[] certs,
127 //NOSONAR
128                                                  final String authType) {
129               // nothing
130             }
131           }
132       };
133       final SSLContext sslContext;
134       try {
135         sslContext = getInstanceJDK();
136         sslContext.init(null, trustManager, getSecureRandom());
137       } catch (final NoSuchAlgorithmException e) {
138         throw new IllegalArgumentException("Bad algorithm", e);
139       } catch (final KeyManagementException e) {
140         throw new IllegalArgumentException("Bad KeyManagement", e);
141       } catch (final Exception e) {
142         throw new IllegalArgumentException("Bad Provider", e);
143       }
144       final SSLSocketFactory sslSocketFactory = sslContext.getSocketFactory();
145       ftpClient.setSSLSocketFactory(sslSocketFactory);
146       if (this.ssl < 0) {
147         ftpClient.setSecurity(FTPClient.SECURITY_FTPS);
148       } else {
149         ftpClient.setSecurity(FTPClient.SECURITY_FTPES);
150       }
151     } else {
152       ftpClient = new FTPClient();
153     }
154     if (timeout > 0) {
155       System.setProperty("ftp4j.activeDataTransfer.acceptTimeout",
156                          String.valueOf(timeout));
157     }
158     System.setProperty("ftp4j.activeDataTransfer.hostAddress", "127.0.0.1");
159 
160     ftpClient.addCommunicationListener(new FTPCommunicationListener() {
161       @Override
162       public final void sent(final String arg0) {
163         logger.debug("Command: {}", arg0);
164       }
165 
166       @Override
167       public final void received(final String arg0) {
168         logger.debug("Answer: {}", arg0);
169       }
170     });
171     final FTPConnector connector = ftpClient.getConnector();
172     int timeoutDefault = timeout > 0? timeout / 1000 : 30;
173     connector.setCloseTimeout(timeoutDefault);
174     connector.setReadTimeout(timeoutDefault);
175     connector.setConnectionTimeout(timeoutDefault);
176     connector.setUseSuggestedAddressForDataConnections(true);
177   }
178 
179   @Override
180   public void setReportActiveExternalIPAddress(final String ipAddress) {
181     if (ipAddress != null) {
182       SystemPropertyUtil.set("ftp4j.activeDataTransfer.hostAddress", ipAddress);
183     } else {
184       SystemPropertyUtil.clear("ftp4j.activeDataTransfer.hostAddress");
185     }
186   }
187 
188   @Override
189   public void setActiveDataTransferPortRange(final int from, final int to) {
190     if (from <= 0 || to <= 0) {
191       SystemPropertyUtil.clear("ftp4j.activeDataTransfer.portRange");
192     } else {
193       SystemPropertyUtil.set("ftp4j.activeDataTransfer.portRange",
194                              from + "-" + to);
195     }
196   }
197 
198   @Override
199   public final String getResult() {
200     return result;
201   }
202 
203   private void reconnect() {
204     ftpClient.setAutoNoopTimeout(0);
205     try {
206       ftpClient.logout();
207     } catch (final Exception e) {
208       // do nothing
209     } finally {
210       disconnect();
211     }
212     waitAfterDataCommand();
213     connect();
214     if (directory != null) {
215       changeDir(directory);
216     }
217   }
218 
219   @Override
220   public final boolean connect() {
221     result = null;
222     boolean isActive = false;
223     try {
224       waitAfterDataCommand();
225       Exception lastExcemption = null;
226       for (int i = 0; i < 5; i++) {
227         try {
228           ftpClient.connect(server, port);
229           lastExcemption = null;
230           break;
231         } catch (final SocketException e) {
232           result = CONNECTION_IN_ERROR;
233           lastExcemption = e;
234         } catch (final Exception e) {
235           result = CONNECTION_IN_ERROR;
236           lastExcemption = e;
237         }
238         waitAfterDataCommand();
239       }
240       if (lastExcemption != null) {
241         logger.error(result + ": {}", lastExcemption.getMessage());
242         return false;
243       }
244       try {
245         if (acct == null) {
246           // no account
247           ftpClient.login(user, pwd);
248         } else {
249           ftpClient.login(user, pwd, acct);
250         }
251       } catch (final Exception e) {
252         logout();
253         result = LOGIN_IN_ERROR;
254         logger.error(result);
255         return false;
256       }
257       try {
258         ftpClient.setType(FTPClient.TYPE_BINARY);
259       } catch (final IllegalArgumentException e1) {
260         result = SET_BINARY_IN_ERROR;
261         logger.error(result + ": {}", e1.getMessage());
262         return false;
263       }
264       changeMode(isPassive);
265       if (keepalive > 0) {
266         ftpClient.setAutoNoopTimeout(keepalive);
267       }
268       isActive = true;
269       return true;
270     } finally {
271       if (!isActive && !ftpClient.isPassive()) {
272         disconnect();
273       }
274     }
275   }
276 
277   @Override
278   public final void logout() {
279     result = null;
280     ftpClient.setAutoNoopTimeout(0);
281     logger.debug("QUIT");
282     if (executeCommand("QUIT") == null) {
283       try {
284         ftpClient.logout();
285       } catch (final Exception e) {
286         // do nothing
287       } finally {
288         disconnect();
289       }
290     }
291   }
292 
293   @Override
294   public final void disconnect() {
295     result = null;
296     ftpClient.setAutoNoopTimeout(0);
297     try {
298       ftpClient.disconnect(false);
299     } catch (final Exception e) {
300       logger.debug(DISCONNECTION_IN_ERROR, e);
301     }
302   }
303 
304   @Override
305   public final boolean makeDir(final String newDir) {
306     result = null;
307     try {
308       ftpClient.createDirectory(newDir);
309       return true;
310     } catch (final Exception e) {
311       try {
312         reconnect();
313         ftpClient.createDirectory(newDir);
314         return true;
315       } catch (final Exception e1) {
316         SysErrLogger.FAKE_LOGGER.ignoreLog(e1);
317       }
318       result = MKDIR_IN_ERROR;
319       logger.error(result + ": {}", e.getMessage());
320       waitAfterDataCommand();
321       return false;
322     }
323   }
324 
325   @Override
326   public final boolean changeDir(final String newDir) {
327     result = null;
328     try {
329       directory = newDir;
330       ftpClient.changeDirectory(newDir);
331       return true;
332     } catch (final IOException e) {
333       try {
334         reconnect();
335         ftpClient.changeDirectory(newDir);
336         return true;
337       } catch (final Exception e1) {
338         SysErrLogger.FAKE_LOGGER.ignoreLog(e1);
339       }
340       result = CHDIR_IN_ERROR;
341       logger.error(result + ": {}", e.getMessage());
342       return false;
343     } catch (final Exception e) {
344       try {
345         reconnect();
346         ftpClient.changeDirectory(newDir);
347         return true;
348       } catch (final Exception e1) {
349         SysErrLogger.FAKE_LOGGER.ignoreLog(e1);
350       }
351       result = CHDIR_IN_ERROR;
352       logger.error(result + ": {}", e.getMessage());
353       return false;
354     }
355   }
356 
357   @Override
358   public final boolean changeFileType(final boolean binaryTransfer) {
359     result = null;
360     try {
361       if (binaryTransfer) {
362         ftpClient.setType(FTPClient.TYPE_BINARY);
363       } else {
364         ftpClient.setType(FTPClient.TYPE_TEXTUAL);
365       }
366       return true;
367     } catch (final IllegalArgumentException e) {
368       result = FILE_TYPE_IN_ERROR;
369       logger.error(result + ": {}", e.getMessage());
370       return false;
371     }
372   }
373 
374   @Override
375   public final void changeMode(final boolean passive) {
376     result = null;
377     isPassive = passive;
378     ftpClient.setPassive(passive);
379     waitAfterDataCommand();
380   }
381 
382   @Override
383   public void compressionMode(final boolean compression) {
384     if (compression) {
385       if (ftpClient.isCompressionSupported()) {
386         try {
387           ftpClient.setType(FTPClient.TYPE_BINARY);
388         } catch (final IllegalArgumentException e1) {
389           result = SET_BINARY_IN_ERROR;
390           logger.error(result + ": {}", e1.getMessage());
391         }
392         ftpClient.setCompressionEnabled(true);
393       } else {
394         logger.warn("Z Compression not supported by Server");
395         ftpClient.setCompressionEnabled(false);
396       }
397     } else {
398       ftpClient.setCompressionEnabled(false);
399     }
400   }
401 
402   @Override
403   public final boolean store(final String local, final String remote) {
404     return transferFile(local, remote, 1);
405   }
406 
407   @Override
408   public final boolean store(final InputStream local, final String remote) {
409     return transferFile(local, remote, 1);
410   }
411 
412   @Override
413   public final boolean append(final String local, final String remote) {
414     return transferFile(local, remote, 2);
415   }
416 
417   @Override
418   public final boolean append(final InputStream local, final String remote) {
419     return transferFile(local, remote, 2);
420   }
421 
422   @Override
423   public final boolean retrieve(final String local, final String remote) {
424     return transferFile(local, remote, -1);
425   }
426 
427   @Override
428   public final boolean retrieve(final OutputStream local, final String remote) {
429     return transferFile(local, remote);
430   }
431 
432   @Override
433   public final boolean transferFile(final String local, final String remote,
434                                     final int getStoreOrAppend) {
435     result = null;
436     try {
437       if (!internalTransferFile(local, remote, getStoreOrAppend)) {
438         reconnect();
439         return internalTransferFile(local, remote, getStoreOrAppend);
440       }
441       return true;
442     } catch (final IOException e) {
443       try {
444         reconnect();
445         return internalTransferFile(local, remote, getStoreOrAppend);
446       } catch (final Exception e1) {
447         SysErrLogger.FAKE_LOGGER.ignoreLog(e1);
448       }
449       result = CANNOT_FINALIZE_TRANSFER_OPERATION;
450       logger.error(result + ": {}", e.getMessage());
451       return false;
452     }
453   }
454 
455   private boolean internalTransferFile(final String local, final String remote,
456                                        final int getStoreOrAppend)
457       throws FileNotFoundException {
458     if (getStoreOrAppend > 0) {
459       final File from = new File(local);
460       logger.debug("Will STOR: {}", from);
461       FileInputStream stream = null;
462       try {
463         stream = new FileInputStream(local);
464         return transferFile(stream, remote, getStoreOrAppend);
465       } finally {
466         FileUtils.close(stream);
467       }
468     } else {
469       OutputStream outputStream = null;
470       if (local == null) {
471         // test
472         logger.debug("Will DLD nullStream: {}", remote);
473         outputStream = new NullOutputStream();
474       } else {
475         logger.debug("Will DLD to local: {} into {}", remote, local);
476         outputStream = new FileOutputStream(local);
477       }
478       try {
479         return transferFile(outputStream, remote);
480       } finally {
481         FileUtils.close(outputStream);
482       }
483     }
484   }
485 
486   @Override
487   public final boolean transferFile(final InputStream local,
488                                     final String remote,
489                                     final int getStoreOrAppend) {
490     result = null;
491     result = CANNOT_FINALIZE_STORE_LIKE_OPERATION;
492     logger.debug("Will STOR to: {}", remote);
493     try {
494       internalTransferFile(local, remote, getStoreOrAppend);
495       return true;
496     } catch (final Exception e) {
497       try {
498         reconnect();
499         return internalTransferFile(local, remote, getStoreOrAppend);
500       } catch (final Exception e1) {
501         SysErrLogger.FAKE_LOGGER.ignoreLog(e1);
502       }
503       logger.error(result + ": {}", e.getMessage());
504       return false;
505     } finally {
506       waitAfterDataCommand();
507     }
508   }
509 
510   private boolean internalTransferFile(final InputStream local,
511                                        final String remote,
512                                        final int getStoreOrAppend)
513       throws IOException, FTPIllegalReplyException, FTPException,
514              FTPDataTransferException, FTPAbortedException {
515     if (getStoreOrAppend == 1) {
516       ftpClient.upload(remote, local, 0, 0, null);
517     } else {
518       // append
519       ftpClient.append(remote, local, 0, null);
520     }
521     result = null;
522     return true;
523   }
524 
525   @Override
526   public final boolean transferFile(final OutputStream local,
527                                     final String remote) {
528     result = null;
529     result = CANNOT_FINALIZE_RETRIEVE_LIKE_OPERATION;
530     logger.debug("Will DLD nullStream: {}", remote);
531     try {
532       ftpClient.download(remote, local, 0, null);
533       result = null;
534       return true;
535     } catch (final Exception e) {
536       try {
537         reconnect();
538         ftpClient.download(remote, local, 0, null);
539         result = null;
540         return true;
541       } catch (final Exception e1) {
542         SysErrLogger.FAKE_LOGGER.ignoreLog(e1);
543       }
544       logger.error(result + ": {}", e.getMessage(), e);
545       return false;
546     } finally {
547       waitAfterDataCommand();
548     }
549   }
550 
551   @Override
552   public final String[] listFiles(final String remote) {
553     result = null;
554     ftpClient.setMLSDPolicy(FTPClient.MLSD_NEVER);
555     try {
556       return internalListFiles(remote);
557     } catch (final Exception e) {
558       try {
559         reconnect();
560         return internalListFiles(remote);
561       } catch (final Exception e1) {
562         SysErrLogger.FAKE_LOGGER.ignoreLog(e1);
563       }
564       result = CANNOT_FINALIZE_TRANSFER_OPERATION;
565       logger.error(result + ": {}", e.getMessage());
566       return null;
567     } finally {
568       waitAfterDataCommand();
569     }
570   }
571 
572   private String[] internalListFiles(final String remote)
573       throws IOException, FTPIllegalReplyException, FTPException,
574              FTPDataTransferException, FTPAbortedException,
575              FTPListParseException {
576     final FTPFile[] list = ftpClient.list(remote);
577     final String[] results = new String[list.length];
578     int i = 0;
579     for (final FTPFile file : list) {
580       results[i] = file.toString();
581       i++;
582     }
583     return results;
584   }
585 
586   @Override
587   public final String[] listFiles() {
588     return listFiles((String) null);
589   }
590 
591   @Override
592   public final String[] mlistFiles(final String remote) {
593     ftpClient.setMLSDPolicy(FTPClient.MLSD_ALWAYS);
594     return listFiles(remote);
595   }
596 
597   @Override
598   public final String[] mlistFiles() {
599     return mlistFiles((String) null);
600   }
601 
602 
603   @Override
604   public final String[] features() {
605     result = null;
606     try {
607       final FTPReply reply = ftpClient.sendCustomCommand("FEAT");
608       return reply.getMessages();
609     } catch (final IOException e) {
610       try {
611         reconnect();
612         final FTPReply reply = ftpClient.sendCustomCommand("FEAT");
613         return reply.getMessages();
614       } catch (final Exception e1) {
615         SysErrLogger.FAKE_LOGGER.ignoreLog(e1);
616       }
617       result = CANNOT_EXECUTE_OPERATION_FEATURE;
618       logger.error(result + ": {}", e.getMessage());
619       return null;
620     } catch (final Exception e) {
621       try {
622         reconnect();
623         final FTPReply reply = ftpClient.sendCustomCommand("FEAT");
624         return reply.getMessages();
625       } catch (final Exception e1) {
626         SysErrLogger.FAKE_LOGGER.ignoreLog(e1);
627       }
628       result = CANNOT_EXECUTE_OPERATION_FEATURE;
629       logger.error(result + ": {}", e.getMessage());
630       return null;
631     }
632   }
633 
634   @Override
635   public final boolean featureEnabled(final String feature) {
636     result = null;
637     try {
638       return internalFeatureEnabled(feature);
639     } catch (final Exception e) {
640       try {
641         reconnect();
642         return internalFeatureEnabled(feature);
643       } catch (final Exception e1) {
644         SysErrLogger.FAKE_LOGGER.ignoreLog(e1);
645       }
646       result = CANNOT_EXECUTE_OPERATION_FEATURE;
647       logger.error(result + ": {}", e.getMessage());
648       return false;
649     }
650   }
651 
652   private boolean internalFeatureEnabled(final String feature)
653       throws IOException, FTPIllegalReplyException {
654     final FTPReply reply = ftpClient.sendCustomCommand("FEAT");
655     final String[] msg = reply.getMessages();
656     for (final String string : msg) {
657       if (string.contains(feature.toUpperCase())) {
658         return true;
659       }
660     }
661     return false;
662   }
663 
664   @Override
665   public boolean deleteFile(final String remote) {
666     result = null;
667     try {
668       logger.debug("DELE {}", remote);
669       ftpClient.deleteFile(remote);
670       return true;
671     } catch (final Exception e) {
672       try {
673         reconnect();
674         ftpClient.deleteFile(remote);
675         return true;
676       } catch (final Exception e1) {
677         SysErrLogger.FAKE_LOGGER.ignoreLog(e1);
678       }
679       result = CANNOT_EXECUTE_OPERATION_SITE;
680       logger.error(result + ": {}", e.getMessage());
681       return false;
682     }
683   }
684 
685   @Override
686   public final String[] executeCommand(final String params) {
687     result = null;
688     try {
689       logger.debug(params);
690       FTPReply reply = ftpClient.sendCustomCommand(params);
691       if (!reply.isSuccessCode()) {
692         reconnect();
693         reply = ftpClient.sendCustomCommand(params);
694         if (!reply.isSuccessCode()) {
695           result = reply.toString();
696           return null;
697         }
698       }
699       return reply.getMessages();
700     } catch (final Exception e) {
701       try {
702         reconnect();
703         final FTPReply reply = ftpClient.sendCustomCommand(params);
704         if (!reply.isSuccessCode()) {
705           result = reply.toString();
706           return null;
707         }
708         return reply.getMessages();
709       } catch (final Exception e1) {
710         SysErrLogger.FAKE_LOGGER.ignoreLog(e1);
711       }
712       result = CANNOT_EXECUTE_OPERATION_SITE;
713       logger.error(result + ": {}", e.getMessage());
714       return null;
715     }
716   }
717 
718   @Override
719   public final String[] executeSiteCommand(final String params) {
720     result = null;
721     try {
722       logger.debug("SITE {}", params);
723       FTPReply reply = ftpClient.sendSiteCommand(params);
724       if (!reply.isSuccessCode()) {
725         reconnect();
726         reply = ftpClient.sendSiteCommand(params);
727         if (!reply.isSuccessCode()) {
728           result = reply.toString();
729           return null;
730         }
731       }
732       return reply.getMessages();
733     } catch (final Exception e) {
734       try {
735         reconnect();
736         final FTPReply reply = ftpClient.sendSiteCommand(params);
737         if (!reply.isSuccessCode()) {
738           result = reply.toString();
739           return null;
740         }
741         return reply.getMessages();
742       } catch (final Exception e1) {
743         SysErrLogger.FAKE_LOGGER.ignoreLog(e1);
744       }
745       result = CANNOT_EXECUTE_OPERATION_SITE;
746       logger.error(result + ": {}", e.getMessage());
747       return null;
748     }
749   }
750 
751   @Override
752   public final void noop() {
753     try {
754       ftpClient.noop();
755     } catch (final Exception e) {
756       try {
757         reconnect();
758         ftpClient.noop();
759       } catch (final Exception 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 }