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