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