001/* 002 * Licensed to the Apache Software Foundation (ASF) under one or more 003 * contributor license agreements. See the NOTICE file distributed with 004 * this work for additional information regarding copyright ownership. 005 * The ASF licenses this file to You under the Apache License, Version 2.0 006 * (the "License"); you may not use this file except in compliance with 007 * the License. You may obtain a copy of the License at 008 * 009 * http://www.apache.org/licenses/LICENSE-2.0 010 * 011 * Unless required by applicable law or agreed to in writing, software 012 * distributed under the License is distributed on an "AS IS" BASIS, 013 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 014 * See the License for the specific language governing permissions and 015 * limitations under the License. 016 */ 017package org.apache.commons.vfs2.provider.sftp; 018 019import java.io.IOException; 020import java.io.InputStreamReader; 021import java.util.Collection; 022 023import org.apache.commons.vfs2.Capability; 024import org.apache.commons.vfs2.FileObject; 025import org.apache.commons.vfs2.FileSystemException; 026import org.apache.commons.vfs2.FileSystemOptions; 027import org.apache.commons.vfs2.UserAuthenticationData; 028import org.apache.commons.vfs2.provider.AbstractFileName; 029import org.apache.commons.vfs2.provider.AbstractFileSystem; 030import org.apache.commons.vfs2.provider.GenericFileName; 031import org.apache.commons.vfs2.util.UserAuthenticatorUtils; 032 033import com.jcraft.jsch.ChannelExec; 034import com.jcraft.jsch.ChannelSftp; 035import com.jcraft.jsch.JSchException; 036import com.jcraft.jsch.Session; 037import com.jcraft.jsch.SftpException; 038 039/** 040 * Represents the files on an SFTP server. 041 */ 042public class SftpFileSystem extends AbstractFileSystem { 043 private static final int SLEEP_MILLIS = 100; 044 045 private static final int EXEC_BUFFER_SIZE = 128; 046 047 private static final long LAST_MOD_TIME_ACCURACY = 1000L; 048 049 private Session session; 050 051 // private final JSch jSch; 052 053 private ChannelSftp idleChannel; 054 055 /** 056 * Cache for the user ID (-1 when not set) 057 */ 058 private int uid = -1; 059 060 /** 061 * Cache for the user groups ids (null when not set) 062 */ 063 private int[] groupsIds; 064 065 protected SftpFileSystem(final GenericFileName rootName, final Session session, 066 final FileSystemOptions fileSystemOptions) { 067 super(rootName, null, fileSystemOptions); 068 this.session = session; 069 } 070 071 @Override 072 protected void doCloseCommunicationLink() { 073 if (idleChannel != null) { 074 idleChannel.disconnect(); 075 idleChannel = null; 076 } 077 078 if (session != null) { 079 session.disconnect(); 080 session = null; 081 } 082 } 083 084 /** 085 * Returns an SFTP channel to the server. 086 * 087 * @return new or reused channel, never null. 088 * @throws FileSystemException if a session cannot be created. 089 * @throws IOException if an I/O error is detected. 090 */ 091 protected ChannelSftp getChannel() throws IOException { 092 ensureSession(); 093 try { 094 // Use the pooled channel, or create a new one 095 final ChannelSftp channel; 096 if (idleChannel != null) { 097 channel = idleChannel; 098 idleChannel = null; 099 } else { 100 channel = (ChannelSftp) session.openChannel("sftp"); 101 channel.connect(); 102 final Boolean userDirIsRoot = SftpFileSystemConfigBuilder.getInstance() 103 .getUserDirIsRoot(getFileSystemOptions()); 104 final String workingDirectory = getRootName().getPath(); 105 if (workingDirectory != null && (userDirIsRoot == null || !userDirIsRoot.booleanValue())) { 106 try { 107 channel.cd(workingDirectory); 108 } catch (final SftpException e) { 109 throw new FileSystemException("vfs.provider.sftp/change-work-directory.error", workingDirectory, 110 e); 111 } 112 } 113 } 114 115 final String fileNameEncoding = SftpFileSystemConfigBuilder.getInstance() 116 .getFileNameEncoding(getFileSystemOptions()); 117 118 if (fileNameEncoding != null) { 119 try { 120 channel.setFilenameEncoding(fileNameEncoding); 121 } catch (final SftpException e) { 122 throw new FileSystemException("vfs.provider.sftp/filename-encoding.error", fileNameEncoding); 123 } 124 } 125 return channel; 126 } catch (final JSchException e) { 127 throw new FileSystemException("vfs.provider.sftp/connect.error", getRootName(), e); 128 } 129 } 130 131 /** 132 * Ensures that the session link is established. 133 * 134 * @throws FileSystemException if a session cannot be created. 135 */ 136 private void ensureSession() throws FileSystemException { 137 if (this.session == null || !this.session.isConnected()) { 138 doCloseCommunicationLink(); 139 140 // channel closed. e.g. by freeUnusedResources, but now we need it again 141 Session session; 142 UserAuthenticationData authData = null; 143 try { 144 final GenericFileName rootName = (GenericFileName) getRootName(); 145 146 authData = UserAuthenticatorUtils.authenticate(getFileSystemOptions(), 147 SftpFileProvider.AUTHENTICATOR_TYPES); 148 149 session = SftpClientFactory.createConnection(rootName.getHostName(), rootName.getPort(), 150 UserAuthenticatorUtils.getData(authData, UserAuthenticationData.USERNAME, 151 UserAuthenticatorUtils.toChar(rootName.getUserName())), 152 UserAuthenticatorUtils.getData(authData, UserAuthenticationData.PASSWORD, 153 UserAuthenticatorUtils.toChar(rootName.getPassword())), 154 getFileSystemOptions()); 155 } catch (final Exception e) { 156 throw new FileSystemException("vfs.provider.sftp/connect.error", getRootName(), e); 157 } finally { 158 UserAuthenticatorUtils.cleanup(authData); 159 } 160 this.session = session; 161 } 162 } 163 164 /** 165 * Returns a channel to the pool. 166 * 167 * @param channel the used channel. 168 */ 169 protected void putChannel(final ChannelSftp channel) { 170 if (idleChannel == null) { 171 // put back the channel only if it is still connected 172 if (channel.isConnected() && !channel.isClosed()) { 173 idleChannel = channel; 174 } 175 } else { 176 channel.disconnect(); 177 } 178 } 179 180 /** 181 * Adds the capabilities of this file system. 182 */ 183 @Override 184 protected void addCapabilities(final Collection<Capability> caps) { 185 caps.addAll(SftpFileProvider.capabilities); 186 } 187 188 /** 189 * Creates a file object. This method is called only if the requested file is not cached. 190 */ 191 @Override 192 protected FileObject createFile(final AbstractFileName name) throws FileSystemException { 193 return new SftpFileObject(name, this); 194 } 195 196 /** 197 * Last modification time is only an int and in seconds, thus can be off by 999. 198 * 199 * @return 1000 200 */ 201 @Override 202 public double getLastModTimeAccuracy() { 203 return LAST_MOD_TIME_ACCURACY; 204 } 205 206 /** 207 * Gets the (numeric) group IDs. 208 * 209 * @return the (numeric) group IDs. 210 * @throws JSchException If a problem occurs while retrieving the group IDs. 211 * @throws IOException if an I/O error is detected. 212 * @since 2.1 213 */ 214 public int[] getGroupsIds() throws JSchException, IOException { 215 if (groupsIds == null) { 216 final StringBuilder output = new StringBuilder(); 217 final int code = executeCommand("id -G", output); 218 if (code != 0) { 219 throw new JSchException("Could not get the groups id of the current user (error code: " + code + ")"); 220 } 221 222 // Retrieve the different groups 223 final String[] groups = output.toString().trim().split("\\s+"); 224 225 final int[] groupsIds = new int[groups.length]; 226 for (int i = 0; i < groups.length; i++) { 227 groupsIds[i] = Integer.parseInt(groups[i]); 228 } 229 230 this.groupsIds = groupsIds; 231 } 232 return groupsIds; 233 } 234 235 /** 236 * Get the (numeric) group IDs. 237 * 238 * @return The numeric user ID 239 * @throws JSchException If a problem occurs while retrieving the group ID. 240 * @throws IOException if an I/O error is detected. 241 * @since 2.1 242 */ 243 public int getUId() throws JSchException, IOException { 244 if (uid < 0) { 245 final StringBuilder output = new StringBuilder(); 246 final int code = executeCommand("id -u", output); 247 if (code != 0) { 248 throw new FileSystemException( 249 "Could not get the user id of the current user (error code: " + code + ")"); 250 } 251 uid = Integer.parseInt(output.toString().trim()); 252 } 253 return uid; 254 } 255 256 /** 257 * Execute a command and returns the (standard) output through a StringBuilder. 258 * 259 * @param command The command 260 * @param output The output 261 * @return The exit code of the command 262 * @throws JSchException if a JSch error is detected. 263 * @throws FileSystemException if a session cannot be created. 264 * @throws IOException if an I/O error is detected. 265 */ 266 private int executeCommand(final String command, final StringBuilder output) throws JSchException, IOException { 267 ensureSession(); 268 final ChannelExec channel = (ChannelExec) session.openChannel("exec"); 269 270 channel.setCommand(command); 271 channel.setInputStream(null); 272 try (final InputStreamReader stream = new InputStreamReader(channel.getInputStream())) { 273 channel.setErrStream(System.err, true); 274 channel.connect(); 275 276 // Read the stream 277 final char[] buffer = new char[EXEC_BUFFER_SIZE]; 278 int read; 279 while ((read = stream.read(buffer, 0, buffer.length)) >= 0) { 280 output.append(buffer, 0, read); 281 } 282 } 283 284 // Wait until the command finishes (should not be long since we read the output stream) 285 while (!channel.isClosed()) { 286 try { 287 Thread.sleep(SLEEP_MILLIS); 288 } catch (final Exception ee) { 289 // TODO: swallow exception, really? 290 } 291 } 292 channel.disconnect(); 293 return channel.getExitStatus(); 294 } 295}