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  import java.util.Objects;
23  
24  import org.apache.commons.logging.Log;
25  import org.apache.commons.logging.LogFactory;
26  import org.apache.commons.vfs2.Capability;
27  import org.apache.commons.vfs2.FileObject;
28  import org.apache.commons.vfs2.FileSystemException;
29  import org.apache.commons.vfs2.FileSystemOptions;
30  import org.apache.commons.vfs2.provider.AbstractFileName;
31  import org.apache.commons.vfs2.provider.AbstractFileSystem;
32  import org.apache.commons.vfs2.provider.GenericFileName;
33  
34  import com.jcraft.jsch.ChannelExec;
35  import com.jcraft.jsch.ChannelSftp;
36  import com.jcraft.jsch.JSchException;
37  import com.jcraft.jsch.Session;
38  import com.jcraft.jsch.SftpException;
39  
40  /**
41   * Represents the files on an SFTP server.
42   */
43  public class SftpFileSystem extends AbstractFileSystem {
44  
45      private static final Log LOG = LogFactory.getLog(SftpFileSystem.class);
46  
47      private static final int UNIDENTIFED = -1;
48  
49      private static final int SLEEP_MILLIS = 100;
50  
51      private static final int EXEC_BUFFER_SIZE = 128;
52  
53      private static final long LAST_MOD_TIME_ACCURACY = 1000L;
54  
55      /**
56       * Session; never null.
57       * <p>
58       * DCL pattern requires that the ivar be volatile.
59       * </p>
60       */
61      private volatile Session session;
62  
63      private volatile ChannelSftp idleChannel;
64  
65      private final int connectTimeoutMillis;
66  
67      /**
68       * Cache for the user ID (-1 when not set)
69       * <p>
70       * DCL pattern requires that the ivar be volatile.
71       * </p>
72       */
73      private volatile int uid = UNIDENTIFED;
74  
75      /**
76       * Cache for the user groups ids (null when not set)
77       * <p>
78       * DCL pattern requires that the ivar be volatile.
79       * </p>
80       */
81      private volatile int[] groupsIds;
82  
83      /**
84       * Some SFTP-only servers disable the exec channel. When exec is disabled, things like getUId() will always fail.
85       */
86      private final boolean execDisabled;
87  
88      protected SftpFileSystem(final GenericFileName rootName, final Session session,
89              final FileSystemOptions fileSystemOptions) {
90          super(rootName, null, fileSystemOptions);
91          this.session = Objects.requireNonNull(session, "session");
92          this.connectTimeoutMillis = SftpFileSystemConfigBuilder.getInstance()
93                  .getConnectTimeoutMillis(fileSystemOptions);
94          this.execDisabled = detectExecDisabled();
95      }
96  
97      @Override
98      protected void doCloseCommunicationLink() {
99          if (idleChannel != null) {
100             synchronized (this) {
101                 if (idleChannel != null) {
102                     idleChannel.disconnect();
103                     idleChannel = null;
104                 }
105             }
106         }
107 
108         if (session != null) {
109             session.disconnect();
110         }
111     }
112 
113     /**
114      * Returns an SFTP channel to the server.
115      *
116      * @return new or reused channel, never null.
117      * @throws FileSystemException if a session cannot be created.
118      * @throws IOException         if an I/O error is detected.
119      */
120     protected ChannelSftp getChannel() throws IOException {
121         try {
122             // Use the pooled channel, or create a new one
123             ChannelSftp channel = null;
124             if (idleChannel != null) {
125                 synchronized (this) {
126                     if (idleChannel != null) {
127                         channel = idleChannel;
128                         idleChannel = null;
129                     }
130                 }
131             } else {
132                 channel = (ChannelSftp) getSession().openChannel("sftp");
133                 channel.connect(connectTimeoutMillis);
134                 final Boolean userDirIsRoot = SftpFileSystemConfigBuilder.getInstance()
135                         .getUserDirIsRoot(getFileSystemOptions());
136                 final String workingDirectory = getRootName().getPath();
137                 if (workingDirectory != null && (userDirIsRoot == null || !userDirIsRoot.booleanValue())) {
138                     try {
139                         channel.cd(workingDirectory);
140                     } catch (final SftpException e) {
141                         throw new FileSystemException("vfs.provider.sftp/change-work-directory.error", workingDirectory,
142                                 e);
143                     }
144                 }
145             }
146 
147             final String fileNameEncoding = SftpFileSystemConfigBuilder.getInstance()
148                     .getFileNameEncoding(getFileSystemOptions());
149 
150             if (fileNameEncoding != null) {
151                 try {
152                     channel.setFilenameEncoding(fileNameEncoding);
153                 } catch (final SftpException e) {
154                     throw new FileSystemException("vfs.provider.sftp/filename-encoding.error", fileNameEncoding);
155                 }
156             }
157             return channel;
158         } catch (final JSchException e) {
159             throw new FileSystemException("vfs.provider.sftp/connect.error", getRootName(), e);
160         }
161     }
162 
163     /**
164      * Ensures that the session link is established.
165      *
166      * @throws FileSystemException if a session cannot be created.
167      */
168     private Session getSession() throws FileSystemException {
169         if (!this.session.isConnected()) {
170             synchronized (this) {
171                 if (!this.session.isConnected()) {
172                     doCloseCommunicationLink();
173                     this.session = SftpFileProvider.createSession((GenericFileName) getRootName(),
174                             getFileSystemOptions());
175                 }
176             }
177         }
178         return this.session;
179     }
180 
181     /**
182      * Returns a channel to the pool.
183      *
184      * @param channel the used channel.
185      */
186     protected void putChannel(final ChannelSftp channel) {
187         if (idleChannel == null) {
188             synchronized (this) {
189                 if (idleChannel == null) {
190                     // put back the channel only if it is still connected
191                     if (channel.isConnected() && !channel.isClosed()) {
192                         idleChannel = channel;
193                     }
194                 } else {
195                     channel.disconnect();
196                 }
197             }
198         } else {
199             channel.disconnect();
200         }
201     }
202 
203     /**
204      * Adds the capabilities of this file system.
205      */
206     @Override
207     protected void addCapabilities(final Collection<Capability> caps) {
208         caps.addAll(SftpFileProvider.capabilities);
209     }
210 
211     /**
212      * Creates a file object. This method is called only if the requested file is not cached.
213      */
214     @Override
215     protected FileObject createFile(final AbstractFileName name) throws FileSystemException {
216         return new SftpFileObject(name, this);
217     }
218 
219     /**
220      * Last modification time is only an int and in seconds, thus can be off by 999.
221      *
222      * @return 1000
223      */
224     @Override
225     public double getLastModTimeAccuracy() {
226         return LAST_MOD_TIME_ACCURACY;
227     }
228 
229     /**
230      * Gets the (numeric) group IDs.
231      *
232      * @return the (numeric) group IDs.
233      * @throws JSchException If a problem occurs while retrieving the group IDs.
234      * @throws IOException   if an I/O error is detected.
235      * @since 2.1
236      */
237     public int[] getGroupsIds() throws JSchException, IOException {
238         if (groupsIds == null) {
239             synchronized (this) {
240                 // DCL pattern requires that the ivar be volatile.
241                 if (groupsIds == null) {
242                     final StringBuilder output = new StringBuilder();
243                     final int code = executeCommand("id -G", output);
244                     if (code != 0) {
245                         throw new JSchException(
246                                 "Could not get the groups id of the current user (error code: " + code + ")");
247                     }
248                     // Retrieve the different groups
249                     final String[] groups = output.toString().trim().split("\\s+");
250 
251                     final int[] groupsIds = new int[groups.length];
252                     for (int i = 0; i < groups.length; i++) {
253                         groupsIds[i] = Integer.parseInt(groups[i]);
254                     }
255                     this.groupsIds = groupsIds;
256 
257                 }
258             }
259         }
260         return groupsIds;
261     }
262 
263     /**
264      * Gets the (numeric) group IDs.
265      *
266      * @return The numeric user ID
267      * @throws JSchException If a problem occurs while retrieving the group ID.
268      * @throws IOException   if an I/O error is detected.
269      * @since 2.1
270      */
271     public int getUId() throws JSchException, IOException {
272         if (uid == UNIDENTIFED) {
273             synchronized (this) {
274                 if (uid == UNIDENTIFED) {
275                     final StringBuilder output = new StringBuilder();
276                     final int code = executeCommand("id -u", output);
277                     if (code != 0) {
278                         throw new FileSystemException(
279                                 "Could not get the user id of the current user (error code: " + code + ")");
280                     }
281                     uid = Integer.parseInt(output.toString().trim());
282                 }
283             }
284         }
285         return uid;
286     }
287 
288     /**
289      * Executes a command and returns the (standard) output through a StringBuilder.
290      *
291      * @param command The command
292      * @param output  The output
293      * @return The exit code of the command
294      * @throws JSchException       if a JSch error is detected.
295      * @throws FileSystemException if a session cannot be created.
296      * @throws IOException         if an I/O error is detected.
297      */
298     private int executeCommand(final String command, final StringBuilder output) throws JSchException, IOException {
299         final ChannelExec channel = (ChannelExec) getSession().openChannel("exec");
300         try {
301             channel.setCommand(command);
302             channel.setInputStream(null);
303             try (final InputStreamReader stream = new InputStreamReader(channel.getInputStream())) {
304                 channel.setErrStream(System.err, true);
305                 channel.connect(connectTimeoutMillis);
306 
307                 // Read the stream
308                 final char[] buffer = new char[EXEC_BUFFER_SIZE];
309                 int read;
310                 while ((read = stream.read(buffer, 0, buffer.length)) >= 0) {
311                     output.append(buffer, 0, read);
312                 }
313             }
314 
315             // Wait until the command finishes (should not be long since we read the output stream)
316             while (!channel.isClosed()) {
317                 try {
318                     Thread.sleep(SLEEP_MILLIS);
319                 } catch (final Exception ee) {
320                     // TODO: swallow exception, really?
321                 }
322             }
323         } finally {
324             channel.disconnect();
325         }
326         return channel.getExitStatus();
327     }
328 
329     /**
330      * @return Whether the exec channel is disabled.
331      * @see SftpFileSystem#execDisabled
332      */
333     public boolean isExecDisabled() {
334         return execDisabled;
335     }
336 
337     /**
338      * Some SFTP-only servers disable the exec channel.
339      *
340      * Attempt to detect this by calling getUid.
341      */
342     private boolean detectExecDisabled() {
343         try {
344             getUId();
345             return false;
346         } catch(JSchException | IOException e) {
347             LOG.debug("Cannot get UID, assuming no exec channel is present", e);
348             return true;
349         }
350     }
351 
352 }