View Javadoc
1   /*
2    * Copyright (c) MuleSoft, Inc.  All rights reserved.  http://www.mulesoft.com
3    * The software in this package is published under the terms of the CPAL v1.0
4    * license, a copy of which has been included with this distribution in the
5    * LICENSE.txt file.
6    */
7   package org.mule.transport.sftp;
8   
9   import org.mule.api.endpoint.ImmutableEndpoint;
10  import org.mule.transport.sftp.notification.SftpNotifier;
11  
12  import com.jcraft.jsch.Channel;
13  import com.jcraft.jsch.ChannelSftp;
14  import com.jcraft.jsch.ChannelSftp.LsEntry;
15  import com.jcraft.jsch.JSch;
16  import com.jcraft.jsch.JSchException;
17  import com.jcraft.jsch.Session;
18  import com.jcraft.jsch.SftpATTRS;
19  import com.jcraft.jsch.SftpException;
20  
21  import java.io.File;
22  import java.io.IOException;
23  import java.io.InputStream;
24  import java.util.ArrayList;
25  import java.util.List;
26  import java.util.Properties;
27  import java.util.Vector;
28  
29  import org.apache.commons.lang.NotImplementedException;
30  import org.apache.commons.logging.Log;
31  import org.apache.commons.logging.LogFactory;
32  
33  import static org.mule.transport.sftp.notification.SftpTransportNotification.SFTP_DELETE_ACTION;
34  import static org.mule.transport.sftp.notification.SftpTransportNotification.SFTP_GET_ACTION;
35  import static org.mule.transport.sftp.notification.SftpTransportNotification.SFTP_PUT_ACTION;
36  import static org.mule.transport.sftp.notification.SftpTransportNotification.SFTP_RENAME_ACTION;
37  
38  /**
39   * <code>SftpClient</code> Wrapper around jsch sftp library. Provides access to basic
40   * sftp commands.
41   */
42  
43  public class SftpClient
44  {
45      private Log logger = LogFactory.getLog(getClass());
46  
47      public static final String CHANNEL_SFTP = "sftp";
48  
49      public static final String STRICT_HOST_KEY_CHECKING = "StrictHostKeyChecking";
50  
51      private ChannelSftp channelSftp;
52  
53      private JSch jsch;
54      private SftpNotifier notifier;
55  
56      private Session session;
57  
58      private final String host;
59  
60      private int port = 22;
61  
62      private String home;
63  
64      // Keep track of the current working directory for improved logging.
65      private String currentDirectory = "";
66  
67      private static final Object lock = new Object();
68  
69      public SftpClient(String host)
70      {
71          this(host, null);
72      }
73  
74      public SftpClient(String host, SftpNotifier notifier)
75      {
76          this.host = host;
77          this.notifier = notifier;
78  
79          jsch = new JSch();
80      }
81  
82      public void changeWorkingDirectory(String wd) throws IOException
83      {
84          currentDirectory = wd;
85  
86          try
87          {
88              wd = getAbsolutePath(wd);
89              if (logger.isDebugEnabled())
90              {
91                  logger.debug("Attempting to cwd to: " + wd);
92              }
93              channelSftp.cd(wd);
94          }
95          catch (SftpException e)
96          {
97              String message = "Error '" + e.getMessage() + "' occurred when trying to CDW to '" + wd + "'.";
98              logger.error(message);
99              throw new IOException(message);
100         }
101     }
102 
103     /**
104      * Converts a relative path to an absolute path according to
105      * http://tools.ietf.org/html/draft-ietf-secsh-scp-sftp-ssh-uri-04.
106      *
107      * @param path relative path
108      * @return Absolute path
109      */
110     public String getAbsolutePath(String path)
111     {
112         if (path.startsWith("/~"))
113         {
114             return home + path.substring(2, path.length());
115         }
116 
117         // Already absolute!
118         return path;
119     }
120 
121     public void login(String user, String password) throws IOException
122     {
123         try
124         {
125             Properties hash = new Properties();
126             hash.put(STRICT_HOST_KEY_CHECKING, "no");
127 
128             session = jsch.getSession(user, host);
129             session.setConfig(hash);
130             session.setPort(port);
131             session.setPassword(password);
132             session.connect();
133 
134             Channel channel = session.openChannel(CHANNEL_SFTP);
135             channel.connect();
136 
137             channelSftp = (ChannelSftp) channel;
138             setHome(channelSftp.pwd());
139         }
140         catch (JSchException e)
141         {
142             logAndThrowLoginError(user, e);
143         }
144         catch (SftpException e)
145         {
146             logAndThrowLoginError(user, e);
147         }
148     }
149 
150     public void login(String user, String identityFile, String passphrase) throws IOException
151     {
152         // Lets first check that the identityFile exist
153         if (!new File(identityFile).exists())
154         {
155             throw new IOException("IdentityFile '" + identityFile + "' not found");
156         }
157 
158         try
159         {
160             if (passphrase == null || "".equals(passphrase))
161             {
162                 jsch.addIdentity(new File(identityFile).getAbsolutePath());
163             }
164             else
165             {
166                 jsch.addIdentity(new File(identityFile).getAbsolutePath(), passphrase);
167             }
168 
169             Properties hash = new Properties();
170             hash.put(STRICT_HOST_KEY_CHECKING, "no");
171 
172             session = jsch.getSession(user, host);
173             session.setConfig(hash);
174             session.setPort(port);
175             session.connect();
176 
177             Channel channel = session.openChannel(CHANNEL_SFTP);
178             channel.connect();
179 
180             channelSftp = (ChannelSftp) channel;
181             setHome(channelSftp.pwd());
182         }
183         catch (JSchException e)
184         {
185             logAndThrowLoginError(user, e);
186         }
187         catch (SftpException e)
188         {
189             logAndThrowLoginError(user, e);
190         }
191     }
192 
193     private void logAndThrowLoginError(String user, Exception e) throws IOException
194     {
195         logger.error("Error during login to " + user + "@" + host, e);
196         throw new IOException("Error during login to " + user + "@" + host + ": " + e.getMessage());
197     }
198 
199     public void setPort(int port)
200     {
201         this.port = port;
202     }
203 
204     public void rename(String filename, String dest) throws IOException
205     {
206         // Notify sftp rename file action
207         if (notifier != null)
208         {
209             notifier.notify(SFTP_RENAME_ACTION, "from: " + currentDirectory + "/" + filename + " - to: "
210                                                 + dest);
211         }
212 
213         String absolutePath = getAbsolutePath(dest);
214         try
215         {
216             if (logger.isDebugEnabled())
217             {
218                 logger.debug("Will try to rename " + currentDirectory + "/" + filename + " to "
219                              + absolutePath);
220             }
221             channelSftp.rename(filename, absolutePath);
222         }
223         catch (SftpException e)
224         {
225             throw new IOException(e.getMessage());
226             // throw new IOException("Error occured when renaming " +
227             // currentDirectory + "/" + filename + " to " + absolutePath +
228             // ". Error Message=" + e.getMessage());
229         }
230     }
231 
232     public void deleteFile(String fileName) throws IOException
233     {
234         // Notify sftp delete file action
235         if (notifier != null)
236         {
237             notifier.notify(SFTP_DELETE_ACTION, currentDirectory + "/" + fileName);
238         }
239 
240         try
241         {
242             if (logger.isDebugEnabled())
243             {
244                 logger.debug("Will try to delete " + fileName);
245             }
246             channelSftp.rm(fileName);
247         }
248         catch (SftpException e)
249         {
250             throw new IOException(e.getMessage());
251         }
252     }
253 
254     public void disconnect()
255     {
256         if (channelSftp != null)
257         {
258             channelSftp.disconnect();
259         }
260         if ((session != null) && session.isConnected())
261         {
262             session.disconnect();
263         }
264     }
265 
266     public boolean isConnected()
267     {
268         return (channelSftp != null) && channelSftp.isConnected() && !channelSftp.isClosed()
269                && (session != null) && session.isConnected();
270     }
271 
272     public String[] listFiles() throws IOException
273     {
274         return listFiles(".");
275     }
276 
277     public String[] listFiles(String path) throws IOException
278     {
279         return listDirectory(path, true, false);
280     }
281 
282     public String[] listDirectories() throws IOException
283     {
284         return listDirectory(".", false, true);
285     }
286 
287     public String[] listDirectories(String path) throws IOException
288     {
289         return listDirectory(path, false, true);
290     }
291 
292     private String[] listDirectory(String path, boolean includeFiles, boolean includeDirectories)
293         throws IOException
294     {
295         try
296         {
297             Vector vv = channelSftp.ls(path);
298             if (vv != null)
299             {
300                 List<String> ret = new ArrayList<String>();
301                 for (int i = 0; i < vv.size(); i++)
302                 {
303                     Object obj = vv.elementAt(i);
304                     if (obj instanceof com.jcraft.jsch.ChannelSftp.LsEntry)
305                     {
306                         LsEntry entry = (LsEntry) obj;
307                         if (includeFiles && !entry.getAttrs().isDir())
308                         {
309                             ret.add(entry.getFilename());
310                         }
311                         if (includeDirectories && entry.getAttrs().isDir())
312                         {
313                             if (!entry.getFilename().equals(".") && !entry.getFilename().equals(".."))
314                             {
315                                 ret.add(entry.getFilename());
316                             }
317                         }
318                     }
319                 }
320                 return ret.toArray(new String[ret.size()]);
321             }
322         }
323         catch (SftpException e)
324         {
325             throw new IOException(e.getMessage());
326         }
327         return null;
328     }
329 
330     // public boolean logout()
331     // {
332     // return true;
333     // }
334 
335     public InputStream retrieveFile(String fileName) throws IOException
336     {
337         // Notify sftp get file action
338         long size = getSize(fileName);
339         if (notifier != null)
340         {
341             notifier.notify(SFTP_GET_ACTION, currentDirectory + "/" + fileName, size);
342         }
343 
344         try
345         {
346             return channelSftp.get(fileName);
347         }
348         catch (SftpException e)
349         {
350             throw new IOException(e.getMessage() + ".  Filename is " + fileName);
351         }
352     }
353 
354     // public OutputStream storeFileStream(String fileName) throws IOException
355     // {
356     // try
357     // {
358     // return channelSftp.put(fileName);
359     // } catch (SftpException e)
360     // {
361     // throw new IOException(e.getMessage());
362     // }
363     // }
364 
365     public void storeFile(String fileName, InputStream stream) throws IOException
366     {
367         try
368         {
369 
370             // Notify sftp put file action
371             if (notifier != null)
372             {
373                 notifier.notify(SFTP_PUT_ACTION, currentDirectory + "/" + fileName);
374             }
375 
376             if (logger.isDebugEnabled())
377             {
378                 logger.debug("Sending to SFTP service: Stream = " + stream + " , filename = " + fileName);
379             }
380 
381             channelSftp.put(stream, fileName);
382         }
383         catch (SftpException e)
384         {
385             logger.error("Error writing data over SFTP service, error was: " + e.getMessage(), e);
386             throw new IOException(e.getMessage());
387         }
388     }
389 
390     public void storeFile(String fileNameLocal, String fileNameRemote) throws IOException
391     {
392         try
393         {
394             channelSftp.put(fileNameLocal, fileNameRemote);
395         }
396         catch (SftpException e)
397         {
398             throw new IOException(e.getMessage());
399         }
400     }
401 
402     public long getSize(String filename) throws IOException
403     {
404         try
405         {
406             return channelSftp.stat(filename).getSize();
407         }
408         catch (SftpException e)
409         {
410             throw new IOException(e.getMessage() + " (" + currentDirectory + "/" + filename + ")");
411         }
412     }
413 
414     /**
415      * @param filename File name
416      * @return Number of seconds since the file was written to
417      * @throws IOException If an error occurs
418      */
419     public long getLastModifiedTime(String filename) throws IOException
420     {
421         try
422         {
423             SftpATTRS attrs = channelSftp.stat("./" + filename);
424             return attrs.getMTime() * 1000L;
425         }
426         catch (SftpException e)
427         {
428             throw new IOException(e.getMessage());
429         }
430     }
431 
432     /**
433      * Creates a directory
434      *
435      * @param directoryName The directory name
436      * @throws IOException If an error occurs
437      */
438     public void mkdir(String directoryName) throws IOException
439     {
440         try
441         {
442             if (logger.isDebugEnabled())
443             {
444                 logger.debug("Will try to create directory " + directoryName);
445             }
446             channelSftp.mkdir(directoryName);
447         }
448         catch (SftpException e)
449         {
450             // Don't throw e.getmessage since we only get "2: No such file"..
451             throw new IOException("Could not create the directory '" + directoryName + "', caused by: "
452                                   + e.getMessage());
453             // throw new IOException("Could not create the directory '" +
454             // directoryName + "' in '" + currentDirectory + "', caused by: " +
455             // e.getMessage());
456         }
457     }
458 
459     public void deleteDirectory(String path) throws IOException
460     {
461         path = getAbsolutePath(path);
462         try
463         {
464             if (logger.isDebugEnabled())
465             {
466                 logger.debug("Will try to delete directory " + path);
467             }
468             channelSftp.rmdir(path);
469         }
470         catch (SftpException e)
471         {
472             throw new IOException(e.getMessage());
473         }
474     }
475 
476     /**
477      * Setter for 'home'
478      *
479      * @param home The path to home
480      */
481     void setHome(String home)
482     {
483         this.home = home;
484     }
485 
486     /**
487      * @return the ChannelSftp - useful for some tests
488      */
489     public ChannelSftp getChannelSftp()
490     {
491         return channelSftp;
492     }
493 
494     /**
495      * Creates the directory if it not already exists. TODO: check if the SftpUtil &
496      * SftpClient methods can be merged Note, this method is synchronized because it
497      * in rare cases can be called from two threads at the same time and thus cause
498      * an error.
499      *
500      * @param endpoint
501      * @param newDir
502      * @throws IOException
503      */
504     public void createSftpDirIfNotExists(ImmutableEndpoint endpoint, String newDir) throws IOException
505     {
506         String newDirAbs = endpoint.getEndpointURI().getPath() + "/" + newDir;
507 
508         String currDir = currentDirectory;
509 
510         if (logger.isDebugEnabled())
511         {
512             logger.debug("CHANGE DIR FROM " + currentDirectory + " TO " + newDirAbs);
513         }
514 
515         // We need to have a synchronized block if two++ threads tries to
516         // create the same directory at the same time
517         synchronized (lock)
518         {
519             // Try to change directory to the new dir, if it fails - create it
520             try
521             {
522                 // This method will throw an exception if the directory does not
523                 // exist.
524                 changeWorkingDirectory(newDirAbs);
525             }
526             catch (IOException e)
527             {
528                 logger.info("Got an exception when trying to change the working directory to the new dir. "
529                             + "Will try to create the directory " + newDirAbs);
530                 changeWorkingDirectory(endpoint.getEndpointURI().getPath());
531                 mkdir(newDir);
532 
533                 // Now it should exist!
534                 changeWorkingDirectory(newDirAbs);
535             }
536             finally
537             {
538                 changeWorkingDirectory(currDir);
539                 if (logger.isDebugEnabled())
540                 {
541                     logger.debug("DIR IS NOW BACK TO " + currentDirectory);
542                 }
543             }
544         }
545     }
546 
547     public String duplicateHandling(String destDir, String filename, String duplicateHandling)
548         throws IOException
549     {
550         if (duplicateHandling.equals(SftpConnector.PROPERTY_DUPLICATE_HANDLING_ASS_SEQ_NO))
551         {
552             filename = createUniqueName(destDir, filename);
553 
554         }
555         else if (duplicateHandling.equals(SftpConnector.PROPERTY_DUPLICATE_HANDLING_OVERWRITE))
556         {
557             // TODO. ML FIX. Implement this!
558             throw new NotImplementedException("Strategy "
559                                               + SftpConnector.PROPERTY_DUPLICATE_HANDLING_OVERWRITE
560                                               + " is not yet implemented");
561 
562         }
563         else
564         {
565             // Nothing to do in the case of
566             // PROPERTY_DUPLICATE_HANDLING_THROW_EXCEPTION, if the file already
567             // exists then an error will be throwed...
568         }
569 
570         return filename;
571     }
572 
573     private String createUniqueName(String dir, String path) throws IOException
574     {
575         int fileIdx = 1;
576 
577         String filename;
578         String fileType;
579         int fileTypeIdx = path.lastIndexOf('.');
580         if (fileTypeIdx == -1)
581         {
582             // No file type/extension found
583             filename = path;
584             fileType = "";
585         }
586         else
587         {
588             fileType = path.substring(fileTypeIdx); // Let the fileType include the
589             // leading '.'
590             filename = path.substring(0, fileTypeIdx);
591         }
592 
593         if (logger.isDebugEnabled())
594         {
595             logger.debug("Create a unique name for: " + path + " (" + dir + " - " + filename + " - "
596                          + fileType + ")");
597         }
598 
599         String uniqueFilename = filename;
600         String[] existingFiles = listFiles(getAbsolutePath(dir));
601 
602         while (existsFile(existingFiles, uniqueFilename, fileType))
603         {
604             uniqueFilename = filename + '_' + fileIdx++;
605         }
606 
607         uniqueFilename = uniqueFilename + fileType;
608         if (!path.equals(uniqueFilename) && logger.isInfoEnabled())
609         {
610             logger.info("A file with the original filename (" + dir + "/" + path
611                         + ") already exists, new name: " + uniqueFilename);
612         }
613         if (logger.isDebugEnabled())
614         {
615             logger.debug("Unique name returned: " + uniqueFilename);
616         }
617         return uniqueFilename;
618     }
619 
620     private boolean existsFile(String[] files, String filename, String fileType)
621     {
622         boolean existsFile = false;
623         filename += fileType;
624         for (String file : files)
625         {
626             if (file.equals(filename))
627             {
628                 if (logger.isDebugEnabled())
629                 {
630                     logger.debug("Found existing file: " + file);
631                 }
632                 existsFile = true;
633             }
634         }
635         return existsFile;
636     }
637 
638     public void chmod(String path, int permissions) throws SftpException
639     {
640         path = getAbsolutePath(path);
641         if (logger.isDebugEnabled())
642         {
643             logger.debug("Will try to chmod directory '" + path + "' to permission " + permissions);
644         }
645         channelSftp.chmod(permissions, path);
646     }
647 
648     public void setNotifier(SftpNotifier notifier)
649     {
650         this.notifier = notifier;
651     }
652 
653     public String getHost()
654     {
655         return host;
656     }
657 }