View Javadoc
1   /*
2    * Licensed to the Apache Software Foundation (ASF) under one or more
3    * contributor license agreements.  See the NOTICE file distributed with
4    * this work for additional information regarding copyright ownership.
5    * The ASF licenses this file to You under the Apache License, Version 2.0
6    * (the "License"); you may not use this file except in compliance with
7    * the License.  You may obtain a copy of the License at
8    *
9    *      http://www.apache.org/licenses/LICENSE-2.0
10   *
11   * Unless required by applicable law or agreed to in writing, software
12   * distributed under the License is distributed on an "AS IS" BASIS,
13   * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14   * See the License for the specific language governing permissions and
15   * limitations under the License.
16   */
17  package org.apache.commons.vfs2.provider.sftp;
18  
19  import java.io.IOException;
20  import java.io.InputStreamReader;
21  import java.util.Collection;
22  
23  import org.apache.commons.vfs2.Capability;
24  import org.apache.commons.vfs2.FileObject;
25  import org.apache.commons.vfs2.FileSystemException;
26  import org.apache.commons.vfs2.FileSystemOptions;
27  import org.apache.commons.vfs2.UserAuthenticationData;
28  import org.apache.commons.vfs2.provider.AbstractFileName;
29  import org.apache.commons.vfs2.provider.AbstractFileSystem;
30  import org.apache.commons.vfs2.provider.GenericFileName;
31  import org.apache.commons.vfs2.util.UserAuthenticatorUtils;
32  
33  import com.jcraft.jsch.ChannelExec;
34  import com.jcraft.jsch.ChannelSftp;
35  import com.jcraft.jsch.JSchException;
36  import com.jcraft.jsch.Session;
37  import com.jcraft.jsch.SftpException;
38  
39  /**
40   * Represents the files on an SFTP server.
41   */
42  public class SftpFileSystem
43      extends AbstractFileSystem
44  {
45      private static final int SLEEP_MILLIS = 100;
46  
47      private static final int EXEC_BUFFER_SIZE = 128;
48  
49      private static final long LAST_MOD_TIME_ACCURACY = 1000L;
50  
51      private Session session;
52  
53      // private final JSch jSch;
54  
55      private ChannelSftp idleChannel;
56  
57      /**
58       * Cache for the user ID (-1 when not set)
59       */
60      private int uid = -1;
61  
62      /**
63       * Cache for the user groups ids (null when not set)
64       */
65      private int[] groupsIds;
66  
67      protected SftpFileSystem(final GenericFileName rootName,
68                               final Session session,
69                               final FileSystemOptions fileSystemOptions)
70      {
71          super(rootName, null, fileSystemOptions);
72          this.session = session;
73      }
74  
75      @Override
76      protected void doCloseCommunicationLink()
77      {
78          if (idleChannel != null)
79          {
80              idleChannel.disconnect();
81              idleChannel = null;
82          }
83  
84          if (session != null)
85          {
86              session.disconnect();
87              session = null;
88          }
89      }
90  
91      /**
92       * Returns an SFTP channel to the server.
93       *
94       * @return new or reused channel, never null.
95       * @throws FileSystemException if a session cannot be created.
96       * @throws IOException if an I/O error is detected.
97       */
98      protected ChannelSftp getChannel() throws IOException
99      {
100         ensureSession();
101         try
102         {
103             // Use the pooled channel, or create a new one
104             final ChannelSftp channel;
105             if (idleChannel != null)
106             {
107                 channel = idleChannel;
108                 idleChannel = null;
109             }
110             else
111             {
112                 channel = (ChannelSftp) session.openChannel("sftp");
113                 channel.connect();
114                 final Boolean userDirIsRoot =
115                     SftpFileSystemConfigBuilder.getInstance().getUserDirIsRoot(getFileSystemOptions());
116                 final String workingDirectory = getRootName().getPath();
117                 if (workingDirectory != null && (userDirIsRoot == null || !userDirIsRoot.booleanValue()))
118                 {
119                     try
120                     {
121                         channel.cd(workingDirectory);
122                     }
123                     catch (final SftpException e)
124                     {
125                         throw new FileSystemException("vfs.provider.sftp/change-work-directory.error",
126                             workingDirectory, e);
127                     }
128                 }
129             }
130 
131             final String fileNameEncoding = SftpFileSystemConfigBuilder.getInstance().getFileNameEncoding(
132                     getFileSystemOptions());
133 
134             if (fileNameEncoding != null)
135             {
136                 try
137                 {
138                     channel.setFilenameEncoding(fileNameEncoding);
139                 }
140                 catch (final SftpException e)
141                 {
142                     throw new FileSystemException("vfs.provider.sftp/filename-encoding.error", fileNameEncoding);
143                 }
144             }
145             return channel;
146         }
147         catch (final JSchException e)
148         {
149             throw new FileSystemException("vfs.provider.sftp/connect.error", getRootName(), e);
150         }
151     }
152 
153     /**
154      * Ensures that the session link is established.
155      *
156      * @throws FileSystemException if a session cannot be created.
157      */
158     private void ensureSession() throws FileSystemException
159     {
160         if (this.session == null || !this.session.isConnected())
161         {
162             doCloseCommunicationLink();
163 
164             // channel closed. e.g. by freeUnusedResources, but now we need it again
165             Session session;
166             UserAuthenticationData authData = null;
167             try
168             {
169                 final GenericFileName rootName = (GenericFileName) getRootName();
170 
171                 authData = UserAuthenticatorUtils.authenticate(getFileSystemOptions(),
172                         SftpFileProvider.AUTHENTICATOR_TYPES);
173 
174                 session = SftpClientFactory.createConnection(
175                         rootName.getHostName(),
176                         rootName.getPort(),
177                         UserAuthenticatorUtils.getData(authData, UserAuthenticationData.USERNAME,
178                                 UserAuthenticatorUtils.toChar(rootName.getUserName())),
179                         UserAuthenticatorUtils.getData(authData, UserAuthenticationData.PASSWORD,
180                                 UserAuthenticatorUtils.toChar(rootName.getPassword())),
181                         getFileSystemOptions());
182             }
183             catch (final Exception e)
184             {
185                 throw new FileSystemException("vfs.provider.sftp/connect.error", getRootName(), e);
186             }
187             finally
188             {
189                 UserAuthenticatorUtils.cleanup(authData);
190             }
191             this.session = session;
192         }
193     }
194 
195     /**
196      * Returns a channel to the pool.
197      *
198      * @param channel the used channel.
199      */
200     protected void putChannel(final ChannelSftp channel)
201     {
202         if (idleChannel == null)
203         {
204             // put back the channel only if it is still connected
205             if (channel.isConnected() && !channel.isClosed())
206             {
207                 idleChannel = channel;
208             }
209         }
210         else
211         {
212             channel.disconnect();
213         }
214     }
215 
216     /**
217      * Adds the capabilities of this file system.
218      */
219     @Override
220     protected void addCapabilities(final Collection<Capability> caps)
221     {
222         caps.addAll(SftpFileProvider.capabilities);
223     }
224 
225     /**
226      * Creates a file object.  This method is called only if the requested
227      * file is not cached.
228      */
229     @Override
230     protected FileObject createFile(final AbstractFileName name)
231         throws FileSystemException
232     {
233         return new SftpFileObject(name, this);
234     }
235 
236     /**
237      * Last modification time is only an int and in seconds, thus can be off by 999.
238      *
239      * @return 1000
240      */
241     @Override
242     public double getLastModTimeAccuracy()
243     {
244         return LAST_MOD_TIME_ACCURACY;
245     }
246 
247     /**
248      * Gets the (numeric) group IDs.
249      *
250      * @return the (numeric) group IDs.
251      * @throws JSchException If a problem occurs while retrieving the group IDs.
252      * @throws IOException if an I/O error is detected.
253      * @since 2.1
254      */
255     public int[] getGroupsIds() throws JSchException, IOException
256     {
257         if (groupsIds == null)
258         {
259             final StringBuilder output = new StringBuilder();
260             final int code = executeCommand("id -G", output);
261             if (code != 0)
262             {
263                 throw new JSchException("Could not get the groups id of the current user (error code: " + code + ")");
264             }
265 
266             // Retrieve the different groups
267             final String[] groups = output.toString().trim().split("\\s+");
268 
269             final int[] groupsIds = new int[groups.length];
270             for (int i = 0; i < groups.length; i++)
271             {
272                 groupsIds[i] = Integer.parseInt(groups[i]);
273             }
274 
275             this.groupsIds = groupsIds;
276         }
277         return groupsIds;
278     }
279 
280     /**
281      * Get the (numeric) group IDs.
282      *
283      * @return The numeric user ID
284      * @throws JSchException If a problem occurs while retrieving the group ID.
285      * @throws IOException if an I/O error is detected.
286      * @since 2.1
287      */
288     public int getUId() throws JSchException, IOException
289     {
290         if (uid < 0)
291         {
292             final StringBuilder output = new StringBuilder();
293             final int code = executeCommand("id -u", output);
294             if (code != 0)
295             {
296                 throw new FileSystemException("Could not get the user id of the current user (error code: " + code
297                         + ")");
298             }
299             uid = Integer.parseInt(output.toString().trim());
300         }
301         return uid;
302     }
303 
304     /**
305      * Execute a command and returns the (standard) output through a StringBuilder.
306      *
307      * @param command The command
308      * @param output The output
309      * @return The exit code of the command
310      * @throws JSchException if a JSch error is detected.
311      * @throws FileSystemException if a session cannot be created.
312      * @throws IOException if an I/O error is detected.
313      */
314     private int executeCommand(final String command, final StringBuilder output) throws JSchException, IOException
315     {
316         ensureSession();
317         final ChannelExec channel = (ChannelExec) session.openChannel("exec");
318 
319         channel.setCommand(command);
320         channel.setInputStream(null);
321         final InputStreamReader stream = new InputStreamReader(channel.getInputStream());
322         channel.setErrStream(System.err, true);
323         channel.connect();
324 
325         // Read the stream
326         final char[] buffer = new char[EXEC_BUFFER_SIZE];
327         int read;
328         while ((read = stream.read(buffer, 0, buffer.length)) >= 0)
329         {
330             output.append(buffer, 0, read);
331         }
332         stream.close();
333 
334         // Wait until the command finishes (should not be long since we read the output stream)
335         while (!channel.isClosed())
336         {
337             try
338             {
339                 Thread.sleep(SLEEP_MILLIS);
340             }
341             catch (final Exception ee)
342             {
343                 // TODO: swallow exception, really?
344             }
345         }
346         channel.disconnect();
347         return channel.getExitStatus();
348     }
349 }